SignApk: update SignApk to support SHA256withRSA signing

Change-Id: I849b05addba69b74c9b0b65de5b24dd233e57d7c
This commit is contained in:
Doug Zongker 2013-04-10 09:19:32 -07:00
parent 78045e66d7
commit c0581a02ed
1 changed files with 102 additions and 33 deletions

View File

@ -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<String, Attributes> 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) {