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