OSDN Git Service

change SignApk.java to use bouncy castle for signing
authorDoug Zongker <dougz@google.com>
Tue, 4 Sep 2012 20:32:13 +0000 (13:32 -0700)
committerBrian Carlstrom <bdc@google.com>
Wed, 19 Sep 2012 06:29:10 +0000 (23:29 -0700)
Remove use of the private sun.security.* classes for generating pkcs7
signatures and use bouncy castle instead.

Change-Id: Ie8213575461975085d119e000e764d2a28c26715

tools/signapk/Android.mk
tools/signapk/SignApk.java

index b2de21c..620ccb1 100644 (file)
@@ -21,6 +21,7 @@ include $(CLEAR_VARS)
 LOCAL_MODULE := signapk
 LOCAL_SRC_FILES := SignApk.java
 LOCAL_JAR_MANIFEST := SignApk.mf
+LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host bouncycastle-bcpkix-host
 include $(BUILD_HOST_JAVA_LIBRARY)
 
 ifeq ($(TARGET_BUILD_APPS),)
index cb19296..07aefa7 100644 (file)
 
 package com.android.signapk;
 
-import sun.misc.BASE64Encoder;
-import sun.security.pkcs.ContentInfo;
-import sun.security.pkcs.PKCS7;
-import sun.security.pkcs.SignerInfo;
-import sun.security.x509.AlgorithmId;
-import sun.security.x509.X500Name;
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.DEROutputStream;
+import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
+import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.CMSTypedData;
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+import org.bouncycastle.util.encoders.Base64;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -35,16 +46,15 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
-import java.security.AlgorithmParameters;
 import java.security.DigestOutputStream;
 import java.security.GeneralSecurityException;
 import java.security.Key;
 import java.security.KeyFactory;
 import java.security.MessageDigest;
 import java.security.PrivateKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.Certificate;
+import java.security.Provider;
+import java.security.Security;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
@@ -52,9 +62,7 @@ import java.security.spec.KeySpec;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Enumeration;
-import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.jar.Attributes;
@@ -78,6 +86,8 @@ class SignApk {
 
     private static final String OTACERT_NAME = "META-INF/com/android/otacert";
 
+    private static Provider sBouncyCastleProvider;
+
     // Files matching this pattern are not copied to the output.
     private static Pattern stripPattern =
             Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
@@ -181,7 +191,6 @@ class SignApk {
             main.putValue("Created-By", "1.0 (Android SignApk)");
         }
 
-        BASE64Encoder base64 = new BASE64Encoder();
         MessageDigest md = MessageDigest.getInstance("SHA1");
         byte[] buffer = new byte[4096];
         int num;
@@ -212,7 +221,8 @@ class SignApk {
                 Attributes attr = null;
                 if (input != null) attr = input.getAttributes(name);
                 attr = attr != null ? new Attributes(attr) : new Attributes();
-                attr.putValue("SHA1-Digest", base64.encode(md.digest()));
+                attr.putValue("SHA1-Digest",
+                              new String(Base64.encode(md.digest()), "ASCII"));
                 output.getEntries().put(name, attr);
             }
         }
