diff --git a/tools/signapk/SignApk.java b/tools/signapk/SignApk.java index adfe9a3d7..716ea3b74 100644 --- a/tools/signapk/SignApk.java +++ b/tools/signapk/SignApk.java @@ -78,8 +78,26 @@ 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. + * HISTORICAL NOTE: + * + * Prior to the keylimepie release, SignApk ignored the signature + * algorithm specified in the certificate and always used SHA1withRSA. + * + * Starting with keylimepie, we support SHA256withRSA, and use the + * signature algorithm in the certificate to select which to use + * (SHA256withRSA or SHA1withRSA). + * + * Because there are old keys still in use whose certificate actually + * says "MD5withRSA", we treat these as though they say "SHA1withRSA" + * for compatibility with older releases. This can be changed by + * altering the getAlgorithm() function below. + */ + + +/** + * Command line tool to sign JAR files (including APKs and OTA + * updates) in a way compatible with the mincrypt verifier, using RSA + * keys and SHA1 or SHA-256. */ class SignApk { private static final String CERT_SF_NAME = "META-INF/CERT.SF"; @@ -91,6 +109,27 @@ class SignApk { private static Provider sBouncyCastleProvider; + // bitmasks for which hash algorithms we need the manifest to include. + private static final int USE_SHA1 = 1; + private static final int USE_SHA256 = 2; + + /** + * Return one of USE_SHA1 or USE_SHA256 according to the signature + * algorithm specified in the cert. + */ + private static int getAlgorithm(X509Certificate cert) { + String sigAlg = cert.getSigAlgName(); + if ("SHA1withRSA".equals(sigAlg) || + "MD5withRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above. + return USE_SHA1; + } else if ("SHA256withRSA".equals(sigAlg)) { + return USE_SHA256; + } else { + throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + + "\" in cert [" + cert.getSubjectDN()); + } + } + // Files matching this pattern are not copied to the output. private static Pattern stripPattern = Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" + @@ -182,8 +221,11 @@ class SignApk { } } - /** Add the SHA1 of every file to the manifest, creating it if necessary. */ - private static Manifest addDigestsToManifest(JarFile jar) + /** + * Add the hash(es) of every file to the manifest, creating it if + * necessary. + */ + private static Manifest addDigestsToManifest(JarFile jar, int hashes) throws IOException, GeneralSecurityException { Manifest input = jar.getManifest(); Manifest output = new Manifest(); @@ -195,7 +237,15 @@ class SignApk { main.putValue("Created-By", "1.0 (Android SignApk)"); } - MessageDigest md = MessageDigest.getInstance("SHA1"); + MessageDigest md_sha1 = null; + MessageDigest md_sha256 = null; + if ((hashes & USE_SHA1) != 0) { + md_sha1 = MessageDigest.getInstance("SHA1"); + } + if ((hashes & USE_SHA256) != 0) { + md_sha256 = MessageDigest.getInstance("SHA256"); + } + byte[] buffer = new byte[4096]; int num; @@ -216,14 +266,21 @@ class SignApk { (stripPattern == null || !stripPattern.matcher(name).matches())) { InputStream data = jar.getInputStream(entry); while ((num = data.read(buffer)) > 0) { - md.update(buffer, 0, num); + if (md_sha1 != null) md_sha1.update(buffer, 0, num); + if (md_sha256 != null) md_sha256.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", - new String(Base64.encode(md.digest()), "ASCII")); + if (md_sha1 != null) { + attr.putValue("SHA1-Digest", + new String(Base64.encode(md_sha1.digest()), "ASCII")); + } + if (md_sha256 != null) { + attr.putValue("SHA-256-Digest", + new String(Base64.encode(md_sha256.digest()), "ASCII")); + } output.getEntries().put(name, attr); } } @@ -241,9 +298,10 @@ class SignApk { private static void addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp, - Manifest manifest) + Manifest manifest, + int hash) throws IOException, GeneralSecurityException { - MessageDigest md = MessageDigest.getInstance("SHA1"); + MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256"); JarEntry je = new JarEntry(OTACERT_NAME); je.setTime(timestamp); @@ -258,7 +316,7 @@ class SignApk { input.close(); Attributes attr = new Attributes(); - attr.putValue("SHA1-Digest", + attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest", new String(Base64.encode(md.digest()), "ASCII")); manifest.getEntries().put(OTACERT_NAME, attr); } @@ -293,14 +351,16 @@ class SignApk { } /** Write a .SF file with a digest of the specified manifest. */ - private static void writeSignatureFile(Manifest manifest, OutputStream out) + private static void writeSignatureFile(Manifest manifest, OutputStream out, + int hash) 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)"); - MessageDigest md = MessageDigest.getInstance("SHA1"); + MessageDigest md = MessageDigest.getInstance( + hash == USE_SHA256 ? "SHA256" : "SHA1"); PrintStream print = new PrintStream( new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8"); @@ -308,7 +368,7 @@ class SignApk { // Digest of the entire manifest manifest.write(print); print.flush(); - main.putValue("SHA1-Digest-Manifest", + main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", new String(Base64.encode(md.digest()), "ASCII")); Map entries = manifest.getEntries(); @@ -322,7 +382,7 @@ class SignApk { print.flush(); Attributes sfAttr = new Attributes(); - sfAttr.putValue("SHA1-Digest", + sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest", new String(Base64.encode(md.digest()), "ASCII")); sf.getEntries().put(entry.getKey(), sfAttr); } @@ -353,7 +413,8 @@ class SignApk { JcaCertStore certs = new JcaCertStore(certList); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA") + ContentSigner signer = new JcaContentSignerBuilder( + getAlgorithm(publicKey) == USE_SHA256 ? "SHA256withRSA" : "SHA1withRSA") .setProvider(sBouncyCastleProvider) .build(privateKey); gen.addSignerInfoGenerator( @@ -362,7 +423,7 @@ class SignApk { .setProvider(sBouncyCastleProvider) .build()) .setDirectSignature(true) - .build(sha1Signer, publicKey)); + .build(signer, publicKey)); gen.addCertificates(certs); CMSSignedData sigData = gen.generate(data, false); @@ -499,14 +560,19 @@ class SignApk { signer = new WholeFileSignerOutputStream(out, outputStream); JarOutputStream outputJar = new JarOutputStream(signer); - Manifest manifest = addDigestsToManifest(inputJar); + int hash = getAlgorithm(publicKey); + + // Assume the certificate is valid for at least an hour. + long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; + + Manifest manifest = addDigestsToManifest(inputJar, hash); + copyFiles(manifest, inputJar, outputJar, timestamp); + addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash); + signFile(manifest, inputJar, new X509Certificate[]{ publicKey }, new PrivateKey[]{ privateKey }, outputJar); - // Assume the certificate is valid for at least an hour. - long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; - addOtacert(outputJar, publicKeyFile, timestamp, manifest); signer.notifyClosing(); outputJar.close(); @@ -605,13 +671,8 @@ class SignApk { // Assume the certificate is valid for at least an hour. long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000; - JarEntry je; - - // Everything else - copyFiles(manifest, inputJar, outputJar, timestamp); - // MANIFEST.MF - je = new JarEntry(JarFile.MANIFEST_NAME); + JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); manifest.write(outputJar); @@ -624,7 +685,7 @@ class SignApk { je.setTime(timestamp); outputJar.putNextEntry(je); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeSignatureFile(manifest, baos); + writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k])); byte[] signedData = baos.toByteArray(); outputJar.write(signedData); @@ -671,14 +732,21 @@ class SignApk { JarFile inputJar = null; FileOutputStream outputFile = null; + int hashes = 0; try { File firstPublicKeyFile = new File(args[argstart+0]); X509Certificate[] publicKey = new X509Certificate[numKeys]; - for (int i = 0; i < numKeys; ++i) { - int argNum = argstart + i*2; - publicKey[i] = readPublicKey(new File(args[argNum])); + try { + for (int i = 0; i < numKeys; ++i) { + int argNum = argstart + i*2; + publicKey[i] = readPublicKey(new File(args[argNum])); + hashes |= getAlgorithm(publicKey[i]); + } + } catch (IllegalArgumentException e) { + System.err.println(e); + System.exit(1); } // Set the ZIP file timestamp to the starting valid time @@ -710,8 +778,9 @@ class SignApk { // (~0.1% on full OTA packages I tested). outputJar.setLevel(9); - signFile(addDigestsToManifest(inputJar), inputJar, - publicKey, privateKey, outputJar); + Manifest manifest = addDigestsToManifest(inputJar, hashes); + copyFiles(manifest, inputJar, outputJar, timestamp); + signFile(manifest, inputJar, publicKey, privateKey, outputJar); outputJar.close(); } } catch (Exception e) {