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