Loading tools/signapk/SignApk.java +102 −33 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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))|(" + Loading Loading @@ -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(); Loading @@ -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; Loading @@ -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(); if (md_sha1 != null) { attr.putValue("SHA1-Digest", new String(Base64.encode(md.digest()), "ASCII")); 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); } } Loading @@ -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); Loading @@ -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); } Loading Loading @@ -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"); Loading @@ -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<String, Attributes> entries = manifest.getEntries(); Loading @@ -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); } Loading Loading @@ -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( Loading @@ -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); Loading Loading @@ -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(); Loading Loading @@ -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); Loading @@ -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); Loading Loading @@ -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]; 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 Loading Loading @@ -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) { Loading Loading
tools/signapk/SignApk.java +102 −33 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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))|(" + Loading Loading @@ -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(); Loading @@ -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; Loading @@ -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(); if (md_sha1 != null) { attr.putValue("SHA1-Digest", new String(Base64.encode(md.digest()), "ASCII")); 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); } } Loading @@ -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); Loading @@ -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); } Loading Loading @@ -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"); Loading @@ -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<String, Attributes> entries = manifest.getEntries(); Loading @@ -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); } Loading Loading @@ -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( Loading @@ -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); Loading Loading @@ -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(); Loading Loading @@ -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); Loading @@ -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); Loading Loading @@ -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]; 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 Loading Loading @@ -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) { Loading