Home | History | Annotate | Download | only in signing
      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 
     17 package com.android.builder.signing;
     18 
     19 import com.android.builder.signing.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
     20 
     21 import sun.misc.BASE64Encoder;
     22 import sun.security.pkcs.ContentInfo;
     23 import sun.security.pkcs.PKCS7;
     24 import sun.security.pkcs.SignerInfo;
     25 import sun.security.x509.AlgorithmId;
     26 import sun.security.x509.X500Name;
     27 
     28 import java.io.ByteArrayOutputStream;
     29 import java.io.File;
     30 import java.io.FileInputStream;
     31 import java.io.FilterOutputStream;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.io.OutputStream;
     35 import java.io.PrintStream;
     36 import java.security.DigestOutputStream;
     37 import java.security.GeneralSecurityException;
     38 import java.security.MessageDigest;
     39 import java.security.NoSuchAlgorithmException;
     40 import java.security.PrivateKey;
     41 import java.security.Signature;
     42 import java.security.SignatureException;
     43 import java.security.cert.X509Certificate;
     44 import java.util.Map;
     45 import java.util.jar.Attributes;
     46 import java.util.jar.JarEntry;
     47 import java.util.jar.JarFile;
     48 import java.util.jar.JarOutputStream;
     49 import java.util.jar.Manifest;
     50 import java.util.zip.ZipEntry;
     51 import java.util.zip.ZipInputStream;
     52 
     53 /**
     54  * A Jar file builder with signature support.
     55  */
     56 public class SignedJarBuilder {
     57     private static final String DIGEST_ALGORITHM = "SHA1";
     58     private static final String DIGEST_ATTR = "SHA1-Digest";
     59     private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest";
     60 
     61     /** Write to another stream and also feed it to the Signature object. */
     62     private static class SignatureOutputStream extends FilterOutputStream {
     63         private Signature mSignature;
     64         private int mCount = 0;
     65 
     66         public SignatureOutputStream(OutputStream out, Signature sig) {
     67             super(out);
     68             mSignature = sig;
     69         }
     70 
     71         @Override
     72         public void write(int b) throws IOException {
     73             try {
     74                 mSignature.update((byte) b);
     75             } catch (SignatureException e) {
     76                 throw new IOException("SignatureException: " + e);
     77             }
     78             super.write(b);
     79             mCount++;
     80         }
     81 
     82         @Override
     83         public void write(byte[] b, int off, int len) throws IOException {
     84             try {
     85                 mSignature.update(b, off, len);
     86             } catch (SignatureException e) {
     87                 throw new IOException("SignatureException: " + e);
     88             }
     89             super.write(b, off, len);
     90             mCount += len;
     91         }
     92 
     93         public int size() {
     94             return mCount;
     95         }
     96     }
     97 
     98     private JarOutputStream mOutputJar;
     99     private PrivateKey mKey;
    100     private X509Certificate mCertificate;
    101     private Manifest mManifest;
    102     private BASE64Encoder mBase64Encoder;
    103     private MessageDigest mMessageDigest;
    104 
    105     private byte[] mBuffer = new byte[4096];
    106 
    107     /**
    108      * Classes which implement this interface provides a method to check whether a file should
    109      * be added to a Jar file.
    110      */
    111     public interface IZipEntryFilter {
    112 
    113         /**
    114          * An exception thrown during packaging of a zip file into APK file.
    115          * This is typically thrown by implementations of
    116          * {@link IZipEntryFilter#checkEntry(String)}.
    117          */
    118         public static class ZipAbortException extends Exception {
    119             private static final long serialVersionUID = 1L;
    120 
    121             public ZipAbortException() {
    122                 super();
    123             }
    124 
    125             public ZipAbortException(String format, Object... args) {
    126                 super(String.format(format, args));
    127             }
    128 
    129             public ZipAbortException(Throwable cause, String format, Object... args) {
    130                 super(String.format(format, args), cause);
    131             }
    132 
    133             public ZipAbortException(Throwable cause) {
    134                 super(cause);
    135             }
    136         }
    137 
    138 
    139         /**
    140          * Checks a file for inclusion in a Jar archive.
    141          * @param archivePath the archive file path of the entry
    142          * @return <code>true</code> if the file should be included.
    143          * @throws ZipAbortException if writing the file should be aborted.
    144          */
    145         public boolean checkEntry(String archivePath) throws ZipAbortException;
    146     }
    147 
    148     /**
    149      * Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
    150      * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then
    151      * the archive will not be signed.
    152      * @param out the {@link OutputStream} where to write the Jar archive.
    153      * @param key the {@link PrivateKey} used to sign the archive, or <code>null</code>.
    154      * @param certificate the {@link X509Certificate} used to sign the archive, or
    155      * <code>null</code>.
    156      * @throws IOException
    157      * @throws NoSuchAlgorithmException
    158      */
    159     public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
    160             throws IOException, NoSuchAlgorithmException {
    161         mOutputJar = new JarOutputStream(out);
    162         mOutputJar.setLevel(9);
    163         mKey = key;
    164         mCertificate = certificate;
    165 
    166         if (mKey != null && mCertificate != null) {
    167             mManifest = new Manifest();
    168             Attributes main = mManifest.getMainAttributes();
    169             main.putValue("Manifest-Version", "1.0");
    170             main.putValue("Created-By", "1.0 (Android)");
    171 
    172             mBase64Encoder = new BASE64Encoder();
    173             mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
    174         }
    175     }
    176 
    177     /**
    178      * Writes a new {@link File} into the archive.
    179      * @param inputFile the {@link File} to write.
    180      * @param jarPath the filepath inside the archive.
    181      * @throws IOException
    182      */
    183     public void writeFile(File inputFile, String jarPath) throws IOException {
    184         // Get an input stream on the file.
    185         FileInputStream fis = new FileInputStream(inputFile);
    186         try {
    187 
    188             // create the zip entry
    189             JarEntry entry = new JarEntry(jarPath);
    190             entry.setTime(inputFile.lastModified());
    191 
    192             writeEntry(fis, entry);
    193         } finally {
    194             // close the file stream used to read the file
    195             fis.close();
    196         }
    197     }
    198 
    199     /**
    200      * Copies the content of a Jar/Zip archive into the receiver archive.
    201      * <p/>An optional {@link IZipEntryFilter} allows to selectively choose which files
    202      * to copy over.
    203      * @param input the {@link InputStream} for the Jar/Zip to copy.
    204      * @param filter the filter or <code>null</code>
    205      * @throws IOException
    206      * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write
    207      *                           must be aborted.
    208      */
    209     public void writeZip(InputStream input, IZipEntryFilter filter)
    210             throws IOException, ZipAbortException {
    211         ZipInputStream zis = new ZipInputStream(input);
    212 
    213         try {
    214             // loop on the entries of the intermediary package and put them in the final package.
    215             ZipEntry entry;
    216             while ((entry = zis.getNextEntry()) != null) {
    217                 String name = entry.getName();
    218 
    219                 // do not take directories or anything inside a potential META-INF folder.
    220                 if (entry.isDirectory() || name.startsWith("META-INF/")) {
    221                     continue;
    222                 }
    223 
    224                 // if we have a filter, we check the entry against it
    225                 if (filter != null && filter.checkEntry(name) == false) {
    226                     continue;
    227                 }
    228 
    229                 JarEntry newEntry;
    230 
    231                 // Preserve the STORED method of the input entry.
    232                 if (entry.getMethod() == JarEntry.STORED) {
    233                     newEntry = new JarEntry(entry);
    234                 } else {
    235                     // Create a new entry so that the compressed len is recomputed.
    236                     newEntry = new JarEntry(name);
    237                 }
    238 
    239                 writeEntry(zis, newEntry);
    240 
    241                 zis.closeEntry();
    242             }
    243         } finally {
    244             zis.close();
    245         }
    246     }
    247 
    248     /**
    249      * Closes the Jar archive by creating the manifest, and signing the archive.
    250      * @throws IOException
    251      * @throws GeneralSecurityException
    252      */
    253     public void close() throws IOException, GeneralSecurityException {
    254         if (mManifest != null) {
    255             // write the manifest to the jar file
    256             mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
    257             mManifest.write(mOutputJar);
    258 
    259             // CERT.SF
    260             Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm());
    261             signature.initSign(mKey);
    262             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
    263             writeSignatureFile(new SignatureOutputStream(mOutputJar, signature));
    264 
    265             // CERT.*
    266             mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
    267             writeSignatureBlock(signature, mCertificate, mKey);
    268         }
    269 
    270         mOutputJar.close();
    271         mOutputJar = null;
    272     }
    273 
    274     /**
    275      * Clean up of the builder for interrupted workflow.
    276      * This does nothing if {@link #close()} was called successfully.
    277      */
    278     public void cleanUp() {
    279         if (mOutputJar != null) {
    280             try {
    281                 mOutputJar.close();
    282             } catch (IOException e) {
    283                 // pass
    284             }
    285         }
    286     }
    287 
    288     /**
    289      * Adds an entry to the output jar, and write its content from the {@link InputStream}
    290      * @param input The input stream from where to write the entry content.
    291      * @param entry the entry to write in the jar.
    292      * @throws IOException
    293      */
    294     private void writeEntry(InputStream input, JarEntry entry) throws IOException {
    295         // add the entry to the jar archive
    296         mOutputJar.putNextEntry(entry);
    297 
    298         // read the content of the entry from the input stream, and write it into the archive.
    299         int count;
    300         while ((count = input.read(mBuffer)) != -1) {
    301             mOutputJar.write(mBuffer, 0, count);
    302 
    303             // update the digest
    304             if (mMessageDigest != null) {
    305                 mMessageDigest.update(mBuffer, 0, count);
    306             }
    307         }
    308 
    309         // close the entry for this file
    310         mOutputJar.closeEntry();
    311 
    312         if (mManifest != null) {
    313             // update the manifest for this entry.
    314             Attributes attr = mManifest.getAttributes(entry.getName());
    315             if (attr == null) {
    316                 attr = new Attributes();
    317                 mManifest.getEntries().put(entry.getName(), attr);
    318             }
    319             attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
    320         }
    321     }
    322 
    323     /** Writes a .SF file with a digest to the manifest. */
    324     private void writeSignatureFile(SignatureOutputStream out)
    325             throws IOException, GeneralSecurityException {
    326         Manifest sf = new Manifest();
    327         Attributes main = sf.getMainAttributes();
    328         main.putValue("Signature-Version", "1.0");
    329         main.putValue("Created-By", "1.0 (Android)");
    330 
    331         BASE64Encoder base64 = new BASE64Encoder();
    332         MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
    333         PrintStream print = new PrintStream(
    334                 new DigestOutputStream(new ByteArrayOutputStream(), md),
    335                 true, "UTF-8");
    336 
    337         // Digest of the entire manifest
    338         mManifest.write(print);
    339         print.flush();
    340         main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
    341 
    342         Map<String, Attributes> entries = mManifest.getEntries();
    343         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
    344             // Digest of the manifest stanza for this entry.
    345             print.print("Name: " + entry.getKey() + "\r\n");
    346             for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
    347                 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
    348             }
    349             print.print("\r\n");
    350             print.flush();
    351 
    352             Attributes sfAttr = new Attributes();
    353             sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
    354             sf.getEntries().put(entry.getKey(), sfAttr);
    355         }
    356 
    357         sf.write(out);
    358 
    359         // A bug in the java.util.jar implementation of Android platforms
    360         // up to version 1.6 will cause a spurious IOException to be thrown
    361         // if the length of the signature file is a multiple of 1024 bytes.
    362         // As a workaround, add an extra CRLF in this case.
    363         if ((out.size() % 1024) == 0) {
    364             out.write('\r');
    365             out.write('\n');
    366         }
    367     }
    368 
    369     /** Write the certificate file with a digital signature. */
    370     private void writeSignatureBlock(Signature signature, X509Certificate publicKey,
    371             PrivateKey privateKey)
    372             throws IOException, GeneralSecurityException {
    373         SignerInfo signerInfo = new SignerInfo(
    374                 new X500Name(publicKey.getIssuerX500Principal().getName()),
    375                 publicKey.getSerialNumber(),
    376                 AlgorithmId.get(DIGEST_ALGORITHM),
    377                 AlgorithmId.get(privateKey.getAlgorithm()),
    378                 signature.sign());
    379 
    380         PKCS7 pkcs7 = new PKCS7(
    381                 new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) },
    382                 new ContentInfo(ContentInfo.DATA_OID, null),
    383                 new X509Certificate[] { publicKey },
    384                 new SignerInfo[] { signerInfo });
    385 
    386         pkcs7.encodeSignedData(mOutputJar);
    387     }
    388 }
    389