SignApk: update SignApk to support SHA256withRSA signing
(cherry picked from commit c0581a02ed
)
Change-Id: I6212e0f3a6aa3265b111f0f7348b667af4a668b0
This commit is contained in:
parent
b71677b17c
commit
8562fd478d
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue