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