@@ -232,7 +242,6 @@ class SignApk {
                                    long timestamp,
                                    Manifest manifest)
         throws IOException, GeneralSecurityException {
-        BASE64Encoder base64 = new BASE64Encoder();
         MessageDigest md = MessageDigest.getInstance("SHA1");
 
         JarEntry je = new JarEntry(OTACERT_NAME);
@@ -248,40 +257,31 @@ class SignApk {
         input.close();
 
         Attributes attr = new Attributes();
-        attr.putValue("SHA1-Digest", base64.encode(md.digest()));
+        attr.putValue("SHA1-Digest",
+                      new String(Base64.encode(md.digest()), "ASCII"));
         manifest.getEntries().put(OTACERT_NAME, attr);
     }
 
 
-    /** Write to another stream and also feed it to the Signature object. */
-    private static class SignatureOutputStream extends FilterOutputStream {
-        private Signature mSignature;
+    /** Write to another stream and track how many bytes have been
+     *  written.
+     */
+    private static class CountOutputStream extends FilterOutputStream {
         private int mCount;
 
-        public SignatureOutputStream(OutputStream out, Signature sig) {
+        public CountOutputStream(OutputStream out) {
             super(out);
-            mSignature = sig;
             mCount = 0;
         }
 
         @Override
         public void write(int b) throws IOException {
-            try {
-                mSignature.update((byte) b);
-            } catch (SignatureException e) {
-                throw new IOException("SignatureException: " + e);
-            }
             super.write(b);
             mCount++;
         }
 
         @Override
         public void write(byte[] b, int off, int len) throws IOException {
-            try {
-                mSignature.update(b, off, len);
-            } catch (SignatureException e) {
-                throw new IOException("SignatureException: " + e);
-            }
             super.write(b, off, len);
             mCount += len;
         }
@@ -292,14 +292,13 @@ class SignApk {
     }
 
     /** Write a .SF file with a digest of the specified manifest. */
-    private static void writeSignatureFile(Manifest manifest, SignatureOutputStream out)
-            throws IOException, GeneralSecurityException {
+    private static void writeSignatureFile(Manifest manifest, OutputStream out)
+        throws IOException, GeneralSecurityException {
         Manifest sf = new Manifest();
         Attributes main = sf.getMainAttributes();
         main.putValue("Signature-Version", "1.0");
         main.putValue("Created-By", "1.0 (Android SignApk)");
 
-        BASE64Encoder base64 = new BASE64Encoder();
         MessageDigest md = MessageDigest.getInstance("SHA1");
         PrintStream print = new PrintStream(
                 new DigestOutputStream(new ByteArrayOutputStream(), md),
@@ -308,7 +307,8 @@ class SignApk {
         // Digest of the entire manifest
         manifest.write(print);
         print.flush();
-        main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
+        main.putValue("SHA1-Digest-Manifest",
+                      new String(Base64.encode(md.digest()), "ASCII"));
 
         Map<String, Attributes> entries = manifest.getEntries();
         for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
@@ -321,48 +321,88 @@ class SignApk {
             print.flush();
 
             Attributes sfAttr = new Attributes();
-            sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
+            sfAttr.putValue("SHA1-Digest",
+                            new String(Base64.encode(md.digest()), "ASCII"));
             sf.getEntries().put(entry.getKey(), sfAttr);
         }
 
-        sf.write(out);
+        CountOutputStream cout = new CountOutputStream(out);
+        sf.write(cout);
 
         // A bug in the java.util.jar implementation of Android platforms
         // up to version 1.6 will cause a spurious IOException to be thrown
         // if the length of the signature file is a multiple of 1024 bytes.
         // As a workaround, add an extra CRLF in this case.
-        if ((out.size() % 1024) == 0) {
-            out.write('\r');
-            out.write('\n');
+        if ((cout.size() % 1024) == 0) {
+            cout.write('\r');
+            cout.write('\n');
         }
     }
 
-    /** Write a .RSA file with a digital signature. */
+    private static class CMSByteArraySlice implements CMSTypedData {
+        private final ASN1ObjectIdentifier type;
+        private final byte[] data;
+        private final int offset;
+        private final int length;
+        public CMSByteArraySlice(byte[] data, int offset, int length) {
+            this.data = data;
+            this.offset = offset;
+            this.length = length;
+            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
+        }
+
+        public Object getContent() {
+            throw new UnsupportedOperationException();
+        }
+
+        public ASN1ObjectIdentifier getContentType() {
+            return type;
+        }
+
+        public void write(OutputStream out) throws IOException {
+            out.write(data, offset, length);
+        }
+    }
+
+    /** Sign data and write the digital signature to 'out'. */
     private static void writeSignatureBlock(
-            Signature signature, X509Certificate publicKey, OutputStream out)
-            throws IOException, GeneralSecurityException {
-        SignerInfo signerInfo = new SignerInfo(
-                new X500Name(publicKey.getIssuerX500Principal().getName()),
-                publicKey.getSerialNumber(),
-                AlgorithmId.get("SHA1"),
-                AlgorithmId.get("RSA"),
-                signature.sign());
-
-        PKCS7 pkcs7 = new PKCS7(
-                new AlgorithmId[] { AlgorithmId.get("SHA1") },
-                new ContentInfo(ContentInfo.DATA_OID, null),
-                new X509Certificate[] { publicKey },
-                new SignerInfo[] { signerInfo });
-
-        pkcs7.encodeSignedData(out);
+        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
+        OutputStream out)
+        throws IOException,
+               CertificateEncodingException,
+               OperatorCreationException,
+               CMSException {
+        ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
+        certList.add(publicKey);
+        JcaCertStore certs = new JcaCertStore(certList);
+
+        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
+        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
+            .setProvider(sBouncyCastleProvider)
+            .build(privateKey);
+        gen.addSignerInfoGenerator(
+            new JcaSignerInfoGeneratorBuilder(
+                new JcaDigestCalculatorProviderBuilder()
+                .setProvider(sBouncyCastleProvider)
+                .build())
+            .setDirectSignature(true)
+            .build(sha1Signer, publicKey));
+        gen.addCertificates(certs);
+        CMSSignedData sigData = gen.generate(data, false);
+
+        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
+        DEROutputStream dos = new DEROutputStream(out);
+        dos.writeObject(asn1.readObject());
     }
 
     private static void signWholeOutputFile(byte[] zipData,
                                             OutputStream outputStream,
                                             X509Certificate publicKey,
                                             PrivateKey privateKey)
-        throws IOException, GeneralSecurityException {
-
+        throws IOException,
+               CertificateEncodingException,
+               OperatorCreationException,
+               CMSException {
         // For a zip with no archive comment, the
         // end-of-central-directory record will be 22 bytes long, so
         // we expect to find the EOCD marker 22 bytes from the end.
@@ -373,10 +413,6 @@ class SignApk {
             throw new IllegalArgumentException("zip data already has an archive comment");
         }
 
-        Signature signature = Signature.getInstance("SHA1withRSA");
-        signature.initSign(privateKey);
-        signature.update(zipData, 0, zipData.length-2);
-
         ByteArrayOutputStream temp = new ByteArrayOutputStream();
 
         // put a readable message and a null char at the start of the
@@ -386,7 +422,9 @@ class SignApk {
         byte[] message = "signed by SignApk".getBytes("UTF-8");
         temp.write(message);
         temp.write(0);
-        writeSignatureBlock(signature, publicKey, temp);
+
+        writeSignatureBlock(new CMSByteArraySlice(zipData, 0, zipData.length-2),
+                            publicKey, privateKey, temp);
         int total_size = temp.size() + 6;
         if (total_size > 0xffff) {
             throw new IllegalArgumentException("signature is too big for ZIP file comment");
@@ -399,7 +437,7 @@ class SignApk {
         // bytes [-6:-2] of the file are the little-endian offset from
         // the start of the file to the central directory.  So for the
         // two high bytes to be 0xff 0xff, the archive would have to
-        // be nearly 4GB in side.  So it's unlikely that a real
+        // be nearly 4GB in size.  So it's unlikely that a real
         // commentless archive would have 0xffs here, and lets us tell
         // an old signed archive from a new one.
         temp.write(0xff);
@@ -438,7 +476,7 @@ class SignApk {
         int num;
 
         Map<String, Attributes> entries = manifest.getEntries();
-        List<String> names = new ArrayList(entries.keySet());
+        ArrayList<String> names = new ArrayList<String>(entries.keySet());
         Collections.sort(names);
         for (String name : names) {
             JarEntry inEntry = in.getJarEntry(name);
@@ -469,6 +507,9 @@ class SignApk {
             System.exit(2);
         }
 
+        sBouncyCastleProvider = new BouncyCastleProvider();
+        Security.addProvider(sBouncyCastleProvider);
+
         boolean signWholeFile = false;
         int argstart = 0;
         if (args[0].equals("-w")) {
@@ -527,19 +568,20 @@ class SignApk {
             manifest.write(outputJar);
 
             // CERT.SF
-            Signature signature = Signature.getInstance("SHA1withRSA");
-            signature.initSign(privateKey);
             je = new JarEntry(CERT_SF_NAME);
             je.setTime(timestamp);
             outputJar.putNextEntry(je);
-            writeSignatureFile(manifest,
-                    new SignatureOutputStream(outputJar, signature));
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            writeSignatureFile(manifest, baos);
+            byte[] signedData = baos.toByteArray();
+            outputJar.write(signedData);
 
             // CERT.RSA
             je = new JarEntry(CERT_RSA_NAME);
             je.setTime(timestamp);
             outputJar.putNextEntry(je);
-            writeSignatureBlock(signature, publicKey, outputJar);
+            writeSignatureBlock(new CMSProcessableByteArray(signedData),
+                                publicKey, privateKey, outputJar);
 
             outputJar.close();
             outputJar = null;