• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.signapk;
18 
19 import org.bouncycastle.asn1.ASN1InputStream;
20 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21 import org.bouncycastle.asn1.DEROutputStream;
22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
23 import org.bouncycastle.cert.jcajce.JcaCertStore;
24 import org.bouncycastle.cms.CMSException;
25 import org.bouncycastle.cms.CMSProcessableByteArray;
26 import org.bouncycastle.cms.CMSSignedData;
27 import org.bouncycastle.cms.CMSSignedDataGenerator;
28 import org.bouncycastle.cms.CMSTypedData;
29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
30 import org.bouncycastle.jce.provider.BouncyCastleProvider;
31 import org.bouncycastle.operator.ContentSigner;
32 import org.bouncycastle.operator.OperatorCreationException;
33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
35 import org.bouncycastle.util.encoders.Base64;
36 
37 import java.io.BufferedReader;
38 import java.io.ByteArrayOutputStream;
39 import java.io.DataInputStream;
40 import java.io.File;
41 import java.io.FileInputStream;
42 import java.io.FileOutputStream;
43 import java.io.FilterOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.InputStreamReader;
47 import java.io.OutputStream;
48 import java.io.PrintStream;
49 import java.security.DigestOutputStream;
50 import java.security.GeneralSecurityException;
51 import java.security.Key;
52 import java.security.KeyFactory;
53 import java.security.MessageDigest;
54 import java.security.PrivateKey;
55 import java.security.Provider;
56 import java.security.Security;
57 import java.security.cert.CertificateEncodingException;
58 import java.security.cert.CertificateFactory;
59 import java.security.cert.X509Certificate;
60 import java.security.spec.InvalidKeySpecException;
61 import java.security.spec.KeySpec;
62 import java.security.spec.PKCS8EncodedKeySpec;
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.Enumeration;
66 import java.util.Map;
67 import java.util.TreeMap;
68 import java.util.jar.Attributes;
69 import java.util.jar.JarEntry;
70 import java.util.jar.JarFile;
71 import java.util.jar.JarOutputStream;
72 import java.util.jar.Manifest;
73 import java.util.regex.Pattern;
74 import javax.crypto.Cipher;
75 import javax.crypto.EncryptedPrivateKeyInfo;
76 import javax.crypto.SecretKeyFactory;
77 import javax.crypto.spec.PBEKeySpec;
78 
79 /**
80  * Command line tool to sign JAR files (including APKs and OTA updates) in
81  * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
82  */
83 class SignApk {
84     private static final String CERT_SF_NAME = "META-INF/CERT.SF";
85     private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
86 
87     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
88 
89     private static Provider sBouncyCastleProvider;
90 
91     // Files matching this pattern are not copied to the output.
92     private static Pattern stripPattern =
93             Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
94 
readPublicKey(File file)95     private static X509Certificate readPublicKey(File file)
96             throws IOException, GeneralSecurityException {
97         FileInputStream input = new FileInputStream(file);
98         try {
99             CertificateFactory cf = CertificateFactory.getInstance("X.509");
100             return (X509Certificate) cf.generateCertificate(input);
101         } finally {
102             input.close();
103         }
104     }
105 
106     /**
107      * Reads the password from stdin and returns it as a string.
108      *
109      * @param keyFile The file containing the private key.  Used to prompt the user.
110      */
readPassword(File keyFile)111     private static String readPassword(File keyFile) {
112         // TODO: use Console.readPassword() when it's available.
113         System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
114         System.out.flush();
115         BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
116         try {
117             return stdin.readLine();
118         } catch (IOException ex) {
119             return null;
120         }
121     }
122 
123     /**
124      * Decrypt an encrypted PKCS 8 format private key.
125      *
126      * Based on ghstark's post on Aug 6, 2006 at
127      * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
128      *
129      * @param encryptedPrivateKey The raw data of the private key
130      * @param keyFile The file containing the private key
131      */
decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)132     private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
133             throws GeneralSecurityException {
134         EncryptedPrivateKeyInfo epkInfo;
135         try {
136             epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
137         } catch (IOException ex) {
138             // Probably not an encrypted key.
139             return null;
140         }
141 
142         char[] password = readPassword(keyFile).toCharArray();
143 
144         SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
145         Key key = skFactory.generateSecret(new PBEKeySpec(password));
146 
147         Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
148         cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
149 
150         try {
151             return epkInfo.getKeySpec(cipher);
152         } catch (InvalidKeySpecException ex) {
153             System.err.println("signapk: Password for " + keyFile + " may be bad.");
154             throw ex;
155         }
156     }
157 
158     /** Read a PKCS 8 format private key. */
readPrivateKey(File file)159     private static PrivateKey readPrivateKey(File file)
160             throws IOException, GeneralSecurityException {
161         DataInputStream input = new DataInputStream(new FileInputStream(file));
162         try {
163             byte[] bytes = new byte[(int) file.length()];
164             input.read(bytes);
165 
166             KeySpec spec = decryptPrivateKey(bytes, file);
167             if (spec == null) {
168                 spec = new PKCS8EncodedKeySpec(bytes);
169             }
170 
171             try {
172                 return KeyFactory.getInstance("RSA").generatePrivate(spec);
173             } catch (InvalidKeySpecException ex) {
174                 return KeyFactory.getInstance("DSA").generatePrivate(spec);
175             }
176         } finally {
177             input.close();
178         }
179     }
180 
181     /** Add the SHA1 of every file to the manifest, creating it if necessary. */
addDigestsToManifest(JarFile jar)182     private static Manifest addDigestsToManifest(JarFile jar)
183             throws IOException, GeneralSecurityException {
184         Manifest input = jar.getManifest();
185         Manifest output = new Manifest();
186         Attributes main = output.getMainAttributes();
187         if (input != null) {
188             main.putAll(input.getMainAttributes());
189         } else {
190             main.putValue("Manifest-Version", "1.0");
191             main.putValue("Created-By", "1.0 (Android SignApk)");
192         }
193 
194         MessageDigest md = MessageDigest.getInstance("SHA1");
195         byte[] buffer = new byte[4096];
196         int num;
197 
198         // We sort the input entries by name, and add them to the
199         // output manifest in sorted order.  We expect that the output
200         // map will be deterministic.
201 
202         TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
203 
204         for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
205             JarEntry entry = e.nextElement();
206             byName.put(entry.getName(), entry);
207         }
208 
209         for (JarEntry entry: byName.values()) {
210             String name = entry.getName();
211             if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
212                 !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
213                 !name.equals(OTACERT_NAME) &&
214                 (stripPattern == null ||
215                  !stripPattern.matcher(name).matches())) {
216                 InputStream data = jar.getInputStream(entry);
217                 while ((num = data.read(buffer)) > 0) {
218                     md.update(buffer, 0, num);
219                 }
220 
221                 Attributes attr = null;
222                 if (input != null) attr = input.getAttributes(name);
223                 attr = attr != null ? new Attributes(attr) : new Attributes();
224                 attr.putValue("SHA1-Digest",
225                               new String(Base64.encode(md.digest()), "ASCII"));
226                 output.getEntries().put(name, attr);
227             }
228         }
229 
230         return output;
231     }
232 
233     /**
234      * Add a copy of the public key to the archive; this should
235      * exactly match one of the files in
236      * /system/etc/security/otacerts.zip on the device.  (The same
237      * cert can be extracted from the CERT.RSA file but this is much
238      * easier to get at.)
239      */
addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, Manifest manifest)240     private static void addOtacert(JarOutputStream outputJar,
241                                    File publicKeyFile,
242                                    long timestamp,
243                                    Manifest manifest)
244         throws IOException, GeneralSecurityException {
245         MessageDigest md = MessageDigest.getInstance("SHA1");
246 
247         JarEntry je = new JarEntry(OTACERT_NAME);
248         je.setTime(timestamp);
249         outputJar.putNextEntry(je);
250         FileInputStream input = new FileInputStream(publicKeyFile);
251         byte[] b = new byte[4096];
252         int read;
253         while ((read = input.read(b)) != -1) {
254             outputJar.write(b, 0, read);
255             md.update(b, 0, read);
256         }
257         input.close();
258 
259         Attributes attr = new Attributes();
260         attr.putValue("SHA1-Digest",
261                       new String(Base64.encode(md.digest()), "ASCII"));
262         manifest.getEntries().put(OTACERT_NAME, attr);
263     }
264 
265 
266     /** Write to another stream and track how many bytes have been
267      *  written.
268      */
269     private static class CountOutputStream extends FilterOutputStream {
270         private int mCount;
271 
CountOutputStream(OutputStream out)272         public CountOutputStream(OutputStream out) {
273             super(out);
274             mCount = 0;
275         }
276 
277         @Override
write(int b)278         public void write(int b) throws IOException {
279             super.write(b);
280             mCount++;
281         }
282 
283         @Override
write(byte[] b, int off, int len)284         public void write(byte[] b, int off, int len) throws IOException {
285             super.write(b, off, len);
286             mCount += len;
287         }
288 
size()289         public int size() {
290             return mCount;
291         }
292     }
293 
294     /** Write a .SF file with a digest of the specified manifest. */
writeSignatureFile(Manifest manifest, OutputStream out)295     private static void writeSignatureFile(Manifest manifest, OutputStream out)
296         throws IOException, GeneralSecurityException {
297         Manifest sf = new Manifest();
298         Attributes main = sf.getMainAttributes();
299         main.putValue("Signature-Version", "1.0");
300         main.putValue("Created-By", "1.0 (Android SignApk)");
301 
302         MessageDigest md = MessageDigest.getInstance("SHA1");
303         PrintStream print = new PrintStream(
304                 new DigestOutputStream(new ByteArrayOutputStream(), md),
305                 true, "UTF-8");
306 
307         // Digest of the entire manifest
308         manifest.write(print);
309         print.flush();
310         main.putValue("SHA1-Digest-Manifest",
311                       new String(Base64.encode(md.digest()), "ASCII"));
312 
313         Map<String, Attributes> entries = manifest.getEntries();
314         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
315             // Digest of the manifest stanza for this entry.
316             print.print("Name: " + entry.getKey() + "\r\n");
317             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
318                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
319             }
320             print.print("\r\n");
321             print.flush();
322 
323             Attributes sfAttr = new Attributes();
324             sfAttr.putValue("SHA1-Digest",
325                             new String(Base64.encode(md.digest()), "ASCII"));
326             sf.getEntries().put(entry.getKey(), sfAttr);
327         }
328 
329         CountOutputStream cout = new CountOutputStream(out);
330         sf.write(cout);
331 
332         // A bug in the java.util.jar implementation of Android platforms
333         // up to version 1.6 will cause a spurious IOException to be thrown
334         // if the length of the signature file is a multiple of 1024 bytes.
335         // As a workaround, add an extra CRLF in this case.
336         if ((cout.size() % 1024) == 0) {
337             cout.write('\r');
338             cout.write('\n');
339         }
340     }
341 
342     private static class CMSByteArraySlice implements CMSTypedData {
343         private final ASN1ObjectIdentifier type;
344         private final byte[] data;
345         private final int offset;
346         private final int length;
CMSByteArraySlice(byte[] data, int offset, int length)347         public CMSByteArraySlice(byte[] data, int offset, int length) {
348             this.data = data;
349             this.offset = offset;
350             this.length = length;
351             this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
352         }
353 
getContent()354         public Object getContent() {
355             throw new UnsupportedOperationException();
356         }
357 
getContentType()358         public ASN1ObjectIdentifier getContentType() {
359             return type;
360         }
361 
write(OutputStream out)362         public void write(OutputStream out) throws IOException {
363             out.write(data, offset, length);
364         }
365     }
366 
367     /** Sign data and write the digital signature to 'out'. */
writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)368     private static void writeSignatureBlock(
369         CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
370         OutputStream out)
371         throws IOException,
372                CertificateEncodingException,
373                OperatorCreationException,
374                CMSException {
375         ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
376         certList.add(publicKey);
377         JcaCertStore certs = new JcaCertStore(certList);
378 
379         CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
380         ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
381             .setProvider(sBouncyCastleProvider)
382             .build(privateKey);
383         gen.addSignerInfoGenerator(
384             new JcaSignerInfoGeneratorBuilder(
385                 new JcaDigestCalculatorProviderBuilder()
386                 .setProvider(sBouncyCastleProvider)
387                 .build())
388             .setDirectSignature(true)
389             .build(sha1Signer, publicKey));
390         gen.addCertificates(certs);
391         CMSSignedData sigData = gen.generate(data, false);
392 
393         ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
394         DEROutputStream dos = new DEROutputStream(out);
395         dos.writeObject(asn1.readObject());
396     }
397 
signWholeOutputFile(byte[] zipData, OutputStream outputStream, X509Certificate publicKey, PrivateKey privateKey)398     private static void signWholeOutputFile(byte[] zipData,
399                                             OutputStream outputStream,
400                                             X509Certificate publicKey,
401                                             PrivateKey privateKey)
402         throws IOException,
403                CertificateEncodingException,
404                OperatorCreationException,
405                CMSException {
406         // For a zip with no archive comment, the
407         // end-of-central-directory record will be 22 bytes long, so
408         // we expect to find the EOCD marker 22 bytes from the end.
409         if (zipData[zipData.length-22] != 0x50 ||
410             zipData[zipData.length-21] != 0x4b ||
411             zipData[zipData.length-20] != 0x05 ||
412             zipData[zipData.length-19] != 0x06) {
413             throw new IllegalArgumentException("zip data already has an archive comment");
414         }
415 
416         ByteArrayOutputStream temp = new ByteArrayOutputStream();
417 
418         // put a readable message and a null char at the start of the
419         // archive comment, so that tools that display the comment
420         // (hopefully) show something sensible.
421         // TODO: anything more useful we can put in this message?
422         byte[] message = "signed by SignApk".getBytes("UTF-8");
423         temp.write(message);
424         temp.write(0);
425 
426         writeSignatureBlock(new CMSByteArraySlice(zipData, 0, zipData.length-2),
427                             publicKey, privateKey, temp);
428         int total_size = temp.size() + 6;
429         if (total_size > 0xffff) {
430             throw new IllegalArgumentException("signature is too big for ZIP file comment");
431         }
432         // signature starts this many bytes from the end of the file
433         int signature_start = total_size - message.length - 1;
434         temp.write(signature_start & 0xff);
435         temp.write((signature_start >> 8) & 0xff);
436         // Why the 0xff bytes?  In a zip file with no archive comment,
437         // bytes [-6:-2] of the file are the little-endian offset from
438         // the start of the file to the central directory.  So for the
439         // two high bytes to be 0xff 0xff, the archive would have to
440         // be nearly 4GB in size.  So it's unlikely that a real
441         // commentless archive would have 0xffs here, and lets us tell
442         // an old signed archive from a new one.
443         temp.write(0xff);
444         temp.write(0xff);
445         temp.write(total_size & 0xff);
446         temp.write((total_size >> 8) & 0xff);
447         temp.flush();
448 
449         // Signature verification checks that the EOCD header is the
450         // last such sequence in the file (to avoid minzip finding a
451         // fake EOCD appended after the signature in its scan).  The
452         // odds of producing this sequence by chance are very low, but
453         // let's catch it here if it does.
454         byte[] b = temp.toByteArray();
455         for (int i = 0; i < b.length-3; ++i) {
456             if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
457                 throw new IllegalArgumentException("found spurious EOCD header at " + i);
458             }
459         }
460 
461         outputStream.write(zipData, 0, zipData.length-2);
462         outputStream.write(total_size & 0xff);
463         outputStream.write((total_size >> 8) & 0xff);
464         temp.writeTo(outputStream);
465     }
466 
467     /**
468      * Copy all the files in a manifest from input to output.  We set
469      * the modification times in the output to a fixed time, so as to
470      * reduce variation in the output file and make incremental OTAs
471      * more efficient.
472      */
copyFiles(Manifest manifest, JarFile in, JarOutputStream out, long timestamp)473     private static void copyFiles(Manifest manifest,
474         JarFile in, JarOutputStream out, long timestamp) throws IOException {
475         byte[] buffer = new byte[4096];
476         int num;
477 
478         Map<String, Attributes> entries = manifest.getEntries();
479         ArrayList<String> names = new ArrayList<String>(entries.keySet());
480         Collections.sort(names);
481         for (String name : names) {
482             JarEntry inEntry = in.getJarEntry(name);
483             JarEntry outEntry = null;
484             if (inEntry.getMethod() == JarEntry.STORED) {
485                 // Preserve the STORED method of the input entry.
486                 outEntry = new JarEntry(inEntry);
487             } else {
488                 // Create a new entry so that the compressed len is recomputed.
489                 outEntry = new JarEntry(name);
490             }
491             outEntry.setTime(timestamp);
492             out.putNextEntry(outEntry);
493 
494             InputStream data = in.getInputStream(inEntry);
495             while ((num = data.read(buffer)) > 0) {
496                 out.write(buffer, 0, num);
497             }
498             out.flush();
499         }
500     }
501 
main(String[] args)502     public static void main(String[] args) {
503         if (args.length != 4 && args.length != 5) {
504             System.err.println("Usage: signapk [-w] " +
505                     "publickey.x509[.pem] privatekey.pk8 " +
506                     "input.jar output.jar");
507             System.exit(2);
508         }
509 
510         sBouncyCastleProvider = new BouncyCastleProvider();
511         Security.addProvider(sBouncyCastleProvider);
512 
513         boolean signWholeFile = false;
514         int argstart = 0;
515         if (args[0].equals("-w")) {
516             signWholeFile = true;
517             argstart = 1;
518         }
519 
520         JarFile inputJar = null;
521         JarOutputStream outputJar = null;
522         FileOutputStream outputFile = null;
523 
524         try {
525             File publicKeyFile = new File(args[argstart+0]);
526             X509Certificate publicKey = readPublicKey(publicKeyFile);
527 
528             // Assume the certificate is valid for at least an hour.
529             long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
530 
531             PrivateKey privateKey = readPrivateKey(new File(args[argstart+1]));
532             inputJar = new JarFile(new File(args[argstart+2]), false);  // Don't verify.
533 
534             OutputStream outputStream = null;
535             if (signWholeFile) {
536                 outputStream = new ByteArrayOutputStream();
537             } else {
538                 outputStream = outputFile = new FileOutputStream(args[argstart+3]);
539             }
540             outputJar = new JarOutputStream(outputStream);
541 
542             // For signing .apks, use the maximum compression to make
543             // them as small as possible (since they live forever on
544             // the system partition).  For OTA packages, use the
545             // default compression level, which is much much faster
546             // and produces output that is only a tiny bit larger
547             // (~0.1% on full OTA packages I tested).
548             if (!signWholeFile) {
549                 outputJar.setLevel(9);
550             }
551 
552             JarEntry je;
553 
554             Manifest manifest = addDigestsToManifest(inputJar);
555 
556             // Everything else
557             copyFiles(manifest, inputJar, outputJar, timestamp);
558 
559             // otacert
560             if (signWholeFile) {
561                 addOtacert(outputJar, publicKeyFile, timestamp, manifest);
562             }
563 
564             // MANIFEST.MF
565             je = new JarEntry(JarFile.MANIFEST_NAME);
566             je.setTime(timestamp);
567             outputJar.putNextEntry(je);
568             manifest.write(outputJar);
569 
570             // CERT.SF
571             je = new JarEntry(CERT_SF_NAME);
572             je.setTime(timestamp);
573             outputJar.putNextEntry(je);
574             ByteArrayOutputStream baos = new ByteArrayOutputStream();
575             writeSignatureFile(manifest, baos);
576             byte[] signedData = baos.toByteArray();
577             outputJar.write(signedData);
578 
579             // CERT.RSA
580             je = new JarEntry(CERT_RSA_NAME);
581             je.setTime(timestamp);
582             outputJar.putNextEntry(je);
583             writeSignatureBlock(new CMSProcessableByteArray(signedData),
584                                 publicKey, privateKey, outputJar);
585 
586             outputJar.close();
587             outputJar = null;
588             outputStream.flush();
589 
590             if (signWholeFile) {
591                 outputFile = new FileOutputStream(args[argstart+3]);
592                 signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),
593                                     outputFile, publicKey, privateKey);
594             }
595         } catch (Exception e) {
596             e.printStackTrace();
597             System.exit(1);
598         } finally {
599             try {
600                 if (inputJar != null) inputJar.close();
601                 if (outputFile != null) outputFile.close();
602             } catch (IOException e) {
603                 e.printStackTrace();
604                 System.exit(1);
605             }
606         }
607     }
608 }
609