SignApk.java revision ab2a3b0061f26ebc95bc320fcfac316ccf14f567
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.CMSSignedData;
27import org.bouncycastle.cms.CMSSignedDataGenerator;
28import org.bouncycastle.cms.CMSTypedData;
29import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
30import org.bouncycastle.jce.provider.BouncyCastleProvider;
31import org.bouncycastle.operator.ContentSigner;
32import org.bouncycastle.operator.OperatorCreationException;
33import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
34import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
35import org.conscrypt.OpenSSLProvider;
36
37import com.android.apksigner.core.ApkSignerEngine;
38import com.android.apksigner.core.DefaultApkSignerEngine;
39import com.android.apksigner.core.apk.ApkUtils;
40import com.android.apksigner.core.util.DataSink;
41import com.android.apksigner.core.util.DataSources;
42import com.android.apksigner.core.zip.ZipFormatException;
43
44import java.io.Console;
45import java.io.BufferedReader;
46import java.io.ByteArrayInputStream;
47import java.io.ByteArrayOutputStream;
48import java.io.DataInputStream;
49import java.io.File;
50import java.io.FileInputStream;
51import java.io.FileOutputStream;
52import java.io.FilterOutputStream;
53import java.io.IOException;
54import java.io.InputStream;
55import java.io.InputStreamReader;
56import java.io.OutputStream;
57import java.lang.reflect.Constructor;
58import java.nio.ByteBuffer;
59import java.nio.ByteOrder;
60import java.security.GeneralSecurityException;
61import java.security.Key;
62import java.security.KeyFactory;
63import java.security.PrivateKey;
64import java.security.Provider;
65import java.security.Security;
66import java.security.cert.CertificateEncodingException;
67import java.security.cert.CertificateFactory;
68import java.security.cert.X509Certificate;
69import java.security.spec.InvalidKeySpecException;
70import java.security.spec.PKCS8EncodedKeySpec;
71import java.util.ArrayList;
72import java.util.Collections;
73import java.util.Enumeration;
74import java.util.List;
75import java.util.Locale;
76import java.util.TimeZone;
77import java.util.jar.JarEntry;
78import java.util.jar.JarFile;
79import java.util.jar.JarOutputStream;
80import java.util.regex.Pattern;
81
82import javax.crypto.Cipher;
83import javax.crypto.EncryptedPrivateKeyInfo;
84import javax.crypto.SecretKeyFactory;
85import javax.crypto.spec.PBEKeySpec;
86
87/**
88 * HISTORICAL NOTE:
89 *
90 * Prior to the keylimepie release, SignApk ignored the signature
91 * algorithm specified in the certificate and always used SHA1withRSA.
92 *
93 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
94 * the signature algorithm in the certificate to select which to use
95 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
96 *
97 * Because there are old keys still in use whose certificate actually
98 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
99 * for compatibility with older releases.  This can be changed by
100 * altering the getAlgorithm() function below.
101 */
102
103
104/**
105 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
106 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
107 * SHA-256 (see historical note). The tool can additionally sign APKs using
108 * APK Signature Scheme v2.
109 */
110class SignApk {
111    private static final String OTACERT_NAME = "META-INF/com/android/otacert";
112
113    /**
114     * Extensible data block/field header ID used for storing information about alignment of
115     * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
116     * 4.5 Extensible data fields.
117     */
118    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
119
120    /**
121     * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
122     * entries.
123     */
124    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
125
126    // bitmasks for which hash algorithms we need the manifest to include.
127    private static final int USE_SHA1 = 1;
128    private static final int USE_SHA256 = 2;
129
130    /**
131     * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
132     * for signing an OTA update package using the private key corresponding to the provided
133     * certificate.
134     */
135    private static int getDigestAlgorithmForOta(X509Certificate cert) {
136        String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
137        if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
138            // see "HISTORICAL NOTE" above.
139            return USE_SHA1;
140        } else if (sigAlg.startsWith("SHA256WITH")) {
141            return USE_SHA256;
142        } else {
143            throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
144                                               "\" in cert [" + cert.getSubjectDN());
145        }
146    }
147
148    /**
149     * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
150     * update package using the private key corresponding to the provided certificate and the
151     * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
152     */
153    private static String getJcaSignatureAlgorithmForOta(
154            X509Certificate cert, int hash) {
155        String sigAlgDigestPrefix;
156        switch (hash) {
157            case USE_SHA1:
158                sigAlgDigestPrefix = "SHA1";
159                break;
160            case USE_SHA256:
161                sigAlgDigestPrefix = "SHA256";
162                break;
163            default:
164                throw new IllegalArgumentException("Unknown hash ID: " + hash);
165        }
166
167        String keyAlgorithm = cert.getPublicKey().getAlgorithm();
168        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
169            return sigAlgDigestPrefix + "withRSA";
170        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
171            return sigAlgDigestPrefix + "withECDSA";
172        } else {
173            throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
174        }
175    }
176
177    private static X509Certificate readPublicKey(File file)
178        throws IOException, GeneralSecurityException {
179        FileInputStream input = new FileInputStream(file);
180        try {
181            CertificateFactory cf = CertificateFactory.getInstance("X.509");
182            return (X509Certificate) cf.generateCertificate(input);
183        } finally {
184            input.close();
185        }
186    }
187
188    /**
189     * If a console doesn't exist, reads the password from stdin
190     * If a console exists, reads the password from console and returns it as a string.
191     *
192     * @param keyFile The file containing the private key.  Used to prompt the user.
193     */
194    private static String readPassword(File keyFile) {
195        Console console;
196        char[] pwd;
197        if ((console = System.console()) == null) {
198            System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
199            System.out.flush();
200            BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
201            try {
202                return stdin.readLine();
203            } catch (IOException ex) {
204                return null;
205            }
206        } else {
207            if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
208                return String.valueOf(pwd);
209            } else {
210                return null;
211            }
212        }
213    }
214
215    /**
216     * Decrypt an encrypted PKCS#8 format private key.
217     *
218     * Based on ghstark's post on Aug 6, 2006 at
219     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
220     *
221     * @param encryptedPrivateKey The raw data of the private key
222     * @param keyFile The file containing the private key
223     */
224    private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
225        throws GeneralSecurityException {
226        EncryptedPrivateKeyInfo epkInfo;
227        try {
228            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
229        } catch (IOException ex) {
230            // Probably not an encrypted key.
231            return null;
232        }
233
234        char[] password = readPassword(keyFile).toCharArray();
235
236        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
237        Key key = skFactory.generateSecret(new PBEKeySpec(password));
238
239        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
240        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
241
242        try {
243            return epkInfo.getKeySpec(cipher);
244        } catch (InvalidKeySpecException ex) {
245            System.err.println("signapk: Password for " + keyFile + " may be bad.");
246            throw ex;
247        }
248    }
249
250    /** Read a PKCS#8 format private key. */
251    private static PrivateKey readPrivateKey(File file)
252        throws IOException, GeneralSecurityException {
253        DataInputStream input = new DataInputStream(new FileInputStream(file));
254        try {
255            byte[] bytes = new byte[(int) file.length()];
256            input.read(bytes);
257
258            /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
259            PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
260            if (spec == null) {
261                spec = new PKCS8EncodedKeySpec(bytes);
262            }
263
264            /*
265             * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
266             * OID and use that to construct a KeyFactory.
267             */
268            PrivateKeyInfo pki;
269            try (ASN1InputStream bIn =
270                    new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
271                pki = PrivateKeyInfo.getInstance(bIn.readObject());
272            }
273            String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
274
275            return KeyFactory.getInstance(algOid).generatePrivate(spec);
276        } finally {
277            input.close();
278        }
279    }
280
281    /**
282     * Add a copy of the public key to the archive; this should
283     * exactly match one of the files in
284     * /system/etc/security/otacerts.zip on the device.  (The same
285     * cert can be extracted from the OTA update package's signature
286     * block but this is much easier to get at.)
287     */
288    private static void addOtacert(JarOutputStream outputJar,
289                                   File publicKeyFile,
290                                   long timestamp)
291        throws IOException {
292
293        JarEntry je = new JarEntry(OTACERT_NAME);
294        je.setTime(timestamp);
295        outputJar.putNextEntry(je);
296        FileInputStream input = new FileInputStream(publicKeyFile);
297        byte[] b = new byte[4096];
298        int read;
299        while ((read = input.read(b)) != -1) {
300            outputJar.write(b, 0, read);
301        }
302        input.close();
303    }
304
305
306    /** Sign data and write the digital signature to 'out'. */
307    private static void writeSignatureBlock(
308        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
309        OutputStream out)
310        throws IOException,
311               CertificateEncodingException,
312               OperatorCreationException,
313               CMSException {
314        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
315        certList.add(publicKey);
316        JcaCertStore certs = new JcaCertStore(certList);
317
318        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
319        ContentSigner signer =
320                new JcaContentSignerBuilder(
321                        getJcaSignatureAlgorithmForOta(publicKey, hash))
322                        .build(privateKey);
323        gen.addSignerInfoGenerator(
324            new JcaSignerInfoGeneratorBuilder(
325                new JcaDigestCalculatorProviderBuilder()
326                .build())
327            .setDirectSignature(true)
328            .build(signer, publicKey));
329        gen.addCertificates(certs);
330        CMSSignedData sigData = gen.generate(data, false);
331
332        try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
333            DEROutputStream dos = new DEROutputStream(out);
334            dos.writeObject(asn1.readObject());
335        }
336    }
337
338    /**
339     * Adds ZIP entries which represent the v1 signature (JAR signature scheme).
340     */
341    private static void addV1Signature(
342            ApkSignerEngine apkSigner,
343            ApkSignerEngine.OutputJarSignatureRequest v1Signature,
344            JarOutputStream out,
345            long timestamp) throws IOException {
346        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry
347                : v1Signature.getAdditionalJarEntries()) {
348            String entryName = entry.getName();
349            JarEntry outEntry = new JarEntry(entryName);
350            outEntry.setTime(timestamp);
351            out.putNextEntry(outEntry);
352            byte[] entryData = entry.getData();
353            out.write(entryData);
354            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
355                    apkSigner.outputJarEntry(entryName);
356            if (inspectEntryRequest != null) {
357                inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length);
358                inspectEntryRequest.done();
359            }
360        }
361    }
362
363    /**
364     * Copy all JAR entries from input to output. We set the modification times in the output to a
365     * fixed time, so as to reduce variation in the output file and make incremental OTAs more
366     * efficient.
367     */
368    private static void copyFiles(
369            JarFile in,
370            Pattern ignoredFilenamePattern,
371            ApkSignerEngine apkSigner,
372            JarOutputStream out,
373            long timestamp,
374            int defaultAlignment) throws IOException {
375        byte[] buffer = new byte[4096];
376        int num;
377
378        ArrayList<String> names = new ArrayList<String>();
379        for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) {
380            JarEntry entry = e.nextElement();
381            if (entry.isDirectory()) {
382                continue;
383            }
384            String entryName = entry.getName();
385            if ((ignoredFilenamePattern != null)
386                    && (ignoredFilenamePattern.matcher(entryName).matches())) {
387                continue;
388            }
389            names.add(entryName);
390        }
391        Collections.sort(names);
392
393        boolean firstEntry = true;
394        long offset = 0L;
395
396        // We do the copy in two passes -- first copying all the
397        // entries that are STORED, then copying all the entries that
398        // have any other compression flag (which in practice means
399        // DEFLATED).  This groups all the stored entries together at
400        // the start of the file and makes it easier to do alignment
401        // on them (since only stored entries are aligned).
402
403        List<String> remainingNames = new ArrayList<>(names.size());
404        for (String name : names) {
405            JarEntry inEntry = in.getJarEntry(name);
406            if (inEntry.getMethod() != JarEntry.STORED) {
407                // Defer outputting this entry until we're ready to output compressed entries.
408                remainingNames.add(name);
409                continue;
410            }
411
412            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
413                continue;
414            }
415
416            // Preserve the STORED method of the input entry.
417            JarEntry outEntry = new JarEntry(inEntry);
418            outEntry.setTime(timestamp);
419            // Discard comment and extra fields of this entry to
420            // simplify alignment logic below and for consistency with
421            // how compressed entries are handled later.
422            outEntry.setComment(null);
423            outEntry.setExtra(null);
424
425            int alignment = getStoredEntryDataAlignment(name, defaultAlignment);
426            // Alignment of the entry's data is achieved by adding a data block to the entry's Local
427            // File Header extra field. The data block contains information about the alignment
428            // value and the necessary padding bytes (0x00) to achieve the alignment.  This works
429            // because the entry's data will be located immediately after the extra field.
430            // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format
431            // of the extra field.
432
433            // 'offset' is the offset into the file at which we expect the entry's data to begin.
434            // This is the value we need to make a multiple of 'alignment'.
435            offset += JarFile.LOCHDR + outEntry.getName().length();
436            if (firstEntry) {
437                // The first entry in a jar file has an extra field of four bytes that you can't get
438                // rid of; any extra data you specify in the JarEntry is appended to these forced
439                // four bytes.  This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000.
440                // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540
441                // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619.
442                offset += 4;
443                firstEntry = false;
444            }
445            int extraPaddingSizeBytes = 0;
446            if (alignment > 0) {
447                long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
448                extraPaddingSizeBytes =
449                        (alignment - (int) (paddingStartOffset % alignment)) % alignment;
450            }
451            byte[] extra =
452                    new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes];
453            ByteBuffer extraBuf = ByteBuffer.wrap(extra);
454            extraBuf.order(ByteOrder.LITTLE_ENDIAN);
455            extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID
456            extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size
457            extraBuf.putShort((short) alignment);
458            outEntry.setExtra(extra);
459            offset += extra.length;
460
461            out.putNextEntry(outEntry);
462            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
463                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
464            DataSink entryDataSink =
465                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
466
467            try (InputStream data = in.getInputStream(inEntry)) {
468                while ((num = data.read(buffer)) > 0) {
469                    out.write(buffer, 0, num);
470                    if (entryDataSink != null) {
471                        entryDataSink.consume(buffer, 0, num);
472                    }
473                    offset += num;
474                }
475            }
476            out.flush();
477            if (inspectEntryRequest != null) {
478                inspectEntryRequest.done();
479            }
480        }
481
482        // Copy all the non-STORED entries.  We don't attempt to
483        // maintain the 'offset' variable past this point; we don't do
484        // alignment on these entries.
485
486        for (String name : remainingNames) {
487            JarEntry inEntry = in.getJarEntry(name);
488            if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) {
489                continue;
490            }
491
492            // Create a new entry so that the compressed len is recomputed.
493            JarEntry outEntry = new JarEntry(name);
494            outEntry.setTime(timestamp);
495            out.putNextEntry(outEntry);
496            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
497                    (apkSigner != null) ? apkSigner.outputJarEntry(name) : null;
498            DataSink entryDataSink =
499                    (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null;
500
501            InputStream data = in.getInputStream(inEntry);
502            while ((num = data.read(buffer)) > 0) {
503                out.write(buffer, 0, num);
504                if (entryDataSink != null) {
505                    entryDataSink.consume(buffer, 0, num);
506                }
507            }
508            out.flush();
509            if (inspectEntryRequest != null) {
510                inspectEntryRequest.done();
511            }
512        }
513    }
514
515    private static boolean shouldOutputApkEntry(
516            ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)
517                    throws IOException {
518        if (apkSigner == null) {
519            return true;
520        }
521
522        ApkSignerEngine.InputJarEntryInstructions instructions =
523                apkSigner.inputJarEntry(inEntry.getName());
524        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
525                instructions.getInspectJarEntryRequest();
526        if (inspectEntryRequest != null) {
527            provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf);
528        }
529        switch (instructions.getOutputPolicy()) {
530            case OUTPUT:
531                return true;
532            case SKIP:
533            case OUTPUT_BY_ENGINE:
534                return false;
535            default:
536                throw new RuntimeException(
537                        "Unsupported output policy: " + instructions.getOutputPolicy());
538        }
539    }
540
541    private static void provideJarEntry(
542            JarFile jarFile,
543            JarEntry jarEntry,
544            ApkSignerEngine.InspectJarEntryRequest request,
545            byte[] tmpbuf) throws IOException {
546        DataSink dataSink = request.getDataSink();
547        try (InputStream in = jarFile.getInputStream(jarEntry)) {
548            int chunkSize;
549            while ((chunkSize = in.read(tmpbuf)) > 0) {
550                dataSink.consume(tmpbuf, 0, chunkSize);
551            }
552            request.done();
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 final JarFile inputJar;
635        private final File publicKeyFile;
636        private final X509Certificate publicKey;
637        private final PrivateKey privateKey;
638        private final int hash;
639        private final long timestamp;
640        private final OutputStream outputStream;
641        private final ASN1ObjectIdentifier type;
642        private WholeFileSignerOutputStream signer;
643
644        // Files matching this pattern are not copied to the output.
645        private static final Pattern STRIP_PATTERN =
646                Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
647                        + Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
648
649        public CMSSigner(JarFile inputJar, File publicKeyFile,
650                         X509Certificate publicKey, PrivateKey privateKey, int hash,
651                         long timestamp, OutputStream outputStream) {
652            this.inputJar = inputJar;
653            this.publicKeyFile = publicKeyFile;
654            this.publicKey = publicKey;
655            this.privateKey = privateKey;
656            this.hash = hash;
657            this.timestamp = timestamp;
658            this.outputStream = outputStream;
659            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
660        }
661
662        /**
663         * This should actually return byte[] or something similar, but nothing
664         * actually checks it currently.
665         */
666        @Override
667        public Object getContent() {
668            return this;
669        }
670
671        @Override
672        public ASN1ObjectIdentifier getContentType() {
673            return type;
674        }
675
676        @Override
677        public void write(OutputStream out) throws IOException {
678            try {
679                signer = new WholeFileSignerOutputStream(out, outputStream);
680                JarOutputStream outputJar = new JarOutputStream(signer);
681
682                copyFiles(inputJar, STRIP_PATTERN, null, outputJar, timestamp, 0);
683                addOtacert(outputJar, publicKeyFile, timestamp);
684
685                signer.notifyClosing();
686                outputJar.close();
687                signer.finish();
688            }
689            catch (Exception e) {
690                throw new IOException(e);
691            }
692        }
693
694        public void writeSignatureBlock(ByteArrayOutputStream temp)
695            throws IOException,
696                   CertificateEncodingException,
697                   OperatorCreationException,
698                   CMSException {
699            SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp);
700        }
701
702        public WholeFileSignerOutputStream getSigner() {
703            return signer;
704        }
705    }
706
707    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
708                                      X509Certificate publicKey, PrivateKey privateKey,
709                                      int hash, long timestamp,
710                                      OutputStream outputStream) throws Exception {
711        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
712                publicKey, privateKey, hash, timestamp, outputStream);
713
714        ByteArrayOutputStream temp = new ByteArrayOutputStream();
715
716        // put a readable message and a null char at the start of the
717        // archive comment, so that tools that display the comment
718        // (hopefully) show something sensible.
719        // TODO: anything more useful we can put in this message?
720        byte[] message = "signed by SignApk".getBytes("UTF-8");
721        temp.write(message);
722        temp.write(0);
723
724        cmsOut.writeSignatureBlock(temp);
725
726        byte[] zipData = cmsOut.getSigner().getTail();
727
728        // For a zip with no archive comment, the
729        // end-of-central-directory record will be 22 bytes long, so
730        // we expect to find the EOCD marker 22 bytes from the end.
731        if (zipData[zipData.length-22] != 0x50 ||
732            zipData[zipData.length-21] != 0x4b ||
733            zipData[zipData.length-20] != 0x05 ||
734            zipData[zipData.length-19] != 0x06) {
735            throw new IllegalArgumentException("zip data already has an archive comment");
736        }
737
738        int total_size = temp.size() + 6;
739        if (total_size > 0xffff) {
740            throw new IllegalArgumentException("signature is too big for ZIP file comment");
741        }
742        // signature starts this many bytes from the end of the file
743        int signature_start = total_size - message.length - 1;
744        temp.write(signature_start & 0xff);
745        temp.write((signature_start >> 8) & 0xff);
746        // Why the 0xff bytes?  In a zip file with no archive comment,
747        // bytes [-6:-2] of the file are the little-endian offset from
748        // the start of the file to the central directory.  So for the
749        // two high bytes to be 0xff 0xff, the archive would have to
750        // be nearly 4GB in size.  So it's unlikely that a real
751        // commentless archive would have 0xffs here, and lets us tell
752        // an old signed archive from a new one.
753        temp.write(0xff);
754        temp.write(0xff);
755        temp.write(total_size & 0xff);
756        temp.write((total_size >> 8) & 0xff);
757        temp.flush();
758
759        // Signature verification checks that the EOCD header is the
760        // last such sequence in the file (to avoid minzip finding a
761        // fake EOCD appended after the signature in its scan).  The
762        // odds of producing this sequence by chance are very low, but
763        // let's catch it here if it does.
764        byte[] b = temp.toByteArray();
765        for (int i = 0; i < b.length-3; ++i) {
766            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
767                throw new IllegalArgumentException("found spurious EOCD header at " + i);
768            }
769        }
770
771        outputStream.write(total_size & 0xff);
772        outputStream.write((total_size >> 8) & 0xff);
773        temp.writeTo(outputStream);
774    }
775
776    /**
777     * Tries to load a JSE Provider by class name. This is for custom PrivateKey
778     * types that might be stored in PKCS#11-like storage.
779     */
780    private static void loadProviderIfNecessary(String providerClassName) {
781        if (providerClassName == null) {
782            return;
783        }
784
785        final Class<?> klass;
786        try {
787            final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
788            if (sysLoader != null) {
789                klass = sysLoader.loadClass(providerClassName);
790            } else {
791                klass = Class.forName(providerClassName);
792            }
793        } catch (ClassNotFoundException e) {
794            e.printStackTrace();
795            System.exit(1);
796            return;
797        }
798
799        Constructor<?> constructor = null;
800        for (Constructor<?> c : klass.getConstructors()) {
801            if (c.getParameterTypes().length == 0) {
802                constructor = c;
803                break;
804            }
805        }
806        if (constructor == null) {
807            System.err.println("No zero-arg constructor found for " + providerClassName);
808            System.exit(1);
809            return;
810        }
811
812        final Object o;
813        try {
814            o = constructor.newInstance();
815        } catch (Exception e) {
816            e.printStackTrace();
817            System.exit(1);
818            return;
819        }
820        if (!(o instanceof Provider)) {
821            System.err.println("Not a Provider class: " + providerClassName);
822            System.exit(1);
823        }
824
825        Security.insertProviderAt((Provider) o, 1);
826    }
827
828    private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs(
829            PrivateKey[] privateKeys, X509Certificate[] certificates) {
830        if (privateKeys.length != certificates.length) {
831            throw new IllegalArgumentException(
832                    "The number of private keys must match the number of certificates: "
833                            + privateKeys.length + " vs" + certificates.length);
834        }
835        List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>();
836        String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s";
837        for (int i = 0; i < privateKeys.length; i++) {
838            String signerName = String.format(Locale.US, signerNameFormat, (i + 1));
839            DefaultApkSignerEngine.SignerConfig signerConfig =
840                    new DefaultApkSignerEngine.SignerConfig.Builder(
841                            signerName,
842                            privateKeys[i],
843                            Collections.singletonList(certificates[i]))
844                            .build();
845            signerConfigs.add(signerConfig);
846        }
847        return signerConfigs;
848    }
849
850    private static class ZipSections {
851        ByteBuffer beforeCentralDir;
852        ByteBuffer centralDir;
853        ByteBuffer eocd;
854    }
855
856    private static ZipSections findMainZipSections(ByteBuffer apk)
857            throws IOException, ZipFormatException {
858        apk.slice();
859        ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk));
860        long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
861        long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes();
862        long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes;
863        long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
864        if (centralDirEndOffset != eocdStartOffset) {
865            throw new ZipFormatException(
866                    "ZIP Central Directory is not immediately followed by End of Central Directory"
867                            + ". CD end: " + centralDirEndOffset
868                            + ", EoCD start: " + eocdStartOffset);
869        }
870        apk.position(0);
871        apk.limit((int) centralDirStartOffset);
872        ByteBuffer beforeCentralDir = apk.slice();
873
874        apk.position((int) centralDirStartOffset);
875        apk.limit((int) centralDirEndOffset);
876        ByteBuffer centralDir = apk.slice();
877
878        apk.position((int) eocdStartOffset);
879        apk.limit(apk.capacity());
880        ByteBuffer eocd = apk.slice();
881
882        apk.position(0);
883        apk.limit(apk.capacity());
884
885        ZipSections result = new ZipSections();
886        result.beforeCentralDir = beforeCentralDir;
887        result.centralDir = centralDir;
888        result.eocd = eocd;
889        return result;
890    }
891
892    private static void usage() {
893        System.err.println("Usage: signapk [-w] " +
894                           "[-a <alignment>] " +
895                           "[-providerClass <className>] " +
896                           "[--min-sdk-version <n>] " +
897                           "[--disable-v2] " +
898                           "publickey.x509[.pem] privatekey.pk8 " +
899                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
900                           "input.jar output.jar");
901        System.exit(2);
902    }
903
904    public static void main(String[] args) {
905        if (args.length < 4) usage();
906
907        // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than
908        // the standard or Bouncy Castle ones.
909        Security.insertProviderAt(new OpenSSLProvider(), 1);
910        // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer
911        // DSA which may still be needed.
912        // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed.
913        Security.addProvider(new BouncyCastleProvider());
914
915        boolean signWholeFile = false;
916        String providerClass = null;
917        int alignment = 4;
918        int minSdkVersion = 0;
919        boolean signUsingApkSignatureSchemeV2 = true;
920
921        int argstart = 0;
922        while (argstart < args.length && args[argstart].startsWith("-")) {
923            if ("-w".equals(args[argstart])) {
924                signWholeFile = true;
925                ++argstart;
926            } else if ("-providerClass".equals(args[argstart])) {
927                if (argstart + 1 >= args.length) {
928                    usage();
929                }
930                providerClass = args[++argstart];
931                ++argstart;
932            } else if ("-a".equals(args[argstart])) {
933                alignment = Integer.parseInt(args[++argstart]);
934                ++argstart;
935            } else if ("--min-sdk-version".equals(args[argstart])) {
936                String minSdkVersionString = args[++argstart];
937                try {
938                    minSdkVersion = Integer.parseInt(minSdkVersionString);
939                } catch (NumberFormatException e) {
940                    throw new IllegalArgumentException(
941                            "--min-sdk-version must be a decimal number: " + minSdkVersionString);
942                }
943                ++argstart;
944            } else if ("--disable-v2".equals(args[argstart])) {
945                signUsingApkSignatureSchemeV2 = false;
946                ++argstart;
947            } else {
948                usage();
949            }
950        }
951
952        if ((args.length - argstart) % 2 == 1) usage();
953        int numKeys = ((args.length - argstart) / 2) - 1;
954        if (signWholeFile && numKeys > 1) {
955            System.err.println("Only one key may be used with -w.");
956            System.exit(2);
957        }
958
959        loadProviderIfNecessary(providerClass);
960
961        String inputFilename = args[args.length-2];
962        String outputFilename = args[args.length-1];
963
964        JarFile inputJar = null;
965        FileOutputStream outputFile = null;
966
967        try {
968            File firstPublicKeyFile = new File(args[argstart+0]);
969
970            X509Certificate[] publicKey = new X509Certificate[numKeys];
971            try {
972                for (int i = 0; i < numKeys; ++i) {
973                    int argNum = argstart + i*2;
974                    publicKey[i] = readPublicKey(new File(args[argNum]));
975                }
976            } catch (IllegalArgumentException e) {
977                System.err.println(e);
978                System.exit(1);
979            }
980
981            // Set all ZIP file timestamps to Jan 1 2009 00:00:00.
982            long timestamp = 1230768000000L;
983            // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
984            // timestamp using the current timezone. We thus adjust the milliseconds since epoch
985            // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
986            timestamp -= TimeZone.getDefault().getOffset(timestamp);
987
988            PrivateKey[] privateKey = new PrivateKey[numKeys];
989            for (int i = 0; i < numKeys; ++i) {
990                int argNum = argstart + i*2 + 1;
991                privateKey[i] = readPrivateKey(new File(args[argNum]));
992            }
993            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
994
995            outputFile = new FileOutputStream(outputFilename);
996
997            // NOTE: Signing currently recompresses any compressed entries using Deflate (default
998            // compression level for OTA update files and maximum compession level for APKs).
999            if (signWholeFile) {
1000                int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
1001                signWholeFile(inputJar, firstPublicKeyFile,
1002                        publicKey[0], privateKey[0], digestAlgorithm,
1003                        timestamp,
1004                        outputFile);
1005            } else {
1006                try (ApkSignerEngine apkSigner =
1007                        new DefaultApkSignerEngine.Builder(
1008                                createSignerConfigs(privateKey, publicKey), minSdkVersion)
1009                                .setV1SigningEnabled(true)
1010                                .setV2SigningEnabled(signUsingApkSignatureSchemeV2)
1011                                .setOtherSignersSignaturesPreserved(false)
1012                                .build()) {
1013                    // We don't preserve the input APK's APK Signing Block (which contains v2
1014                    // signatures)
1015                    apkSigner.inputApkSigningBlock(null);
1016
1017                    // Build the output APK in memory, by copying input APK's ZIP entries across
1018                    // and then signing the output APK.
1019                    ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
1020                    JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
1021                    // Use maximum compression for compressed entries because the APK lives forever
1022                    // on the system partition.
1023                    outputJar.setLevel(9);
1024                    copyFiles(inputJar, null, apkSigner, outputJar, timestamp, alignment);
1025                    ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest =
1026                            apkSigner.outputJarEntries();
1027                    if (addV1SignatureRequest != null) {
1028                        addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp);
1029                        addV1SignatureRequest.done();
1030                    }
1031                    outputJar.close();
1032                    ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
1033                    v1SignedApkBuf.reset();
1034                    ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk};
1035
1036                    ZipSections zipSections = findMainZipSections(v1SignedApk);
1037                    ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest =
1038                            apkSigner.outputZipSections(
1039                                    DataSources.asDataSource(zipSections.beforeCentralDir),
1040                                    DataSources.asDataSource(zipSections.centralDir),
1041                                    DataSources.asDataSource(zipSections.eocd));
1042                    if (addV2SignatureRequest != null) {
1043                        // Need to insert the returned APK Signing Block before ZIP Central
1044                        // Directory.
1045                        byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
1046                        // Because the APK Signing Block is inserted before the Central Directory,
1047                        // we need to adjust accordingly the offset of Central Directory inside the
1048                        // ZIP End of Central Directory (EoCD) record.
1049                        ByteBuffer modifiedEocd = ByteBuffer.allocate(zipSections.eocd.remaining());
1050                        modifiedEocd.put(zipSections.eocd);
1051                        modifiedEocd.flip();
1052                        modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
1053                        ApkUtils.setZipEocdCentralDirectoryOffset(
1054                                modifiedEocd,
1055                                zipSections.beforeCentralDir.remaining() + apkSigningBlock.length);
1056                        outputChunks =
1057                                new ByteBuffer[] {
1058                                        zipSections.beforeCentralDir,
1059                                        ByteBuffer.wrap(apkSigningBlock),
1060                                        zipSections.centralDir,
1061                                        modifiedEocd};
1062                        addV2SignatureRequest.done();
1063                    }
1064
1065                    // This assumes outputChunks are array-backed. To avoid this assumption, the
1066                    // code could be rewritten to use FileChannel.
1067                    for (ByteBuffer outputChunk : outputChunks) {
1068                        outputFile.write(
1069                                outputChunk.array(),
1070                                outputChunk.arrayOffset() + outputChunk.position(),
1071                                outputChunk.remaining());
1072                        outputChunk.position(outputChunk.limit());
1073                    }
1074
1075                    outputFile.close();
1076                    outputFile = null;
1077                    apkSigner.outputDone();
1078                }
1079
1080                return;
1081            }
1082        } catch (Exception e) {
1083            e.printStackTrace();
1084            System.exit(1);
1085        } finally {
1086            try {
1087                if (inputJar != null) inputJar.close();
1088                if (outputFile != null) outputFile.close();
1089            } catch (IOException e) {
1090                e.printStackTrace();
1091                System.exit(1);
1092            }
1093        }
1094    }
1095}
1096