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
17package com.android.signapk;
18
19import org.bouncycastle.asn1.ASN1InputStream;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.DEROutputStream;
22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
23import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
24import org.bouncycastle.cert.jcajce.JcaCertStore;
25import org.bouncycastle.cms.CMSException;
26import org.bouncycastle.cms.CMSProcessableByteArray;
27import org.bouncycastle.cms.CMSSignedData;
28import org.bouncycastle.cms.CMSSignedDataGenerator;
29import org.bouncycastle.cms.CMSTypedData;
30import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
31import org.bouncycastle.jce.provider.BouncyCastleProvider;
32import org.bouncycastle.operator.ContentSigner;
33import org.bouncycastle.operator.OperatorCreationException;
34import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
35import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
36import org.bouncycastle.util.encoders.Base64;
37
38import java.io.BufferedReader;
39import java.io.ByteArrayInputStream;
40import java.io.ByteArrayOutputStream;
41import java.io.DataInputStream;
42import java.io.File;
43import java.io.FileInputStream;
44import java.io.FileOutputStream;
45import java.io.FilterOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.InputStreamReader;
49import java.io.OutputStream;
50import java.io.PrintStream;
51import java.lang.reflect.Constructor;
52import java.security.DigestOutputStream;
53import java.security.GeneralSecurityException;
54import java.security.Key;
55import java.security.KeyFactory;
56import java.security.MessageDigest;
57import java.security.PrivateKey;
58import java.security.Provider;
59import java.security.Security;
60import java.security.cert.CertificateEncodingException;
61import java.security.cert.CertificateFactory;
62import java.security.cert.X509Certificate;
63import java.security.spec.InvalidKeySpecException;
64import java.security.spec.PKCS8EncodedKeySpec;
65import java.util.ArrayList;
66import java.util.Collections;
67import java.util.Enumeration;
68import java.util.Locale;
69import java.util.Map;
70import java.util.TreeMap;
71import java.util.jar.Attributes;
72import java.util.jar.JarEntry;
73import java.util.jar.JarFile;
74import java.util.jar.JarOutputStream;
75import java.util.jar.Manifest;
76import java.util.regex.Pattern;
77import javax.crypto.Cipher;
78import javax.crypto.EncryptedPrivateKeyInfo;
79import javax.crypto.SecretKeyFactory;
80import javax.crypto.spec.PBEKeySpec;
81
82/**
83 * HISTORICAL NOTE:
84 *
85 * Prior to the keylimepie release, SignApk ignored the signature
86 * algorithm specified in the certificate and always used SHA1withRSA.
87 *
88 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
89 * the signature algorithm in the certificate to select which to use
90 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
91 *
92 * Because there are old keys still in use whose certificate actually
93 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
94 * for compatibility with older releases.  This can be changed by
95 * altering the getAlgorithm() function below.
96 */
97
98
99/**
100 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
101 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
102 * SHA-256 (see historical note).
103 */
104class SignApk {
105    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
106    private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
107    private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
108    private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
109
110    private static final String OTACERT_NAME = "META-INF/com/android/otacert";
111
112    private static Provider sBouncyCastleProvider;
113
114    // bitmasks for which hash algorithms we need the manifest to include.
115    private static final int USE_SHA1 = 1;
116    private static final int USE_SHA256 = 2;
117
118    /**
119     * Return one of USE_SHA1 or USE_SHA256 according to the signature
120     * algorithm specified in the cert.
121     */
122    private static int getDigestAlgorithm(X509Certificate cert) {
123        String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
124        if ("SHA1WITHRSA".equals(sigAlg) ||
125            "MD5WITHRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
126            return USE_SHA1;
127        } else if (sigAlg.startsWith("SHA256WITH")) {
128            return USE_SHA256;
129        } else {
130            throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
131                                               "\" in cert [" + cert.getSubjectDN());
132        }
133    }
134
135    /** Returns the expected signature algorithm for this key type. */
136    private static String getSignatureAlgorithm(X509Certificate cert) {
137        String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
138        String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
139        if ("RSA".equalsIgnoreCase(keyType)) {
140            if (getDigestAlgorithm(cert) == USE_SHA256) {
141                return "SHA256withRSA";
142            } else {
143                return "SHA1withRSA";
144            }
145        } else if ("EC".equalsIgnoreCase(keyType)) {
146            return "SHA256withECDSA";
147        } else {
148            throw new IllegalArgumentException("unsupported key type: " + keyType);
149        }
150    }
151
152    // Files matching this pattern are not copied to the output.
153    private static Pattern stripPattern =
154        Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
155                        Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
156
157    private static X509Certificate readPublicKey(File file)
158        throws IOException, GeneralSecurityException {
159        FileInputStream input = new FileInputStream(file);
160        try {
161            CertificateFactory cf = CertificateFactory.getInstance("X.509");
162            return (X509Certificate) cf.generateCertificate(input);
163        } finally {
164            input.close();
165        }
166    }
167
168    /**
169     * Reads the password from stdin and returns it as a string.
170     *
171     * @param keyFile The file containing the private key.  Used to prompt the user.
172     */
173    private static String readPassword(File keyFile) {
174        // TODO: use Console.readPassword() when it's available.
175        System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
176        System.out.flush();
177        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
178        try {
179            return stdin.readLine();
180        } catch (IOException ex) {
181            return null;
182        }
183    }
184
185    /**
186     * Decrypt an encrypted PKCS#8 format private key.
187     *
188     * Based on ghstark's post on Aug 6, 2006 at
189     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
190     *
191     * @param encryptedPrivateKey The raw data of the private key
192     * @param keyFile The file containing the private key
193     */
194    private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
195        throws GeneralSecurityException {
196        EncryptedPrivateKeyInfo epkInfo;
197        try {
198            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
199        } catch (IOException ex) {
200            // Probably not an encrypted key.
201            return null;
202        }
203
204        char[] password = readPassword(keyFile).toCharArray();
205
206        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
207        Key key = skFactory.generateSecret(new PBEKeySpec(password));
208
209        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
210        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
211
212        try {
213            return epkInfo.getKeySpec(cipher);
214        } catch (InvalidKeySpecException ex) {
215            System.err.println("signapk: Password for " + keyFile + " may be bad.");
216            throw ex;
217        }
218    }
219
220    /** Read a PKCS#8 format private key. */
221    private static PrivateKey readPrivateKey(File file)
222        throws IOException, GeneralSecurityException {
223        DataInputStream input = new DataInputStream(new FileInputStream(file));
224        try {
225            byte[] bytes = new byte[(int) file.length()];
226            input.read(bytes);
227
228            /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
229            PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
230            if (spec == null) {
231                spec = new PKCS8EncodedKeySpec(bytes);
232            }
233
234            /*
235             * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
236             * OID and use that to construct a KeyFactory.
237             */
238            ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
239            PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
240            String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
241
242            return KeyFactory.getInstance(algOid).generatePrivate(spec);
243        } finally {
244            input.close();
245        }
246    }
247
248    /**
249     * Add the hash(es) of every file to the manifest, creating it if
250     * necessary.
251     */
252    private static Manifest addDigestsToManifest(JarFile jar, int hashes)
253        throws IOException, GeneralSecurityException {
254        Manifest input = jar.getManifest();
255        Manifest output = new Manifest();
256        Attributes main = output.getMainAttributes();
257        if (input != null) {
258            main.putAll(input.getMainAttributes());
259        } else {
260            main.putValue("Manifest-Version", "1.0");
261            main.putValue("Created-By", "1.0 (Android SignApk)");
262        }
263
264        MessageDigest md_sha1 = null;
265        MessageDigest md_sha256 = null;
266        if ((hashes & USE_SHA1) != 0) {
267            md_sha1 = MessageDigest.getInstance("SHA1");
268        }
269        if ((hashes & USE_SHA256) != 0) {
270            md_sha256 = MessageDigest.getInstance("SHA256");
271        }
272
273        byte[] buffer = new byte[4096];
274        int num;
275
276        // We sort the input entries by name, and add them to the
277        // output manifest in sorted order.  We expect that the output
278        // map will be deterministic.
279
280        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
281
282        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
283            JarEntry entry = e.nextElement();
284            byName.put(entry.getName(), entry);
285        }
286
287        for (JarEntry entry: byName.values()) {
288            String name = entry.getName();
289            if (!entry.isDirectory() &&
290                (stripPattern == null || !stripPattern.matcher(name).matches())) {
291                InputStream data = jar.getInputStream(entry);
292                while ((num = data.read(buffer)) > 0) {
293                    if (md_sha1 != null) md_sha1.update(buffer, 0, num);
294                    if (md_sha256 != null) md_sha256.update(buffer, 0, num);
295                }
296
297                Attributes attr = null;
298                if (input != null) attr = input.getAttributes(name);
299                attr = attr != null ? new Attributes(attr) : new Attributes();
300                if (md_sha1 != null) {
301                    attr.putValue("SHA1-Digest",
302                                  new String(Base64.encode(md_sha1.digest()), "ASCII"));
303                }
304                if (md_sha256 != null) {
305                    attr.putValue("SHA-256-Digest",
306                                  new String(Base64.encode(md_sha256.digest()), "ASCII"));
307                }
308                output.getEntries().put(name, attr);
309            }
310        }
311
312        return output;
313    }
314
315    /**
316     * Add a copy of the public key to the archive; this should
317     * exactly match one of the files in
318     * /system/etc/security/otacerts.zip on the device.  (The same
319     * cert can be extracted from the CERT.RSA file but this is much
320     * easier to get at.)
321     */
322    private static void addOtacert(JarOutputStream outputJar,
323                                   File publicKeyFile,
324                                   long timestamp,
325                                   Manifest manifest,
326                                   int hash)
327        throws IOException, GeneralSecurityException {
328        MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
329
330        JarEntry je = new JarEntry(OTACERT_NAME);
331        je.setTime(timestamp);
332        outputJar.putNextEntry(je);
333        FileInputStream input = new FileInputStream(publicKeyFile);
334        byte[] b = new byte[4096];
335        int read;
336        while ((read = input.read(b)) != -1) {
337            outputJar.write(b, 0, read);
338            md.update(b, 0, read);
339        }
340        input.close();
341
342        Attributes attr = new Attributes();
343        attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
344                      new String(Base64.encode(md.digest()), "ASCII"));
345        manifest.getEntries().put(OTACERT_NAME, attr);
346    }
347
348
349    /** Write to another stream and track how many bytes have been
350     *  written.
351     */
352    private static class CountOutputStream extends FilterOutputStream {
353        private int mCount;
354
355        public CountOutputStream(OutputStream out) {
356            super(out);
357            mCount = 0;
358        }
359
360        @Override
361        public void write(int b) throws IOException {
362            super.write(b);
363            mCount++;
364        }
365
366        @Override
367        public void write(byte[] b, int off, int len) throws IOException {
368            super.write(b, off, len);
369            mCount += len;
370        }
371
372        public int size() {
373            return mCount;
374        }
375    }
376
377    /** Write a .SF file with a digest of the specified manifest. */
378    private static void writeSignatureFile(Manifest manifest, OutputStream out,
379                                           int hash)
380        throws IOException, GeneralSecurityException {
381        Manifest sf = new Manifest();
382        Attributes main = sf.getMainAttributes();
383        main.putValue("Signature-Version", "1.0");
384        main.putValue("Created-By", "1.0 (Android SignApk)");
385
386        MessageDigest md = MessageDigest.getInstance(
387            hash == USE_SHA256 ? "SHA256" : "SHA1");
388        PrintStream print = new PrintStream(
389            new DigestOutputStream(new ByteArrayOutputStream(), md),
390            true, "UTF-8");
391
392        // Digest of the entire manifest
393        manifest.write(print);
394        print.flush();
395        main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
396                      new String(Base64.encode(md.digest()), "ASCII"));
397
398        Map<String, Attributes> entries = manifest.getEntries();
399        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
400            // Digest of the manifest stanza for this entry.
401            print.print("Name: " + entry.getKey() + "\r\n");
402            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
403                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
404            }
405            print.print("\r\n");
406            print.flush();
407
408            Attributes sfAttr = new Attributes();
409            sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
410                            new String(Base64.encode(md.digest()), "ASCII"));
411            sf.getEntries().put(entry.getKey(), sfAttr);
412        }
413
414        CountOutputStream cout = new CountOutputStream(out);
415        sf.write(cout);
416
417        // A bug in the java.util.jar implementation of Android platforms
418        // up to version 1.6 will cause a spurious IOException to be thrown
419        // if the length of the signature file is a multiple of 1024 bytes.
420        // As a workaround, add an extra CRLF in this case.
421        if ((cout.size() % 1024) == 0) {
422            cout.write('\r');
423            cout.write('\n');
424        }
425    }
426
427    /** Sign data and write the digital signature to 'out'. */
428    private static void writeSignatureBlock(
429        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
430        OutputStream out)
431        throws IOException,
432               CertificateEncodingException,
433               OperatorCreationException,
434               CMSException {
435        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
436        certList.add(publicKey);
437        JcaCertStore certs = new JcaCertStore(certList);
438
439        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
440        ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
441            .setProvider(sBouncyCastleProvider)
442            .build(privateKey);
443        gen.addSignerInfoGenerator(
444            new JcaSignerInfoGeneratorBuilder(
445                new JcaDigestCalculatorProviderBuilder()
446                .setProvider(sBouncyCastleProvider)
447                .build())
448            .setDirectSignature(true)
449            .build(signer, publicKey));
450        gen.addCertificates(certs);
451        CMSSignedData sigData = gen.generate(data, false);
452
453        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
454        DEROutputStream dos = new DEROutputStream(out);
455        dos.writeObject(asn1.readObject());
456    }
457
458    /**
459     * Copy all the files in a manifest from input to output.  We set
460     * the modification times in the output to a fixed time, so as to
461     * reduce variation in the output file and make incremental OTAs
462     * more efficient.
463     */
464    private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
465                                  long timestamp, int alignment) throws IOException {
466        byte[] buffer = new byte[4096];
467        int num;
468
469        Map<String, Attributes> entries = manifest.getEntries();
470        ArrayList<String> names = new ArrayList<String>(entries.keySet());
471        Collections.sort(names);
472
473        boolean firstEntry = true;
474        long offset = 0L;
475
476        // We do the copy in two passes -- first copying all the
477        // entries that are STORED, then copying all the entries that
478        // have any other compression flag (which in practice means
479        // DEFLATED).  This groups all the stored entries together at
480        // the start of the file and makes it easier to do alignment
481        // on them (since only stored entries are aligned).
482
483        for (String name : names) {
484            JarEntry inEntry = in.getJarEntry(name);
485            JarEntry outEntry = null;
486            if (inEntry.getMethod() != JarEntry.STORED) continue;
487            // Preserve the STORED method of the input entry.
488            outEntry = new JarEntry(inEntry);
489            outEntry.setTime(timestamp);
490
491            // 'offset' is the offset into the file at which we expect
492            // the file data to begin.  This is the value we need to
493            // make a multiple of 'alignement'.
494            offset += JarFile.LOCHDR + outEntry.getName().length();
495            if (firstEntry) {
496                // The first entry in a jar file has an extra field of
497                // four bytes that you can't get rid of; any extra
498                // data you specify in the JarEntry is appended to
499                // these forced four bytes.  This is JAR_MAGIC in
500                // JarOutputStream; the bytes are 0xfeca0000.
501                offset += 4;
502                firstEntry = false;
503            }
504            if (alignment > 0 && (offset % alignment != 0)) {
505                // Set the "extra data" of the entry to between 1 and
506                // alignment-1 bytes, to make the file data begin at
507                // an aligned offset.
508                int needed = alignment - (int)(offset % alignment);
509                outEntry.setExtra(new byte[needed]);
510                offset += needed;
511            }
512
513            out.putNextEntry(outEntry);
514
515            InputStream data = in.getInputStream(inEntry);
516            while ((num = data.read(buffer)) > 0) {
517                out.write(buffer, 0, num);
518                offset += num;
519            }
520            out.flush();
521        }
522
523        // Copy all the non-STORED entries.  We don't attempt to
524        // maintain the 'offset' variable past this point; we don't do
525        // alignment on these entries.
526
527        for (String name : names) {
528            JarEntry inEntry = in.getJarEntry(name);
529            JarEntry outEntry = null;
530            if (inEntry.getMethod() == JarEntry.STORED) continue;
531            // Create a new entry so that the compressed len is recomputed.
532            outEntry = new JarEntry(name);
533            outEntry.setTime(timestamp);
534            out.putNextEntry(outEntry);
535
536            InputStream data = in.getInputStream(inEntry);
537            while ((num = data.read(buffer)) > 0) {
538                out.write(buffer, 0, num);
539            }
540            out.flush();
541        }
542    }
543
544    private static class WholeFileSignerOutputStream extends FilterOutputStream {
545        private boolean closing = false;
546        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
547        private OutputStream tee;
548
549        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
550            super(out);
551            this.tee = tee;
552        }
553
554        public void notifyClosing() {
555            closing = true;
556        }
557
558        public void finish() throws IOException {
559            closing = false;
560
561            byte[] data = footer.toByteArray();
562            if (data.length < 2)
563                throw new IOException("Less than two bytes written to footer");
564            write(data, 0, data.length - 2);
565        }
566
567        public byte[] getTail() {
568            return footer.toByteArray();
569        }
570
571        @Override
572        public void write(byte[] b) throws IOException {
573            write(b, 0, b.length);
574        }
575
576        @Override
577        public void write(byte[] b, int off, int len) throws IOException {
578            if (closing) {
579                // if the jar is about to close, save the footer that will be written
580                footer.write(b, off, len);
581            }
582            else {
583                // write to both output streams. out is the CMSTypedData signer and tee is the file.
584                out.write(b, off, len);
585                tee.write(b, off, len);
586            }
587        }
588
589        @Override
590        public void write(int b) throws IOException {
591            if (closing) {
592                // if the jar is about to close, save the footer that will be written
593                footer.write(b);
594            }
595            else {
596                // write to both output streams. out is the CMSTypedData signer and tee is the file.
597                out.write(b);
598                tee.write(b);
599            }
600        }
601    }
602
603    private static class CMSSigner implements CMSTypedData {
604        private JarFile inputJar;
605        private File publicKeyFile;
606        private X509Certificate publicKey;
607        private PrivateKey privateKey;
608        private String outputFile;
609        private OutputStream outputStream;
610        private final ASN1ObjectIdentifier type;
611        private WholeFileSignerOutputStream signer;
612
613        public CMSSigner(JarFile inputJar, File publicKeyFile,
614                         X509Certificate publicKey, PrivateKey privateKey,
615                         OutputStream outputStream) {
616            this.inputJar = inputJar;
617            this.publicKeyFile = publicKeyFile;
618            this.publicKey = publicKey;
619            this.privateKey = privateKey;
620            this.outputStream = outputStream;
621            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
622        }
623
624        public Object getContent() {
625            throw new UnsupportedOperationException();
626        }
627
628        public ASN1ObjectIdentifier getContentType() {
629            return type;
630        }
631
632        public void write(OutputStream out) throws IOException {
633            try {
634                signer = new WholeFileSignerOutputStream(out, outputStream);
635                JarOutputStream outputJar = new JarOutputStream(signer);
636
637                int hash = getDigestAlgorithm(publicKey);
638
639                // Assume the certificate is valid for at least an hour.
640                long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
641
642                Manifest manifest = addDigestsToManifest(inputJar, hash);
643                copyFiles(manifest, inputJar, outputJar, timestamp, 0);
644                addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
645
646                signFile(manifest, inputJar,
647                         new X509Certificate[]{ publicKey },
648                         new PrivateKey[]{ privateKey },
649                         outputJar);
650
651                signer.notifyClosing();
652                outputJar.close();
653                signer.finish();
654            }
655            catch (Exception e) {
656                throw new IOException(e);
657            }
658        }
659
660        public void writeSignatureBlock(ByteArrayOutputStream temp)
661            throws IOException,
662                   CertificateEncodingException,
663                   OperatorCreationException,
664                   CMSException {
665            SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
666        }
667
668        public WholeFileSignerOutputStream getSigner() {
669            return signer;
670        }
671    }
672
673    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
674                                      X509Certificate publicKey, PrivateKey privateKey,
675                                      OutputStream outputStream) throws Exception {
676        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
677                                         publicKey, privateKey, outputStream);
678
679        ByteArrayOutputStream temp = new ByteArrayOutputStream();
680
681        // put a readable message and a null char at the start of the
682        // archive comment, so that tools that display the comment
683        // (hopefully) show something sensible.
684        // TODO: anything more useful we can put in this message?
685        byte[] message = "signed by SignApk".getBytes("UTF-8");
686        temp.write(message);
687        temp.write(0);
688
689        cmsOut.writeSignatureBlock(temp);
690
691        byte[] zipData = cmsOut.getSigner().getTail();
692
693        // For a zip with no archive comment, the
694        // end-of-central-directory record will be 22 bytes long, so
695        // we expect to find the EOCD marker 22 bytes from the end.
696        if (zipData[zipData.length-22] != 0x50 ||
697            zipData[zipData.length-21] != 0x4b ||
698            zipData[zipData.length-20] != 0x05 ||
699            zipData[zipData.length-19] != 0x06) {
700            throw new IllegalArgumentException("zip data already has an archive comment");
701        }
702
703        int total_size = temp.size() + 6;
704        if (total_size > 0xffff) {
705            throw new IllegalArgumentException("signature is too big for ZIP file comment");
706        }
707        // signature starts this many bytes from the end of the file
708        int signature_start = total_size - message.length - 1;
709        temp.write(signature_start & 0xff);
710        temp.write((signature_start >> 8) & 0xff);
711        // Why the 0xff bytes?  In a zip file with no archive comment,
712        // bytes [-6:-2] of the file are the little-endian offset from
713        // the start of the file to the central directory.  So for the
714        // two high bytes to be 0xff 0xff, the archive would have to
715        // be nearly 4GB in size.  So it's unlikely that a real
716        // commentless archive would have 0xffs here, and lets us tell
717        // an old signed archive from a new one.
718        temp.write(0xff);
719        temp.write(0xff);
720        temp.write(total_size & 0xff);
721        temp.write((total_size >> 8) & 0xff);
722        temp.flush();
723
724        // Signature verification checks that the EOCD header is the
725        // last such sequence in the file (to avoid minzip finding a
726        // fake EOCD appended after the signature in its scan).  The
727        // odds of producing this sequence by chance are very low, but
728        // let's catch it here if it does.
729        byte[] b = temp.toByteArray();
730        for (int i = 0; i < b.length-3; ++i) {
731            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
732                throw new IllegalArgumentException("found spurious EOCD header at " + i);
733            }
734        }
735
736        outputStream.write(total_size & 0xff);
737        outputStream.write((total_size >> 8) & 0xff);
738        temp.writeTo(outputStream);
739    }
740
741    private static void signFile(Manifest manifest, JarFile inputJar,
742                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
743                                 JarOutputStream outputJar)
744        throws Exception {
745        // Assume the certificate is valid for at least an hour.
746        long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
747
748        // MANIFEST.MF
749        JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
750        je.setTime(timestamp);
751        outputJar.putNextEntry(je);
752        manifest.write(outputJar);
753
754        int numKeys = publicKey.length;
755        for (int k = 0; k < numKeys; ++k) {
756            // CERT.SF / CERT#.SF
757            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
758                              (String.format(CERT_SF_MULTI_NAME, k)));
759            je.setTime(timestamp);
760            outputJar.putNextEntry(je);
761            ByteArrayOutputStream baos = new ByteArrayOutputStream();
762            writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
763            byte[] signedData = baos.toByteArray();
764            outputJar.write(signedData);
765
766            // CERT.{EC,RSA} / CERT#.{EC,RSA}
767            final String keyType = publicKey[k].getPublicKey().getAlgorithm();
768            je = new JarEntry(numKeys == 1 ?
769                              (String.format(CERT_SIG_NAME, keyType)) :
770                              (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
771            je.setTime(timestamp);
772            outputJar.putNextEntry(je);
773            writeSignatureBlock(new CMSProcessableByteArray(signedData),
774                                publicKey[k], privateKey[k], outputJar);
775        }
776    }
777
778    /**
779     * Tries to load a JSE Provider by class name. This is for custom PrivateKey
780     * types that might be stored in PKCS#11-like storage.
781     */
782    private static void loadProviderIfNecessary(String providerClassName) {
783        if (providerClassName == null) {
784            return;
785        }
786
787        final Class<?> klass;
788        try {
789            final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
790            if (sysLoader != null) {
791                klass = sysLoader.loadClass(providerClassName);
792            } else {
793                klass = Class.forName(providerClassName);
794            }
795        } catch (ClassNotFoundException e) {
796            e.printStackTrace();
797            System.exit(1);
798            return;
799        }
800
801        Constructor<?> constructor = null;
802        for (Constructor<?> c : klass.getConstructors()) {
803            if (c.getParameterTypes().length == 0) {
804                constructor = c;
805                break;
806            }
807        }
808        if (constructor == null) {
809            System.err.println("No zero-arg constructor found for " + providerClassName);
810            System.exit(1);
811            return;
812        }
813
814        final Object o;
815        try {
816            o = constructor.newInstance();
817        } catch (Exception e) {
818            e.printStackTrace();
819            System.exit(1);
820            return;
821        }
822        if (!(o instanceof Provider)) {
823            System.err.println("Not a Provider class: " + providerClassName);
824            System.exit(1);
825        }
826
827        Security.insertProviderAt((Provider) o, 1);
828    }
829
830    private static void usage() {
831        System.err.println("Usage: signapk [-w] " +
832                           "[-a <alignment>] " +
833                           "[-providerClass <className>] " +
834                           "publickey.x509[.pem] privatekey.pk8 " +
835                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
836                           "input.jar output.jar");
837        System.exit(2);
838    }
839
840    public static void main(String[] args) {
841        if (args.length < 4) usage();
842
843        sBouncyCastleProvider = new BouncyCastleProvider();
844        Security.addProvider(sBouncyCastleProvider);
845
846        boolean signWholeFile = false;
847        String providerClass = null;
848        String providerArg = null;
849        int alignment = 4;
850
851        int argstart = 0;
852        while (argstart < args.length && args[argstart].startsWith("-")) {
853            if ("-w".equals(args[argstart])) {
854                signWholeFile = true;
855                ++argstart;
856            } else if ("-providerClass".equals(args[argstart])) {
857                if (argstart + 1 >= args.length) {
858                    usage();
859                }
860                providerClass = args[++argstart];
861                ++argstart;
862            } else if ("-a".equals(args[argstart])) {
863                alignment = Integer.parseInt(args[++argstart]);
864                ++argstart;
865            } else {
866                usage();
867            }
868        }
869
870        if ((args.length - argstart) % 2 == 1) usage();
871        int numKeys = ((args.length - argstart) / 2) - 1;
872        if (signWholeFile && numKeys > 1) {
873            System.err.println("Only one key may be used with -w.");
874            System.exit(2);
875        }
876
877        loadProviderIfNecessary(providerClass);
878
879        String inputFilename = args[args.length-2];
880        String outputFilename = args[args.length-1];
881
882        JarFile inputJar = null;
883        FileOutputStream outputFile = null;
884        int hashes = 0;
885
886        try {
887            File firstPublicKeyFile = new File(args[argstart+0]);
888
889            X509Certificate[] publicKey = new X509Certificate[numKeys];
890            try {
891                for (int i = 0; i < numKeys; ++i) {
892                    int argNum = argstart + i*2;
893                    publicKey[i] = readPublicKey(new File(args[argNum]));
894                    hashes |= getDigestAlgorithm(publicKey[i]);
895                }
896            } catch (IllegalArgumentException e) {
897                System.err.println(e);
898                System.exit(1);
899            }
900
901            // Set the ZIP file timestamp to the starting valid time
902            // of the 0th certificate plus one hour (to match what
903            // we've historically done).
904            long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
905
906            PrivateKey[] privateKey = new PrivateKey[numKeys];
907            for (int i = 0; i < numKeys; ++i) {
908                int argNum = argstart + i*2 + 1;
909                privateKey[i] = readPrivateKey(new File(args[argNum]));
910            }
911            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
912
913            outputFile = new FileOutputStream(outputFilename);
914
915
916            if (signWholeFile) {
917                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
918                                      publicKey[0], privateKey[0], outputFile);
919            } else {
920                JarOutputStream outputJar = new JarOutputStream(outputFile);
921
922                // For signing .apks, use the maximum compression to make
923                // them as small as possible (since they live forever on
924                // the system partition).  For OTA packages, use the
925                // default compression level, which is much much faster
926                // and produces output that is only a tiny bit larger
927                // (~0.1% on full OTA packages I tested).
928                outputJar.setLevel(9);
929
930                Manifest manifest = addDigestsToManifest(inputJar, hashes);
931                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
932                signFile(manifest, inputJar, publicKey, privateKey, outputJar);
933                outputJar.close();
934            }
935        } catch (Exception e) {
936            e.printStackTrace();
937            System.exit(1);
938        } finally {
939            try {
940                if (inputJar != null) inputJar.close();
941                if (outputFile != null) outputFile.close();
942            } catch (IOException e) {
943                e.printStackTrace();
944                System.exit(1);
945            }
946        }
947    }
948}
949