blob: 340a9f55198431ead9b433ced0da1f3b75df986e [file] [log] [blame]
The Android Open Source Project88b60792009-03-03 19:28:42 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.signapk;
18
19import sun.misc.BASE64Encoder;
20import sun.security.pkcs.ContentInfo;
21import sun.security.pkcs.PKCS7;
22import sun.security.pkcs.SignerInfo;
23import sun.security.x509.AlgorithmId;
24import sun.security.x509.X500Name;
25
26import java.io.BufferedReader;
27import java.io.ByteArrayOutputStream;
28import java.io.DataInputStream;
29import java.io.File;
30import java.io.FileInputStream;
31import java.io.FileOutputStream;
32import java.io.FilterOutputStream;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.InputStreamReader;
36import java.io.OutputStream;
37import java.io.PrintStream;
38import java.security.AlgorithmParameters;
39import java.security.DigestOutputStream;
40import java.security.GeneralSecurityException;
41import java.security.Key;
42import java.security.KeyFactory;
43import java.security.MessageDigest;
44import java.security.PrivateKey;
45import java.security.Signature;
46import java.security.SignatureException;
47import java.security.cert.Certificate;
48import java.security.cert.CertificateFactory;
49import java.security.cert.X509Certificate;
50import java.security.spec.InvalidKeySpecException;
51import java.security.spec.KeySpec;
52import java.security.spec.PKCS8EncodedKeySpec;
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.Date;
56import java.util.Enumeration;
57import java.util.List;
58import java.util.Map;
59import java.util.TreeMap;
60import java.util.jar.Attributes;
61import java.util.jar.JarEntry;
62import java.util.jar.JarFile;
63import java.util.jar.JarOutputStream;
64import java.util.jar.Manifest;
65import javax.crypto.Cipher;
66import javax.crypto.EncryptedPrivateKeyInfo;
67import javax.crypto.SecretKeyFactory;
68import javax.crypto.spec.PBEKeySpec;
69
70/**
71 * Command line tool to sign JAR files (including APKs and OTA updates) in
72 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
73 */
74class SignApk {
75 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
76 private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
77
78 private static X509Certificate readPublicKey(File file)
79 throws IOException, GeneralSecurityException {
80 FileInputStream input = new FileInputStream(file);
81 try {
82 CertificateFactory cf = CertificateFactory.getInstance("X.509");
83 return (X509Certificate) cf.generateCertificate(input);
84 } finally {
85 input.close();
86 }
87 }
88
89 /**
90 * Reads the password from stdin and returns it as a string.
91 *
92 * @param keyFile The file containing the private key. Used to prompt the user.
93 */
94 private static String readPassword(File keyFile) {
95 // TODO: use Console.readPassword() when it's available.
96 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
97 System.out.flush();
98 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
99 try {
100 return stdin.readLine();
101 } catch (IOException ex) {
102 return null;
103 }
104 }
105
106 /**
107 * Decrypt an encrypted PKCS 8 format private key.
108 *
109 * Based on ghstark's post on Aug 6, 2006 at
110 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
111 *
112 * @param encryptedPrivateKey The raw data of the private key
113 * @param keyFile The file containing the private key
114 */
115 private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
116 throws GeneralSecurityException {
117 EncryptedPrivateKeyInfo epkInfo;
118 try {
119 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
120 } catch (IOException ex) {
121 // Probably not an encrypted key.
122 return null;
123 }
124
125 char[] password = readPassword(keyFile).toCharArray();
126
127 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
128 Key key = skFactory.generateSecret(new PBEKeySpec(password));
129
130 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
131 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
132
133 try {
134 return epkInfo.getKeySpec(cipher);
135 } catch (InvalidKeySpecException ex) {
136 System.err.println("signapk: Password for " + keyFile + " may be bad.");
137 throw ex;
138 }
139 }
140
141 /** Read a PKCS 8 format private key. */
142 private static PrivateKey readPrivateKey(File file)
143 throws IOException, GeneralSecurityException {
144 DataInputStream input = new DataInputStream(new FileInputStream(file));
145 try {
146 byte[] bytes = new byte[(int) file.length()];
147 input.read(bytes);
148
149 KeySpec spec = decryptPrivateKey(bytes, file);
150 if (spec == null) {
151 spec = new PKCS8EncodedKeySpec(bytes);
152 }
153
154 try {
155 return KeyFactory.getInstance("RSA").generatePrivate(spec);
156 } catch (InvalidKeySpecException ex) {
157 return KeyFactory.getInstance("DSA").generatePrivate(spec);
158 }
159 } finally {
160 input.close();
161 }
162 }
163
164 /** Add the SHA1 of every file to the manifest, creating it if necessary. */
165 private static Manifest addDigestsToManifest(JarFile jar)
166 throws IOException, GeneralSecurityException {
167 Manifest input = jar.getManifest();
168 Manifest output = new Manifest();
169 Attributes main = output.getMainAttributes();
170 if (input != null) {
171 main.putAll(input.getMainAttributes());
172 } else {
173 main.putValue("Manifest-Version", "1.0");
174 main.putValue("Created-By", "1.0 (Android SignApk)");
175 }
176
177 BASE64Encoder base64 = new BASE64Encoder();
178 MessageDigest md = MessageDigest.getInstance("SHA1");
179 byte[] buffer = new byte[4096];
180 int num;
181
182 // We sort the input entries by name, and add them to the
183 // output manifest in sorted order. We expect that the output
184 // map will be deterministic.
185
186 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
187
188 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
189 JarEntry entry = e.nextElement();
190 byName.put(entry.getName(), entry);
191 }
192
193 for (JarEntry entry: byName.values()) {
194 String name = entry.getName();
195 if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
196 !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME)) {
197 InputStream data = jar.getInputStream(entry);
198 while ((num = data.read(buffer)) > 0) {
199 md.update(buffer, 0, num);
200 }
201
202 Attributes attr = null;
203 if (input != null) attr = input.getAttributes(name);
204 attr = attr != null ? new Attributes(attr) : new Attributes();
205 attr.putValue("SHA1-Digest", base64.encode(md.digest()));
206 output.getEntries().put(name, attr);
207 }
208 }
209
210 return output;
211 }
212
213 /** Write to another stream and also feed it to the Signature object. */
214 private static class SignatureOutputStream extends FilterOutputStream {
215 private Signature mSignature;
216
217 public SignatureOutputStream(OutputStream out, Signature sig) {
218 super(out);
219 mSignature = sig;
220 }
221
222 @Override
223 public void write(int b) throws IOException {
224 try {
225 mSignature.update((byte) b);
226 } catch (SignatureException e) {
227 throw new IOException("SignatureException: " + e);
228 }
229 super.write(b);
230 }
231
232 @Override
233 public void write(byte[] b, int off, int len) throws IOException {
234 try {
235 mSignature.update(b, off, len);
236 } catch (SignatureException e) {
237 throw new IOException("SignatureException: " + e);
238 }
239 super.write(b, off, len);
240 }
241 }
242
243 /** Write a .SF file with a digest the specified manifest. */
244 private static void writeSignatureFile(Manifest manifest, OutputStream out)
245 throws IOException, GeneralSecurityException {
246 Manifest sf = new Manifest();
247 Attributes main = sf.getMainAttributes();
248 main.putValue("Signature-Version", "1.0");
249 main.putValue("Created-By", "1.0 (Android SignApk)");
250
251 BASE64Encoder base64 = new BASE64Encoder();
252 MessageDigest md = MessageDigest.getInstance("SHA1");
253 PrintStream print = new PrintStream(
254 new DigestOutputStream(new ByteArrayOutputStream(), md),
255 true, "UTF-8");
256
257 // Digest of the entire manifest
258 manifest.write(print);
259 print.flush();
260 main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
261
262 Map<String, Attributes> entries = manifest.getEntries();
263 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
264 // Digest of the manifest stanza for this entry.
265 print.print("Name: " + entry.getKey() + "\r\n");
266 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
267 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
268 }
269 print.print("\r\n");
270 print.flush();
271
272 Attributes sfAttr = new Attributes();
273 sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
274 sf.getEntries().put(entry.getKey(), sfAttr);
275 }
276
277 sf.write(out);
278 }
279
280 /** Write a .RSA file with a digital signature. */
281 private static void writeSignatureBlock(
282 Signature signature, X509Certificate publicKey, OutputStream out)
283 throws IOException, GeneralSecurityException {
284 SignerInfo signerInfo = new SignerInfo(
285 new X500Name(publicKey.getIssuerX500Principal().getName()),
286 publicKey.getSerialNumber(),
287 AlgorithmId.get("SHA1"),
288 AlgorithmId.get("RSA"),
289 signature.sign());
290
291 PKCS7 pkcs7 = new PKCS7(
292 new AlgorithmId[] { AlgorithmId.get("SHA1") },
293 new ContentInfo(ContentInfo.DATA_OID, null),
294 new X509Certificate[] { publicKey },
295 new SignerInfo[] { signerInfo });
296
297 pkcs7.encodeSignedData(out);
298 }
299
300 /** Copy all the files in a manifest from input to output. */
301 private static void copyFiles(Manifest manifest,
302 JarFile in, JarOutputStream out) throws IOException {
303 byte[] buffer = new byte[4096];
304 int num;
305
306 Map<String, Attributes> entries = manifest.getEntries();
307 List<String> names = new ArrayList(entries.keySet());
308 Collections.sort(names);
309 for (String name : names) {
310 JarEntry inEntry = in.getJarEntry(name);
311 if (inEntry.getMethod() == JarEntry.STORED) {
312 // Preserve the STORED method of the input entry.
313 out.putNextEntry(new JarEntry(inEntry));
314 } else {
315 // Create a new entry so that the compressed len is recomputed.
316 JarEntry je = new JarEntry(name);
317 je.setTime(inEntry.getTime());
318 out.putNextEntry(je);
319 }
320
321 InputStream data = in.getInputStream(inEntry);
322 while ((num = data.read(buffer)) > 0) {
323 out.write(buffer, 0, num);
324 }
325 out.flush();
326 }
327 }
328
329 public static void main(String[] args) {
330 if (args.length != 4) {
331 System.err.println("Usage: signapk " +
332 "publickey.x509[.pem] privatekey.pk8 " +
333 "input.jar output.jar");
334 System.exit(2);
335 }
336
337 JarFile inputJar = null;
338 JarOutputStream outputJar = null;
339
340 try {
341 X509Certificate publicKey = readPublicKey(new File(args[0]));
342
343 // Assume the certificate is valid for at least an hour.
344 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
345
346 PrivateKey privateKey = readPrivateKey(new File(args[1]));
347 inputJar = new JarFile(new File(args[2]), false); // Don't verify.
348 outputJar = new JarOutputStream(new FileOutputStream(args[3]));
349 outputJar.setLevel(9);
350
351 JarEntry je;
352
353 // MANIFEST.MF
354 Manifest manifest = addDigestsToManifest(inputJar);
355 je = new JarEntry(JarFile.MANIFEST_NAME);
356 je.setTime(timestamp);
357 outputJar.putNextEntry(je);
358 manifest.write(outputJar);
359
360 // CERT.SF
361 Signature signature = Signature.getInstance("SHA1withRSA");
362 signature.initSign(privateKey);
363 je = new JarEntry(CERT_SF_NAME);
364 je.setTime(timestamp);
365 outputJar.putNextEntry(je);
366 writeSignatureFile(manifest,
367 new SignatureOutputStream(outputJar, signature));
368
369 // CERT.RSA
370 je = new JarEntry(CERT_RSA_NAME);
371 je.setTime(timestamp);
372 outputJar.putNextEntry(je);
373 writeSignatureBlock(signature, publicKey, outputJar);
374
375 // Everything else
376 copyFiles(manifest, inputJar, outputJar);
377 } catch (Exception e) {
378 e.printStackTrace();
379 System.exit(1);
380 } finally {
381 try {
382 if (inputJar != null) inputJar.close();
383 if (outputJar != null) outputJar.close();
384 } catch (IOException e) {
385 e.printStackTrace();
386 System.exit(1);
387 }
388 }
389 }
390}