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