private static final int MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES = 18;
/**
- * Return one of USE_SHA1 or USE_SHA256 according to the signature
- * algorithm specified in the cert.
+ * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
+ * for v1 signing (using JAR Signature Scheme) an APK using the private key corresponding to the
+ * provided certificate.
+ *
+ * @param minSdkVersion minimum Android platform API Level supported by the APK (see
+ * minSdkVersion attribute in AndroidManifest.xml). The higher the minSdkVersion, the
+ * stronger hash may be used for signing the APK.
*/
- private static int getDigestAlgorithm(X509Certificate cert, int minSdkVersion) {
+ private static int getV1DigestAlgorithmForApk(X509Certificate cert, int minSdkVersion) {
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
// see "HISTORICAL NOTE" above.
}
}
- /** Returns the expected signature algorithm for this key type. */
- private static String getSignatureAlgorithm(X509Certificate cert, int minSdkVersion) {
- String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
- if ("RSA".equalsIgnoreCase(keyType)) {
- if ((minSdkVersion >= MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES)
- || (getDigestAlgorithm(cert, minSdkVersion) == USE_SHA256)) {
- return "SHA256withRSA";
- } else {
- return "SHA1withRSA";
- }
- } else if ("EC".equalsIgnoreCase(keyType)) {
- return "SHA256withECDSA";
+ /**
+ * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used
+ * for signing an OTA update package using the private key corresponding to the provided
+ * certificate.
+ */
+ private static int getDigestAlgorithmForOta(X509Certificate cert) {
+ String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
+ if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
+ // see "HISTORICAL NOTE" above.
+ return USE_SHA1;
+ } else if (sigAlg.startsWith("SHA256WITH")) {
+ return USE_SHA256;
} else {
- throw new IllegalArgumentException("unsupported key type: " + keyType);
+ throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
+ "\" in cert [" + cert.getSubjectDN());
+ }
+ }
+
+ /**
+ * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA
+ * or v1 signing an APK using the private key corresponding to the provided certificate and the
+ * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants).
+ */
+ private static String getJcaSignatureAlgorithmForV1SigningOrOta(
+ X509Certificate cert, int hash) {
+ String sigAlgDigestPrefix;
+ switch (hash) {
+ case USE_SHA1:
+ sigAlgDigestPrefix = "SHA1";
+ break;
+ case USE_SHA256:
+ sigAlgDigestPrefix = "SHA256";
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown hash ID: " + hash);
+ }
+
+ String keyAlgorithm = cert.getPublicKey().getAlgorithm();
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ return sigAlgDigestPrefix + "withRSA";
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ return sigAlgDigestPrefix + "withECDSA";
+ } else {
+ throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
}
}
/** Sign data and write the digital signature to 'out'. */
private static void writeSignatureBlock(
- CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int minSdkVersion,
+ CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash,
OutputStream out)
throws IOException,
CertificateEncodingException,
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer =
- new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey, minSdkVersion))
+ new JcaContentSignerBuilder(
+ getJcaSignatureAlgorithmForV1SigningOrOta(publicKey, hash))
.build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
private final File publicKeyFile;
private final X509Certificate publicKey;
private final PrivateKey privateKey;
+ private final int hash;
private final long timestamp;
- private final int minSdkVersion;
private final OutputStream outputStream;
private final ASN1ObjectIdentifier type;
private WholeFileSignerOutputStream signer;
public CMSSigner(JarFile inputJar, File publicKeyFile,
- X509Certificate publicKey, PrivateKey privateKey, long timestamp,
- int minSdkVersion, OutputStream outputStream) {
+ X509Certificate publicKey, PrivateKey privateKey, int hash,
+ long timestamp, OutputStream outputStream) {
this.inputJar = inputJar;
this.publicKeyFile = publicKeyFile;
this.publicKey = publicKey;
this.privateKey = privateKey;
+ this.hash = hash;
this.timestamp = timestamp;
- this.minSdkVersion = minSdkVersion;
this.outputStream = outputStream;
this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
}
signer = new WholeFileSignerOutputStream(out, outputStream);
JarOutputStream outputJar = new JarOutputStream(signer);
- int hash = getDigestAlgorithm(publicKey, minSdkVersion);
-
Manifest manifest = addDigestsToManifest(inputJar, hash);
copyFiles(manifest, inputJar, outputJar, timestamp, 0);
addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
signFile(manifest,
new X509Certificate[]{ publicKey },
new PrivateKey[]{ privateKey },
+ new int[] { hash },
timestamp,
- minSdkVersion,
false, // Don't sign using APK Signature Scheme v2
outputJar);
CertificateEncodingException,
OperatorCreationException,
CMSException {
- SignApk.writeSignatureBlock(this, publicKey, privateKey, minSdkVersion, temp);
+ SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp);
}
public WholeFileSignerOutputStream getSigner() {
private static void signWholeFile(JarFile inputJar, File publicKeyFile,
X509Certificate publicKey, PrivateKey privateKey,
- long timestamp, int minSdkVersion,
+ int hash, long timestamp,
OutputStream outputStream) throws Exception {
CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
- publicKey, privateKey, timestamp, minSdkVersion, outputStream);
+ publicKey, privateKey, hash, timestamp, outputStream);
ByteArrayOutputStream temp = new ByteArrayOutputStream();
}
private static void signFile(Manifest manifest,
- X509Certificate[] publicKey, PrivateKey[] privateKey,
+ X509Certificate[] publicKey, PrivateKey[] privateKey, int[] hash,
long timestamp,
- int minSdkVersion,
boolean additionallySignedUsingAnApkSignatureScheme,
JarOutputStream outputJar)
throws Exception {
writeSignatureFile(
manifest,
baos,
- getDigestAlgorithm(publicKey[k], minSdkVersion),
+ hash[k],
additionallySignedUsingAnApkSignatureScheme);
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
- publicKey[k], privateKey[k], minSdkVersion, outputJar);
+ publicKey[k], privateKey[k], hash[k], outputJar);
}
}
JarFile inputJar = null;
FileOutputStream outputFile = null;
- int hashes = 0;
try {
File firstPublicKeyFile = new File(args[argstart+0]);
for (int i = 0; i < numKeys; ++i) {
int argNum = argstart + i*2;
publicKey[i] = readPublicKey(new File(args[argNum]));
- hashes |= getDigestAlgorithm(publicKey[i], minSdkVersion);
}
} catch (IllegalArgumentException e) {
System.err.println(e);
// NOTE: Signing currently recompresses any compressed entries using Deflate (default
// compression level for OTA update files and maximum compession level for APKs).
if (signWholeFile) {
- SignApk.signWholeFile(inputJar, firstPublicKeyFile,
- publicKey[0], privateKey[0],
- timestamp, minSdkVersion,
- outputFile);
+ int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]);
+ signWholeFile(inputJar, firstPublicKeyFile,
+ publicKey[0], privateKey[0], digestAlgorithm,
+ timestamp,
+ outputFile);
} else {
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
// Use maximum compression for compressed entries because the APK lives forever on
// the system partition.
outputJar.setLevel(9);
- Manifest manifest = addDigestsToManifest(inputJar, hashes);
+ int v1DigestAlgorithmBitSet = 0;
+ int[] v1DigestAlgorithm = new int[numKeys];
+ for (int i = 0; i < numKeys; ++i) {
+ v1DigestAlgorithm[i] = getV1DigestAlgorithmForApk(publicKey[i], minSdkVersion);
+ v1DigestAlgorithmBitSet |= v1DigestAlgorithm[i];
+ }
+ Manifest manifest = addDigestsToManifest(inputJar, v1DigestAlgorithmBitSet);
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
signFile(
manifest,
- publicKey, privateKey,
- timestamp, minSdkVersion, signUsingApkSignatureSchemeV2,
+ publicKey, privateKey, v1DigestAlgorithm,
+ timestamp, signUsingApkSignatureSchemeV2,
outputJar);
outputJar.close();
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());