SignApk.java revision 60817689faf7029d6073e0ebcf0c3bb9cce0cf73
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            // Discard comment and extra fields of this entry to
548            // simplify alignment logic below and for consistency with
549            // how compressed entries are handled later.
550            outEntry.setComment(null);
551            outEntry.setExtra(null);
552
553            // 'offset' is the offset into the file at which we expect
554            // the file data to begin.  This is the value we need to
555            // make a multiple of 'alignement'.
556            offset += JarFile.LOCHDR + outEntry.getName().length();
557            if (firstEntry) {
558                // The first entry in a jar file has an extra field of
559                // four bytes that you can't get rid of; any extra
560                // data you specify in the JarEntry is appended to
561                // these forced four bytes.  This is JAR_MAGIC in
562                // JarOutputStream; the bytes are 0xfeca0000.
563                offset += 4;
564                firstEntry = false;
565            }
566            int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
567            if (alignment > 0 && (offset % alignment != 0)) {
568                // Set the "extra data" of the entry to between 1 and
569                // alignment-1 bytes, to make the file data begin at
570                // an aligned offset.
571                int needed = alignment - (int)(offset % alignment);
572                outEntry.setExtra(new byte[needed]);
573                offset += needed;
574            }
575
576            out.putNextEntry(outEntry);
577
578            InputStream data = in.getInputStream(inEntry);
579            while ((num = data.read(buffer)) > 0) {
580                out.write(buffer, 0, num);
581                offset += num;
582            }
583            out.flush();
584        }
585
586        // Copy all the non-STORED entries.  We don't attempt to
587        // maintain the 'offset' variable past this point; we don't do
588        // alignment on these entries.
589
590        for (String name : names) {
591            JarEntry inEntry = in.getJarEntry(name);
592            JarEntry outEntry = null;
593            if (inEntry.getMethod() == JarEntry.STORED) continue;
594            // Create a new entry so that the compressed len is recomputed.
595            outEntry = new JarEntry(name);
596            outEntry.setTime(timestamp);
597            out.putNextEntry(outEntry);
598
599            InputStream data = in.getInputStream(inEntry);
600            while ((num = data.read(buffer)) > 0) {
601                out.write(buffer, 0, num);
602            }
603            out.flush();
604        }
605    }
606
607    /**
608     * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
609     * relative to start of file or {@code 0} if alignment of this entry's data is not important.
610     */
611    private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) {
612        if (defaultAlignment <= 0) {
613            return 0;
614        }
615
616        if (entryName.endsWith(".so")) {
617            // Align .so contents to memory page boundary to enable memory-mapped
618            // execution.
619            return 4096;
620        } else {
621            return defaultAlignment;
622        }
623    }
624
625    private static class WholeFileSignerOutputStream extends FilterOutputStream {
626        private boolean closing = false;
627        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
628        private OutputStream tee;
629
630        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
631            super(out);
632            this.tee = tee;
633        }
634
635        public void notifyClosing() {
636            closing = true;
637        }
638
639        public void finish() throws IOException {
640            closing = false;
641
642            byte[] data = footer.toByteArray();
643            if (data.length < 2)
644                throw new IOException("Less than two bytes written to footer");
645            write(data, 0, data.length - 2);
646        }
647
648        public byte[] getTail() {
649            return footer.toByteArray();
650        }
651
652        @Override
653        public void write(byte[] b) throws IOException {
654            write(b, 0, b.length);
655        }
656
657        @Override
658        public void write(byte[] b, int off, int len) throws IOException {
659            if (closing) {
660                // if the jar is about to close, save the footer that will be written
661                footer.write(b, off, len);
662            }
663            else {
664                // write to both output streams. out is the CMSTypedData signer and tee is the file.
665                out.write(b, off, len);
666                tee.write(b, off, len);
667            }
668        }
669
670        @Override
671        public void write(int b) throws IOException {
672            if (closing) {
673                // if the jar is about to close, save the footer that will be written
674                footer.write(b);
675            }
676            else {
677                // write to both output streams. out is the CMSTypedData signer and tee is the file.
678                out.write(b);
679                tee.write(b);
680            }
681        }
682    }
683
684    private static class CMSSigner implements CMSTypedData {
685        private final JarFile inputJar;
686        private final File publicKeyFile;
687        private final X509Certificate publicKey;
688        private final PrivateKey privateKey;
689        private final long timestamp;
690        private final int minSdkVersion;
691        private final OutputStream outputStream;
692        private final ASN1ObjectIdentifier type;
693        private WholeFileSignerOutputStream signer;
694
695        public CMSSigner(JarFile inputJar, File publicKeyFile,
696                         X509Certificate publicKey, PrivateKey privateKey, long timestamp,
697                         int minSdkVersion, OutputStream outputStream) {
698            this.inputJar = inputJar;
699            this.publicKeyFile = publicKeyFile;
700            this.publicKey = publicKey;
701            this.privateKey = privateKey;
702            this.timestamp = timestamp;
703            this.minSdkVersion = minSdkVersion;
704            this.outputStream = outputStream;
705            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
706        }
707
708        /**
709         * This should actually return byte[] or something similar, but nothing
710         * actually checks it currently.
711         */
712        @Override
713        public Object getContent() {
714            return this;
715        }
716
717        @Override
718        public ASN1ObjectIdentifier getContentType() {
719            return type;
720        }
721
722        @Override
723        public void write(OutputStream out) throws IOException {
724            try {
725                signer = new WholeFileSignerOutputStream(out, outputStream);
726                JarOutputStream outputJar = new JarOutputStream(signer);
727
728                int hash = getDigestAlgorithm(publicKey, minSdkVersion);
729
730                Manifest manifest = addDigestsToManifest(inputJar, hash);
731                copyFiles(manifest, inputJar, outputJar, timestamp, 0);
732                addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
733
734                signFile(manifest,
735                         new X509Certificate[]{ publicKey },
736                         new PrivateKey[]{ privateKey },
737                         timestamp,
738                         minSdkVersion,
739                         false, // Don't sign using APK Signature Scheme v2
740                         outputJar);
741
742                signer.notifyClosing();
743                outputJar.close();
744                signer.finish();
745            }
746            catch (Exception e) {
747                throw new IOException(e);
748            }
749        }
750
751        public void writeSignatureBlock(ByteArrayOutputStream temp)
752            throws IOException,
753                   CertificateEncodingException,
754                   OperatorCreationException,
755                   CMSException {
756            SignApk.writeSignatureBlock(this, publicKey, privateKey, minSdkVersion, temp);
757        }
758
759        public WholeFileSignerOutputStream getSigner() {
760            return signer;
761        }
762    }
763
764    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
765                                      X509Certificate publicKey, PrivateKey privateKey,
766                                      long timestamp, int minSdkVersion,
767                                      OutputStream outputStream) throws Exception {
768        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
769                publicKey, privateKey, timestamp, minSdkVersion, outputStream);
770
771        ByteArrayOutputStream temp = new ByteArrayOutputStream();
772
773        // put a readable message and a null char at the start of the
774        // archive comment, so that tools that display the comment
775        // (hopefully) show something sensible.
776        // TODO: anything more useful we can put in this message?
777        byte[] message = "signed by SignApk".getBytes("UTF-8");
778        temp.write(message);
779        temp.write(0);
780
781        cmsOut.writeSignatureBlock(temp);
782
783        byte[] zipData = cmsOut.getSigner().getTail();
784
785        // For a zip with no archive comment, the
786        // end-of-central-directory record will be 22 bytes long, so
787        // we expect to find the EOCD marker 22 bytes from the end.
788        if (zipData[zipData.length-22] != 0x50 ||
789            zipData[zipData.length-21] != 0x4b ||
790            zipData[zipData.length-20] != 0x05 ||
791            zipData[zipData.length-19] != 0x06) {
792            throw new IllegalArgumentException("zip data already has an archive comment");
793        }
794
795        int total_size = temp.size() + 6;
796        if (total_size > 0xffff) {
797            throw new IllegalArgumentException("signature is too big for ZIP file comment");
798        }
799        // signature starts this many bytes from the end of the file
800        int signature_start = total_size - message.length - 1;
801        temp.write(signature_start & 0xff);
802        temp.write((signature_start >> 8) & 0xff);
803        // Why the 0xff bytes?  In a zip file with no archive comment,
804        // bytes [-6:-2] of the file are the little-endian offset from
805        // the start of the file to the central directory.  So for the
806        // two high bytes to be 0xff 0xff, the archive would have to
807        // be nearly 4GB in size.  So it's unlikely that a real
808        // commentless archive would have 0xffs here, and lets us tell
809        // an old signed archive from a new one.
810        temp.write(0xff);
811        temp.write(0xff);
812        temp.write(total_size & 0xff);
813        temp.write((total_size >> 8) & 0xff);
814        temp.flush();
815
816        // Signature verification checks that the EOCD header is the
817        // last such sequence in the file (to avoid minzip finding a
818        // fake EOCD appended after the signature in its scan).  The
819        // odds of producing this sequence by chance are very low, but
820        // let's catch it here if it does.
821        byte[] b = temp.toByteArray();
822        for (int i = 0; i < b.length-3; ++i) {
823            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
824                throw new IllegalArgumentException("found spurious EOCD header at " + i);
825            }
826        }
827
828        outputStream.write(total_size & 0xff);
829        outputStream.write((total_size >> 8) & 0xff);
830        temp.writeTo(outputStream);
831    }
832
833    private static void signFile(Manifest manifest,
834                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
835                                 long timestamp,
836                                 int minSdkVersion,
837                                 boolean additionallySignedUsingAnApkSignatureScheme,
838                                 JarOutputStream outputJar)
839        throws Exception {
840
841        // MANIFEST.MF
842        JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
843        je.setTime(timestamp);
844        outputJar.putNextEntry(je);
845        manifest.write(outputJar);
846
847        int numKeys = publicKey.length;
848        for (int k = 0; k < numKeys; ++k) {
849            // CERT.SF / CERT#.SF
850            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
851                              (String.format(CERT_SF_MULTI_NAME, k)));
852            je.setTime(timestamp);
853            outputJar.putNextEntry(je);
854            ByteArrayOutputStream baos = new ByteArrayOutputStream();
855            writeSignatureFile(
856                    manifest,
857                    baos,
858                    getDigestAlgorithm(publicKey[k], minSdkVersion),
859                    additionallySignedUsingAnApkSignatureScheme);
860            byte[] signedData = baos.toByteArray();
861            outputJar.write(signedData);
862
863            // CERT.{EC,RSA} / CERT#.{EC,RSA}
864            final String keyType = publicKey[k].getPublicKey().getAlgorithm();
865            je = new JarEntry(numKeys == 1 ?
866                              (String.format(CERT_SIG_NAME, keyType)) :
867                              (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
868            je.setTime(timestamp);
869            outputJar.putNextEntry(je);
870            writeSignatureBlock(new CMSProcessableByteArray(signedData),
871                                publicKey[k], privateKey[k], minSdkVersion, outputJar);
872        }
873    }
874
875    /**
876     * Tries to load a JSE Provider by class name. This is for custom PrivateKey
877     * types that might be stored in PKCS#11-like storage.
878     */
879    private static void loadProviderIfNecessary(String providerClassName) {
880        if (providerClassName == null) {
881            return;
882        }
883
884        final Class<?> klass;
885        try {
886            final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
887            if (sysLoader != null) {
888                klass = sysLoader.loadClass(providerClassName);
889            } else {
890                klass = Class.forName(providerClassName);
891            }
892        } catch (ClassNotFoundException e) {
893            e.printStackTrace();
894            System.exit(1);
895            return;
896        }
897
898        Constructor<?> constructor = null;
899        for (Constructor<?> c : klass.getConstructors()) {
900            if (c.getParameterTypes().length == 0) {
901                constructor = c;
902                break;
903            }
904        }
905        if (constructor == null) {
906            System.err.println("No zero-arg constructor found for " + providerClassName);
907            System.exit(1);
908            return;
909        }
910
911        final Object o;
912        try {
913            o = constructor.newInstance();
914        } catch (Exception e) {
915            e.printStackTrace();
916            System.exit(1);
917            return;
918        }
919        if (!(o instanceof Provider)) {
920            System.err.println("Not a Provider class: " + providerClassName);
921            System.exit(1);
922        }
923
924        Security.insertProviderAt((Provider) o, 1);
925    }
926
927    /**
928     * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
929     * into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
930     */
931    public static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
932            PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
933                    throws InvalidKeyException {
934        if (privateKeys.length != certificates.length) {
935            throw new IllegalArgumentException(
936                    "The number of private keys must match the number of certificates: "
937                            + privateKeys.length + " vs" + certificates.length);
938        }
939        List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
940        for (int i = 0; i < privateKeys.length; i++) {
941            PrivateKey privateKey = privateKeys[i];
942            X509Certificate certificate = certificates[i];
943            PublicKey publicKey = certificate.getPublicKey();
944            String keyAlgorithm = privateKey.getAlgorithm();
945            if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
946                throw new InvalidKeyException(
947                        "Key algorithm of private key #" + (i + 1) + " does not match key"
948                        + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
949                        + " vs " + publicKey.getAlgorithm());
950            }
951            ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
952            signerConfig.privateKey = privateKey;
953            signerConfig.certificates = Collections.singletonList(certificate);
954            List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
955            for (String digestAlgorithm : digestAlgorithms) {
956                try {
957                    signatureAlgorithms.add(
958                            getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
959                } catch (IllegalArgumentException e) {
960                    throw new InvalidKeyException(
961                            "Unsupported key and digest algorithm combination for signer #"
962                                    + (i + 1),
963                            e);
964                }
965            }
966            signerConfig.signatureAlgorithms = signatureAlgorithms;
967            result.add(signerConfig);
968        }
969        return result;
970    }
971
972    private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
973        if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
974            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
975                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
976                // deterministic signatures which make life easier for OTA updates (fewer files
977                // changed when deterministic signature schemes are used).
978                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
979            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
980                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
981            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
982                return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
983            } else {
984                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
985            }
986        } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
987            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
988                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
989                // deterministic signatures which make life easier for OTA updates (fewer files
990                // changed when deterministic signature schemes are used).
991                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
992            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
993                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
994            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
995                throw new IllegalArgumentException("SHA-512 is not supported with DSA");
996            } else {
997                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
998            }
999        } else {
1000            throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
1001        }
1002    }
1003
1004    private static void usage() {
1005        System.err.println("Usage: signapk [-w] " +
1006                           "[-a <alignment>] " +
1007                           "[-providerClass <className>] " +
1008                           "[--min-sdk-version <n>] " +
1009                           "[--disable-v2] " +
1010                           "publickey.x509[.pem] privatekey.pk8 " +
1011                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
1012                           "input.jar output.jar");
1013        System.exit(2);
1014    }
1015
1016    public static void main(String[] args) {
1017        if (args.length < 4) usage();
1018
1019        // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
1020        // the standard or Bouncy Castle ones.
1021        Security.insertProviderAt(new OpenSSLProvider(), 1);
1022        // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
1023        // DSA which may still be needed.
1024        // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
1025        Security.addProvider(new BouncyCastleProvider());
1026
1027        boolean signWholeFile = false;
1028        String providerClass = null;
1029        int alignment = 4;
1030        int minSdkVersion = 0;
1031        boolean signUsingApkSignatureSchemeV2 = true;
1032
1033        int argstart = 0;
1034        while (argstart < args.length && args[argstart].startsWith("-")) {
1035            if ("-w".equals(args[argstart])) {
1036                signWholeFile = true;
1037                ++argstart;
1038            } else if ("-providerClass".equals(args[argstart])) {
1039                if (argstart + 1 >= args.length) {
1040                    usage();
1041                }
1042                providerClass = args[++argstart];
1043                ++argstart;
1044            } else if ("-a".equals(args[argstart])) {
1045                alignment = Integer.parseInt(args[++argstart]);
1046                ++argstart;
1047            } else if ("--min-sdk-version".equals(args[argstart])) {
1048                String minSdkVersionString = args[++argstart];
1049                try {
1050                    minSdkVersion = Integer.parseInt(minSdkVersionString);
1051                } catch (NumberFormatException e) {
1052                    throw new IllegalArgumentException(
1053                            "--min-sdk-version must be a decimal number: " + minSdkVersionString);
1054                }
1055                ++argstart;
1056            } else if ("--disable-v2".equals(args[argstart])) {
1057                signUsingApkSignatureSchemeV2 = false;
1058                ++argstart;
1059            } else {
1060                usage();
1061            }
1062        }
1063
1064        if ((args.length - argstart) % 2 == 1) usage();
1065        int numKeys = ((args.length - argstart) / 2) - 1;
1066        if (signWholeFile && numKeys > 1) {
1067            System.err.println("Only one key may be used with -w.");
1068            System.exit(2);
1069        }
1070
1071        loadProviderIfNecessary(providerClass);
1072
1073        String inputFilename = args[args.length-2];
1074        String outputFilename = args[args.length-1];
1075
1076        JarFile inputJar = null;
1077        FileOutputStream outputFile = null;
1078        int hashes = 0;
1079
1080        try {
1081            File firstPublicKeyFile = new File(args[argstart+0]);
1082
1083            X509Certificate[] publicKey = new X509Certificate[numKeys];
1084            try {
1085                for (int i = 0; i < numKeys; ++i) {
1086                    int argNum = argstart + i*2;
1087                    publicKey[i] = readPublicKey(new File(args[argNum]));
1088                    hashes |= getDigestAlgorithm(publicKey[i], minSdkVersion);
1089                }
1090            } catch (IllegalArgumentException e) {
1091                System.err.println(e);
1092                System.exit(1);
1093            }
1094
1095            // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
1096            long timestamp = 1230768000000L;
1097            // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
1098            // timestamp using the current timezone. We thus adjust the milliseconds since epoch
1099            // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
1100            timestamp -= TimeZone.getDefault().getOffset(timestamp);
1101
1102            PrivateKey[] privateKey = new PrivateKey[numKeys];
1103            for (int i = 0; i < numKeys; ++i) {
1104                int argNum = argstart + i*2 + 1;
1105                privateKey[i] = readPrivateKey(new File(args[argNum]));
1106            }
1107            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
1108
1109            outputFile = new FileOutputStream(outputFilename);
1110
1111            // NOTE: Signing currently recompresses any compressed entries using Deflate (default
1112            // compression level for OTA update files and maximum compession level for APKs).
1113            if (signWholeFile) {
1114                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
1115                                      publicKey[0], privateKey[0],
1116                                      timestamp, minSdkVersion,
1117                                      outputFile);
1118            } else {
1119                // Generate, in memory, an APK signed using standard JAR Signature Scheme.
1120                ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
1121                JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
1122                // Use maximum compression for compressed entries because the APK lives forever on
1123                // the system partition.
1124                outputJar.setLevel(9);
1125                Manifest manifest = addDigestsToManifest(inputJar, hashes);
1126                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
1127                signFile(
1128                        manifest,
1129                        publicKey, privateKey,
1130                        timestamp, minSdkVersion, signUsingApkSignatureSchemeV2,
1131                        outputJar);
1132                outputJar.close();
1133                ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
1134                v1SignedApkBuf.reset();
1135
1136                ByteBuffer[] outputChunks;
1137                if (signUsingApkSignatureSchemeV2) {
1138                    // Additionally sign the APK using the APK Signature Scheme v2.
1139                    ByteBuffer apkContents = v1SignedApk;
1140                    List<ApkSignerV2.SignerConfig> signerConfigs =
1141                            createV2SignerConfigs(
1142                                    privateKey,
1143                                    publicKey,
1144                                    new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
1145                    outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
1146                } else {
1147                    // Output the JAR-signed APK as is.
1148                    outputChunks = new ByteBuffer[] {v1SignedApk};
1149                }
1150
1151                // This assumes outputChunks are array-backed. To avoid this assumption, the
1152                // code could be rewritten to use FileChannel.
1153                for (ByteBuffer outputChunk : outputChunks) {
1154                    outputFile.write(
1155                            outputChunk.array(),
1156                            outputChunk.arrayOffset() + outputChunk.position(),
1157                            outputChunk.remaining());
1158                    outputChunk.position(outputChunk.limit());
1159                }
1160
1161                outputFile.close();
1162                outputFile = null;
1163                return;
1164            }
1165        } catch (Exception e) {
1166            e.printStackTrace();
1167            System.exit(1);
1168        } finally {
1169            try {
1170                if (inputJar != null) inputJar.close();
1171                if (outputFile != null) outputFile.close();
1172            } catch (IOException e) {
1173                e.printStackTrace();
1174                System.exit(1);
1175            }
1176        }
1177    }
1178}
1179