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