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