diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:28:42 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:28:42 -0800 |
commit | 88b607994a148f4af5bffee163e39ce8296750c6 (patch) | |
tree | fa249ff843e976cf034f2029437d3362a8396321 /tools/signapk | |
parent | 05806d7af62e07c6225b2e7103a1b115ecf6c9ad (diff) | |
download | build-88b607994a148f4af5bffee163e39ce8296750c6.zip build-88b607994a148f4af5bffee163e39ce8296750c6.tar.gz build-88b607994a148f4af5bffee163e39ce8296750c6.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'tools/signapk')
-rw-r--r-- | tools/signapk/Android.mk | 27 | ||||
-rw-r--r-- | tools/signapk/SignApk.java | 390 | ||||
-rw-r--r-- | tools/signapk/SignApk.mf | 1 | ||||
-rwxr-xr-x | tools/signapk/test/run | 30 |
4 files changed, 448 insertions, 0 deletions
diff --git a/tools/signapk/Android.mk b/tools/signapk/Android.mk new file mode 100644 index 0000000..ccc76fd --- /dev/null +++ b/tools/signapk/Android.mk @@ -0,0 +1,27 @@ +# +# Copyright (C) 2008 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +LOCAL_PATH := $(call my-dir) + +# the signapk tool (a .jar application used to sign packages) +# ============================================================ +include $(CLEAR_VARS) +LOCAL_MODULE := signapk +LOCAL_SRC_FILES := SignApk.java +LOCAL_JAR_MANIFEST := SignApk.mf +include $(BUILD_HOST_JAVA_LIBRARY) + +# The post-build signing tools need signapk.jar. +$(call dist-for-goals,droid,$(LOCAL_INSTALLED_MODULE)) diff --git a/tools/signapk/SignApk.java b/tools/signapk/SignApk.java new file mode 100644 index 0000000..340a9f5 --- /dev/null +++ b/tools/signapk/SignApk.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +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.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +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; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** + * Command line tool to sign JAR files (including APKs and OTA updates) in + * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. + */ +class SignApk { + private static final String CERT_SF_NAME = "META-INF/CERT.SF"; + private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; + + private static X509Certificate readPublicKey(File file) + throws IOException, GeneralSecurityException { + FileInputStream input = new FileInputStream(file); + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(input); + } finally { + input.close(); + } + } + + /** + * Reads the password from stdin and returns it as a string. + * + * @param keyFile The file containing the private key. Used to prompt the user. + */ + private static String readPassword(File keyFile) { + // TODO: use Console.readPassword() when it's available. + System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); + System.out.flush(); + BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); + try { + return stdin.readLine(); + } catch (IOException ex) { + return null; + } + } + + /** + * Decrypt an encrypted PKCS 8 format private key. + * + * Based on ghstark's post on Aug 6, 2006 at + * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 + * + * @param encryptedPrivateKey The raw data of the private key + * @param keyFile The file containing the private key + */ + private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) + throws GeneralSecurityException { + EncryptedPrivateKeyInfo epkInfo; + try { + epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); + } catch (IOException ex) { + // Probably not an encrypted key. + return null; + } + + char[] password = readPassword(keyFile).toCharArray(); + + SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); + Key key = skFactory.generateSecret(new PBEKeySpec(password)); + + Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); + + try { + return epkInfo.getKeySpec(cipher); + } catch (InvalidKeySpecException ex) { + System.err.println("signapk: Password for " + keyFile + " may be bad."); + throw ex; + } + } + + /** Read a PKCS 8 format private key. */ + private static PrivateKey readPrivateKey(File file) + throws IOException, GeneralSecurityException { + DataInputStream input = new DataInputStream(new FileInputStream(file)); + try { + byte[] bytes = new byte[(int) file.length()]; + input.read(bytes); + + KeySpec spec = decryptPrivateKey(bytes, file); + if (spec == null) { + spec = new PKCS8EncodedKeySpec(bytes); + } + + try { + return KeyFactory.getInstance("RSA").generatePrivate(spec); + } catch (InvalidKeySpecException ex) { + return KeyFactory.getInstance("DSA").generatePrivate(spec); + } + } finally { + input.close(); + } + } + + /** Add the SHA1 of every file to the manifest, creating it if necessary. */ + private static Manifest addDigestsToManifest(JarFile jar) + throws IOException, GeneralSecurityException { + Manifest input = jar.getManifest(); + Manifest output = new Manifest(); + Attributes main = output.getMainAttributes(); + if (input != null) { + main.putAll(input.getMainAttributes()); + } else { + main.putValue("Manifest-Version", "1.0"); + main.putValue("Created-By", "1.0 (Android SignApk)"); + } + + BASE64Encoder base64 = new BASE64Encoder(); + MessageDigest md = MessageDigest.getInstance("SHA1"); + byte[] buffer = new byte[4096]; + int num; + + // We sort the input entries by name, and add them to the + // output manifest in sorted order. We expect that the output + // map will be deterministic. + + TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); + + for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) { + JarEntry entry = e.nextElement(); + byName.put(entry.getName(), entry); + } + + for (JarEntry entry: byName.values()) { + String name = entry.getName(); + if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && + !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME)) { + InputStream data = jar.getInputStream(entry); + while ((num = data.read(buffer)) > 0) { + md.update(buffer, 0, num); + } + + 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())); + output.getEntries().put(name, attr); + } + } + + return output; + } + + /** Write to another stream and also feed it to the Signature object. */ + private static class SignatureOutputStream extends FilterOutputStream { + private Signature mSignature; + + public SignatureOutputStream(OutputStream out, Signature sig) { + super(out); + mSignature = sig; + } + + @Override + public void write(int b) throws IOException { + try { + mSignature.update((byte) b); + } catch (SignatureException e) { + throw new IOException("SignatureException: " + e); + } + super.write(b); + } + + @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); + } + } + + /** Write a .SF file with a digest the specified manifest. */ + 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), + true, "UTF-8"); + + // Digest of the entire manifest + manifest.write(print); + print.flush(); + main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest())); + + Map<String, Attributes> entries = manifest.getEntries(); + for (Map.Entry<String, Attributes> entry : entries.entrySet()) { + // Digest of the manifest stanza for this entry. + print.print("Name: " + entry.getKey() + "\r\n"); + for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { + print.print(att.getKey() + ": " + att.getValue() + "\r\n"); + } + print.print("\r\n"); + print.flush(); + + Attributes sfAttr = new Attributes(); + sfAttr.putValue("SHA1-Digest", base64.encode(md.digest())); + sf.getEntries().put(entry.getKey(), sfAttr); + } + + sf.write(out); + } + + /** Write a .RSA file with a digital signature. */ + 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); + } + + /** Copy all the files in a manifest from input to output. */ + private static void copyFiles(Manifest manifest, + JarFile in, JarOutputStream out) throws IOException { + byte[] buffer = new byte[4096]; + int num; + + Map<String, Attributes> entries = manifest.getEntries(); + List<String> names = new ArrayList(entries.keySet()); + Collections.sort(names); + for (String name : names) { + JarEntry inEntry = in.getJarEntry(name); + if (inEntry.getMethod() == JarEntry.STORED) { + // Preserve the STORED method of the input entry. + out.putNextEntry(new JarEntry(inEntry)); + } else { + // Create a new entry so that the compressed len is recomputed. + JarEntry je = new JarEntry(name); + je.setTime(inEntry.getTime()); + out.putNextEntry(je); + } + + InputStream data = in.getInputStream(inEntry); + while ((num = data.read(buffer)) > 0) { + out.write(buffer, 0, num); + } + out.flush(); + } + } + + public static void main(String[] args) { + if (args.length != 4) { + System.err.println("Usage: signapk " + + "publickey.x509[.pem] privatekey.pk8 " + + "input.jar output.jar"); + System.exit(2); + } + + JarFile inputJar = null; + JarOutputStream outputJar = null; + + try { + X509Certificate publicKey = readPublicKey(new File(args[0])); + + // Assume the certificate is valid for at least an hour. + long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; + + PrivateKey privateKey = readPrivateKey(new File(args[1])); + inputJar = new JarFile(new File(args[2]), false); // Don't verify. + outputJar = new JarOutputStream(new FileOutputStream(args[3])); + outputJar.setLevel(9); + + JarEntry je; + + // MANIFEST.MF + Manifest manifest = addDigestsToManifest(inputJar); + je = new JarEntry(JarFile.MANIFEST_NAME); + je.setTime(timestamp); + outputJar.putNextEntry(je); + 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)); + + // CERT.RSA + je = new JarEntry(CERT_RSA_NAME); + je.setTime(timestamp); + outputJar.putNextEntry(je); + writeSignatureBlock(signature, publicKey, outputJar); + + // Everything else + copyFiles(manifest, inputJar, outputJar); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } finally { + try { + if (inputJar != null) inputJar.close(); + if (outputJar != null) outputJar.close(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + } +} diff --git a/tools/signapk/SignApk.mf b/tools/signapk/SignApk.mf new file mode 100644 index 0000000..2c72e59 --- /dev/null +++ b/tools/signapk/SignApk.mf @@ -0,0 +1 @@ +Main-Class: com.android.signapk.SignApk diff --git a/tools/signapk/test/run b/tools/signapk/test/run new file mode 100755 index 0000000..4e24625 --- /dev/null +++ b/tools/signapk/test/run @@ -0,0 +1,30 @@ +#!/usr/bin/make -f + +package := NotePad.apk + +all: out/signed-$(package) + +clean: + rm -rf out + +.PHONY: FORCE + +DSAPARAM := out/dsaparam +$(DSAPARAM): + mkdir -p $(dir $@) + umask 0077 && openssl dsaparam -out $@ 1024 + +%.pem: $(DSAPARAM) FORCE + mkdir -p $(dir $@) + umask 0077 && openssl gendsa -out $@.pk~ $(DSAPARAM) + umask 0077 && openssl pkcs8 -topk8 -nocrypt \ + -in $@.pk~ -out $@.pk + umask 0077 && openssl req -new -x509 -key $@.pk -out $@ -days 1095 \ + -subj "/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com" + +cert := out/key1.pem +out/signed-$(package): $(package) $(cert) + mkdir -p $(dir $@) + SIGNAPK_DEBUG=1 \ + signapk -input $< -output $@ \ + -key $(cert).pk -cert $(cert) -tempdir out |