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