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.cert.jcajce.JcaCertStore;
24import org.bouncycastle.cms.CMSException;
25import org.bouncycastle.cms.CMSProcessableByteArray;
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.bouncycastle.util.encoders.Base64;
36
37import java.io.BufferedReader;
38import java.io.BufferedOutputStream;
39import java.io.ByteArrayOutputStream;
40import java.io.DataInputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileOutputStream;
44import java.io.FilterOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.io.InputStreamReader;
48import java.io.OutputStream;
49import java.io.PrintStream;
50import java.security.DigestOutputStream;
51import java.security.GeneralSecurityException;
52import java.security.Key;
53import java.security.KeyFactory;
54import java.security.MessageDigest;
55import java.security.PrivateKey;
56import java.security.Provider;
57import java.security.Security;
58import java.security.cert.CertificateEncodingException;
59import java.security.cert.CertificateFactory;
60import java.security.cert.X509Certificate;
61import java.security.spec.InvalidKeySpecException;
62import java.security.spec.KeySpec;
63import java.security.spec.PKCS8EncodedKeySpec;
64import java.util.ArrayList;
65import java.util.Collections;
66import java.util.Enumeration;
67import java.util.Map;
68import java.util.TreeMap;
69import java.util.jar.Attributes;
70import java.util.jar.JarEntry;
71import java.util.jar.JarFile;
72import java.util.jar.JarOutputStream;
73import java.util.jar.Manifest;
74import java.util.regex.Pattern;
75import javax.crypto.Cipher;
76import javax.crypto.EncryptedPrivateKeyInfo;
77import javax.crypto.SecretKeyFactory;
78import javax.crypto.spec.PBEKeySpec;
79
80/**
81 * HISTORICAL NOTE:
82 *
83 * Prior to the keylimepie release, SignApk ignored the signature
84 * algorithm specified in the certificate and always used SHA1withRSA.
85 *
86 * Starting with keylimepie, we support SHA256withRSA, and use the
87 * signature algorithm in the certificate to select which to use
88 * (SHA256withRSA or SHA1withRSA).
89 *
90 * Because there are old keys still in use whose certificate actually
91 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
92 * for compatibility with older releases.  This can be changed by
93 * altering the getAlgorithm() function below.
94 */
95
96
97/**
98 * Command line tool to sign JAR files (including APKs and OTA
99 * updates) in a way compatible with the mincrypt verifier, using RSA
100 * keys and SHA1 or SHA-256.
101 */
102class SignApk {
103    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
104    private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
105    private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
106    private static final String CERT_RSA_MULTI_NAME = "META-INF/CERT%d.RSA";
107
108    private static final String OTACERT_NAME = "META-INF/com/android/otacert";
109
110    private static Provider sBouncyCastleProvider;
111
112    // bitmasks for which hash algorithms we need the manifest to include.
113    private static final int USE_SHA1 = 1;
114    private static final int USE_SHA256 = 2;
115
116    /**
117     * Return one of USE_SHA1 or USE_SHA256 according to the signature
118     * algorithm specified in the cert.
119     */
120    private static int getAlgorithm(X509Certificate cert) {
121        String sigAlg = cert.getSigAlgName();
122        if ("SHA1withRSA".equals(sigAlg) ||
123            "MD5withRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
124            return USE_SHA1;
125        } else if ("SHA256withRSA".equals(sigAlg)) {
126            return USE_SHA256;
127        } else {
128            throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
129                                               "\" in cert [" + cert.getSubjectDN());
130        }
131    }
132
133    // Files matching this pattern are not copied to the output.
134    private static Pattern stripPattern =
135        Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
136                        Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
137
138    private static X509Certificate readPublicKey(File file)
139        throws IOException, GeneralSecurityException {
140        FileInputStream input = new FileInputStream(file);
141        try {
142            CertificateFactory cf = CertificateFactory.getInstance("X.509");
143            return (X509Certificate) cf.generateCertificate(input);
144        } finally {
145            input.close();
146        }
147    }
148
149    /**
150     * Reads the password from stdin and returns it as a string.
151     *
152     * @param keyFile The file containing the private key.  Used to prompt the user.
153     */
154    private static String readPassword(File keyFile) {
155        // TODO: use Console.readPassword() when it's available.
156        System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
157        System.out.flush();
158        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
159        try {
160            return stdin.readLine();
161        } catch (IOException ex) {
162            return null;
163        }
164    }
165
166    /**
167     * Decrypt an encrypted PKCS 8 format private key.
168     *
169     * Based on ghstark's post on Aug 6, 2006 at
170     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
171     *
172     * @param encryptedPrivateKey The raw data of the private key
173     * @param keyFile The file containing the private key
174     */
175    private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
176        throws GeneralSecurityException {
177        EncryptedPrivateKeyInfo epkInfo;
178        try {
179            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
180        } catch (IOException ex) {
181            // Probably not an encrypted key.
182            return null;
183        }
184
185        char[] password = readPassword(keyFile).toCharArray();
186
187        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
188        Key key = skFactory.generateSecret(new PBEKeySpec(password));
189
190        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
191        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
192
193        try {
194            return epkInfo.getKeySpec(cipher);
195        } catch (InvalidKeySpecException ex) {
196            System.err.println("signapk: Password for " + keyFile + " may be bad.");
197            throw ex;
198        }
199    }
200
201    /** Read a PKCS 8 format private key. */
202    private static PrivateKey readPrivateKey(File file)
203        throws IOException, GeneralSecurityException {
204        DataInputStream input = new DataInputStream(new FileInputStream(file));
205        try {
206            byte[] bytes = new byte[(int) file.length()];
207            input.read(bytes);
208
209            KeySpec spec = decryptPrivateKey(bytes, file);
210            if (spec == null) {
211                spec = new PKCS8EncodedKeySpec(bytes);
212            }
213
214            try {
215                return KeyFactory.getInstance("RSA").generatePrivate(spec);
216            } catch (InvalidKeySpecException ex) {
217                return KeyFactory.getInstance("DSA").generatePrivate(spec);
218            }
219        } finally {
220            input.close();
221        }
222    }
223
224    /**
225     * Add the hash(es) of every file to the manifest, creating it if
226     * necessary.
227     */
228    private static Manifest addDigestsToManifest(JarFile jar, int hashes)
229        throws IOException, GeneralSecurityException {
230        Manifest input = jar.getManifest();
231        Manifest output = new Manifest();
232        Attributes main = output.getMainAttributes();
233        if (input != null) {
234            main.putAll(input.getMainAttributes());
235        } else {
236            main.putValue("Manifest-Version", "1.0");
237            main.putValue("Created-By", "1.0 (Android SignApk)");
238        }
239
240        MessageDigest md_sha1 = null;
241        MessageDigest md_sha256 = null;
242        if ((hashes & USE_SHA1) != 0) {
243            md_sha1 = MessageDigest.getInstance("SHA1");
244        }
245        if ((hashes & USE_SHA256) != 0) {
246            md_sha256 = MessageDigest.getInstance("SHA256");
247        }
248
249        byte[] buffer = new byte[4096];
250        int num;
251
252        // We sort the input entries by name, and add them to the
253        // output manifest in sorted order.  We expect that the output
254        // map will be deterministic.
255
256        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
257
258        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
259            JarEntry entry = e.nextElement();
260            byName.put(entry.getName(), entry);
261        }
262
263        for (JarEntry entry: byName.values()) {
264            String name = entry.getName();
265            if (!entry.isDirectory() &&
266                (stripPattern == null || !stripPattern.matcher(name).matches())) {
267                InputStream data = jar.getInputStream(entry);
268                while ((num = data.read(buffer)) > 0) {
269                    if (md_sha1 != null) md_sha1.update(buffer, 0, num);
270                    if (md_sha256 != null) md_sha256.update(buffer, 0, num);
271                }
272
273                Attributes attr = null;
274                if (input != null) attr = input.getAttributes(name);
275                attr = attr != null ? new Attributes(attr) : new Attributes();
276                if (md_sha1 != null) {
277                    attr.putValue("SHA1-Digest",
278                                  new String(Base64.encode(md_sha1.digest()), "ASCII"));
279                }
280                if (md_sha256 != null) {
281                    attr.putValue("SHA-256-Digest",
282                                  new String(Base64.encode(md_sha256.digest()), "ASCII"));
283                }
284                output.getEntries().put(name, attr);
285            }
286        }
287
288        return output;
289    }
290
291    /**
292     * Add a copy of the public key to the archive; this should
293     * exactly match one of the files in
294     * /system/etc/security/otacerts.zip on the device.  (The same
295     * cert can be extracted from the CERT.RSA file but this is much
296     * easier to get at.)
297     */
298    private static void addOtacert(JarOutputStream outputJar,
299                                   File publicKeyFile,
300                                   long timestamp,
301                                   Manifest manifest,
302                                   int hash)
303        throws IOException, GeneralSecurityException {
304        MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
305
306        JarEntry je = new JarEntry(OTACERT_NAME);
307        je.setTime(timestamp);
308        outputJar.putNextEntry(je);
309        FileInputStream input = new FileInputStream(publicKeyFile);
310        byte[] b = new byte[4096];
311        int read;
312        while ((read = input.read(b)) != -1) {
313            outputJar.write(b, 0, read);
314            md.update(b, 0, read);
315        }
316        input.close();
317
318        Attributes attr = new Attributes();
319        attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
320                      new String(Base64.encode(md.digest()), "ASCII"));
321        manifest.getEntries().put(OTACERT_NAME, attr);
322    }
323
324
325    /** Write to another stream and track how many bytes have been
326     *  written.
327     */
328    private static class CountOutputStream extends FilterOutputStream {
329        private int mCount;
330
331        public CountOutputStream(OutputStream out) {
332            super(out);
333            mCount = 0;
334        }
335
336        @Override
337        public void write(int b) throws IOException {
338            super.write(b);
339            mCount++;
340        }
341
342        @Override
343        public void write(byte[] b, int off, int len) throws IOException {
344            super.write(b, off, len);
345            mCount += len;
346        }
347
348        public int size() {
349            return mCount;
350        }
351    }
352
353    /** Write a .SF file with a digest of the specified manifest. */
354    private static void writeSignatureFile(Manifest manifest, OutputStream out,
355                                           int hash)
356        throws IOException, GeneralSecurityException {
357        Manifest sf = new Manifest();
358        Attributes main = sf.getMainAttributes();
359        main.putValue("Signature-Version", "1.0");
360        main.putValue("Created-By", "1.0 (Android SignApk)");
361
362        MessageDigest md = MessageDigest.getInstance(
363            hash == USE_SHA256 ? "SHA256" : "SHA1");
364        PrintStream print = new PrintStream(
365            new DigestOutputStream(new ByteArrayOutputStream(), md),
366            true, "UTF-8");
367
368        // Digest of the entire manifest
369        manifest.write(print);
370        print.flush();
371        main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
372                      new String(Base64.encode(md.digest()), "ASCII"));
373
374        Map<String, Attributes> entries = manifest.getEntries();
375        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
376            // Digest of the manifest stanza for this entry.
377            print.print("Name: " + entry.getKey() + "\r\n");
378            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
379                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
380            }
381            print.print("\r\n");
382            print.flush();
383
384            Attributes sfAttr = new Attributes();
385            sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
386                            new String(Base64.encode(md.digest()), "ASCII"));
387            sf.getEntries().put(entry.getKey(), sfAttr);
388        }
389
390        CountOutputStream cout = new CountOutputStream(out);
391        sf.write(cout);
392
393        // A bug in the java.util.jar implementation of Android platforms
394        // up to version 1.6 will cause a spurious IOException to be thrown
395        // if the length of the signature file is a multiple of 1024 bytes.
396        // As a workaround, add an extra CRLF in this case.
397        if ((cout.size() % 1024) == 0) {
398            cout.write('\r');
399            cout.write('\n');
400        }
401    }
402
403    /** Sign data and write the digital signature to 'out'. */
404    private static void writeSignatureBlock(
405        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
406        OutputStream out)
407        throws IOException,
408               CertificateEncodingException,
409               OperatorCreationException,
410               CMSException {
411        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
412        certList.add(publicKey);
413        JcaCertStore certs = new JcaCertStore(certList);
414
415        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
416        ContentSigner signer = new JcaContentSignerBuilder(
417            getAlgorithm(publicKey) == USE_SHA256 ? "SHA256withRSA" : "SHA1withRSA")
418            .setProvider(sBouncyCastleProvider)
419            .build(privateKey);
420        gen.addSignerInfoGenerator(
421            new JcaSignerInfoGeneratorBuilder(
422                new JcaDigestCalculatorProviderBuilder()
423                .setProvider(sBouncyCastleProvider)
424                .build())
425            .setDirectSignature(true)
426            .build(signer, publicKey));
427        gen.addCertificates(certs);
428        CMSSignedData sigData = gen.generate(data, false);
429
430        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
431        DEROutputStream dos = new DEROutputStream(out);
432        dos.writeObject(asn1.readObject());
433    }
434
435    /**
436     * Copy all the files in a manifest from input to output.  We set
437     * the modification times in the output to a fixed time, so as to
438     * reduce variation in the output file and make incremental OTAs
439     * more efficient.
440     */
441    private static void copyFiles(Manifest manifest,
442                                  JarFile in, JarOutputStream out, long timestamp) throws IOException {
443        byte[] buffer = new byte[4096];
444        int num;
445
446        Map<String, Attributes> entries = manifest.getEntries();
447        ArrayList<String> names = new ArrayList<String>(entries.keySet());
448        Collections.sort(names);
449        for (String name : names) {
450            JarEntry inEntry = in.getJarEntry(name);
451            JarEntry outEntry = null;
452            if (inEntry.getMethod() == JarEntry.STORED) {
453                // Preserve the STORED method of the input entry.
454                outEntry = new JarEntry(inEntry);
455            } else {
456                // Create a new entry so that the compressed len is recomputed.
457                outEntry = new JarEntry(name);
458            }
459            outEntry.setTime(timestamp);
460            out.putNextEntry(outEntry);
461
462            InputStream data = in.getInputStream(inEntry);
463            while ((num = data.read(buffer)) > 0) {
464                out.write(buffer, 0, num);
465            }
466            out.flush();
467        }
468    }
469
470    private static class WholeFileSignerOutputStream extends FilterOutputStream {
471        private boolean closing = false;
472        private ByteArrayOutputStream footer = new ByteArrayOutputStream();
473        private OutputStream tee;
474
475        public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
476            super(out);
477            this.tee = tee;
478        }
479
480        public void notifyClosing() {
481            closing = true;
482        }
483
484        public void finish() throws IOException {
485            closing = false;
486
487            byte[] data = footer.toByteArray();
488            if (data.length < 2)
489                throw new IOException("Less than two bytes written to footer");
490            write(data, 0, data.length - 2);
491        }
492
493        public byte[] getTail() {
494            return footer.toByteArray();
495        }
496
497        @Override
498        public void write(byte[] b) throws IOException {
499            write(b, 0, b.length);
500        }
501
502        @Override
503        public void write(byte[] b, int off, int len) throws IOException {
504            if (closing) {
505                // if the jar is about to close, save the footer that will be written
506                footer.write(b, off, len);
507            }
508            else {
509                // write to both output streams. out is the CMSTypedData signer and tee is the file.
510                out.write(b, off, len);
511                tee.write(b, off, len);
512            }
513        }
514
515        @Override
516        public void write(int b) throws IOException {
517            if (closing) {
518                // if the jar is about to close, save the footer that will be written
519                footer.write(b);
520            }
521            else {
522                // write to both output streams. out is the CMSTypedData signer and tee is the file.
523                out.write(b);
524                tee.write(b);
525            }
526        }
527    }
528
529    private static class CMSSigner implements CMSTypedData {
530        private JarFile inputJar;
531        private File publicKeyFile;
532        private X509Certificate publicKey;
533        private PrivateKey privateKey;
534        private String outputFile;
535        private OutputStream outputStream;
536        private final ASN1ObjectIdentifier type;
537        private WholeFileSignerOutputStream signer;
538
539        public CMSSigner(JarFile inputJar, File publicKeyFile,
540                         X509Certificate publicKey, PrivateKey privateKey,
541                         OutputStream outputStream) {
542            this.inputJar = inputJar;
543            this.publicKeyFile = publicKeyFile;
544            this.publicKey = publicKey;
545            this.privateKey = privateKey;
546            this.outputStream = outputStream;
547            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
548        }
549
550        public Object getContent() {
551            throw new UnsupportedOperationException();
552        }
553
554        public ASN1ObjectIdentifier getContentType() {
555            return type;
556        }
557
558        public void write(OutputStream out) throws IOException {
559            try {
560                signer = new WholeFileSignerOutputStream(out, outputStream);
561                JarOutputStream outputJar = new JarOutputStream(signer);
562
563                int hash = getAlgorithm(publicKey);
564
565                // Assume the certificate is valid for at least an hour.
566                long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
567
568                Manifest manifest = addDigestsToManifest(inputJar, hash);
569                copyFiles(manifest, inputJar, outputJar, timestamp);
570                addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
571
572                signFile(manifest, inputJar,
573                         new X509Certificate[]{ publicKey },
574                         new PrivateKey[]{ privateKey },
575                         outputJar);
576
577                signer.notifyClosing();
578                outputJar.close();
579                signer.finish();
580            }
581            catch (Exception e) {
582                throw new IOException(e);
583            }
584        }
585
586        public void writeSignatureBlock(ByteArrayOutputStream temp)
587            throws IOException,
588                   CertificateEncodingException,
589                   OperatorCreationException,
590                   CMSException {
591            SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
592        }
593
594        public WholeFileSignerOutputStream getSigner() {
595            return signer;
596        }
597    }
598
599    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
600                                      X509Certificate publicKey, PrivateKey privateKey,
601                                      OutputStream outputStream) throws Exception {
602        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
603                                         publicKey, privateKey, outputStream);
604
605        ByteArrayOutputStream temp = new ByteArrayOutputStream();
606
607        // put a readable message and a null char at the start of the
608        // archive comment, so that tools that display the comment
609        // (hopefully) show something sensible.
610        // TODO: anything more useful we can put in this message?
611        byte[] message = "signed by SignApk".getBytes("UTF-8");
612        temp.write(message);
613        temp.write(0);
614
615        cmsOut.writeSignatureBlock(temp);
616
617        byte[] zipData = cmsOut.getSigner().getTail();
618
619        // For a zip with no archive comment, the
620        // end-of-central-directory record will be 22 bytes long, so
621        // we expect to find the EOCD marker 22 bytes from the end.
622        if (zipData[zipData.length-22] != 0x50 ||
623            zipData[zipData.length-21] != 0x4b ||
624            zipData[zipData.length-20] != 0x05 ||
625            zipData[zipData.length-19] != 0x06) {
626            throw new IllegalArgumentException("zip data already has an archive comment");
627        }
628
629        int total_size = temp.size() + 6;
630        if (total_size > 0xffff) {
631            throw new IllegalArgumentException("signature is too big for ZIP file comment");
632        }
633        // signature starts this many bytes from the end of the file
634        int signature_start = total_size - message.length - 1;
635        temp.write(signature_start & 0xff);
636        temp.write((signature_start >> 8) & 0xff);
637        // Why the 0xff bytes?  In a zip file with no archive comment,
638        // bytes [-6:-2] of the file are the little-endian offset from
639        // the start of the file to the central directory.  So for the
640        // two high bytes to be 0xff 0xff, the archive would have to
641        // be nearly 4GB in size.  So it's unlikely that a real
642        // commentless archive would have 0xffs here, and lets us tell
643        // an old signed archive from a new one.
644        temp.write(0xff);
645        temp.write(0xff);
646        temp.write(total_size & 0xff);
647        temp.write((total_size >> 8) & 0xff);
648        temp.flush();
649
650        // Signature verification checks that the EOCD header is the
651        // last such sequence in the file (to avoid minzip finding a
652        // fake EOCD appended after the signature in its scan).  The
653        // odds of producing this sequence by chance are very low, but
654        // let's catch it here if it does.
655        byte[] b = temp.toByteArray();
656        for (int i = 0; i < b.length-3; ++i) {
657            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
658                throw new IllegalArgumentException("found spurious EOCD header at " + i);
659            }
660        }
661
662        outputStream.write(total_size & 0xff);
663        outputStream.write((total_size >> 8) & 0xff);
664        temp.writeTo(outputStream);
665    }
666
667    private static void signFile(Manifest manifest, JarFile inputJar,
668                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
669                                 JarOutputStream outputJar)
670        throws Exception {
671        // Assume the certificate is valid for at least an hour.
672        long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
673
674        // MANIFEST.MF
675        JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
676        je.setTime(timestamp);
677        outputJar.putNextEntry(je);
678        manifest.write(outputJar);
679
680        int numKeys = publicKey.length;
681        for (int k = 0; k < numKeys; ++k) {
682            // CERT.SF / CERT#.SF
683            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
684                              (String.format(CERT_SF_MULTI_NAME, k)));
685            je.setTime(timestamp);
686            outputJar.putNextEntry(je);
687            ByteArrayOutputStream baos = new ByteArrayOutputStream();
688            writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));
689            byte[] signedData = baos.toByteArray();
690            outputJar.write(signedData);
691
692            // CERT.RSA / CERT#.RSA
693            je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
694                              (String.format(CERT_RSA_MULTI_NAME, k)));
695            je.setTime(timestamp);
696            outputJar.putNextEntry(je);
697            writeSignatureBlock(new CMSProcessableByteArray(signedData),
698                                publicKey[k], privateKey[k], outputJar);
699        }
700    }
701
702    private static void usage() {
703        System.err.println("Usage: signapk [-w] " +
704                           "publickey.x509[.pem] privatekey.pk8 " +
705                           "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
706                           "input.jar output.jar");
707        System.exit(2);
708    }
709
710    public static void main(String[] args) {
711        if (args.length < 4) usage();
712
713        sBouncyCastleProvider = new BouncyCastleProvider();
714        Security.addProvider(sBouncyCastleProvider);
715
716        boolean signWholeFile = false;
717        int argstart = 0;
718        if (args[0].equals("-w")) {
719            signWholeFile = true;
720            argstart = 1;
721        }
722
723        if ((args.length - argstart) % 2 == 1) usage();
724        int numKeys = ((args.length - argstart) / 2) - 1;
725        if (signWholeFile && numKeys > 1) {
726            System.err.println("Only one key may be used with -w.");
727            System.exit(2);
728        }
729
730        String inputFilename = args[args.length-2];
731        String outputFilename = args[args.length-1];
732
733        JarFile inputJar = null;
734        FileOutputStream outputFile = null;
735        int hashes = 0;
736
737        try {
738            File firstPublicKeyFile = new File(args[argstart+0]);
739
740            X509Certificate[] publicKey = new X509Certificate[numKeys];
741            try {
742                for (int i = 0; i < numKeys; ++i) {
743                    int argNum = argstart + i*2;
744                    publicKey[i] = readPublicKey(new File(args[argNum]));
745                    hashes |= getAlgorithm(publicKey[i]);
746                }
747            } catch (IllegalArgumentException e) {
748                System.err.println(e);
749                System.exit(1);
750            }
751
752            // Set the ZIP file timestamp to the starting valid time
753            // of the 0th certificate plus one hour (to match what
754            // we've historically done).
755            long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
756
757            PrivateKey[] privateKey = new PrivateKey[numKeys];
758            for (int i = 0; i < numKeys; ++i) {
759                int argNum = argstart + i*2 + 1;
760                privateKey[i] = readPrivateKey(new File(args[argNum]));
761            }
762            inputJar = new JarFile(new File(inputFilename), false);  // Don't verify.
763
764            outputFile = new FileOutputStream(outputFilename);
765
766
767            if (signWholeFile) {
768                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
769                                      publicKey[0], privateKey[0], outputFile);
770            } else {
771                JarOutputStream outputJar = new JarOutputStream(outputFile);
772
773                // For signing .apks, use the maximum compression to make
774                // them as small as possible (since they live forever on
775                // the system partition).  For OTA packages, use the
776                // default compression level, which is much much faster
777                // and produces output that is only a tiny bit larger
778                // (~0.1% on full OTA packages I tested).
779                outputJar.setLevel(9);
780
781                Manifest manifest = addDigestsToManifest(inputJar, hashes);
782                copyFiles(manifest, inputJar, outputJar, timestamp);
783                signFile(manifest, inputJar, publicKey, privateKey, outputJar);
784                outputJar.close();
785            }
786        } catch (Exception e) {
787            e.printStackTrace();
788            System.exit(1);
789        } finally {
790            try {
791                if (inputJar != null) inputJar.close();
792                if (outputFile != null) outputFile.close();
793            } catch (IOException e) {
794                e.printStackTrace();
795                System.exit(1);
796            }
797        }
798    }
799}
800