blob: 9f4cccef8344a50a17e123b2c42c58eb24b8e846 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksigner.core.internal.apk.v1;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignatureEncryptionAlgorithmFinder;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder;
import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import com.android.apksigner.core.internal.jar.ManifestWriter;
import com.android.apksigner.core.internal.jar.SignatureFileWriter;
import com.android.apksigner.core.internal.util.Pair;
/**
* APK signer which uses JAR signing (aka v1 signing scheme).
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
*/
public abstract class V1SchemeSigner {
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
new Attributes.Name("Created-By");
private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)";
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
new Attributes.Name("X-Android-APK-Signed");
/**
* Signer configuration.
*/
public static class SignerConfig {
/** Name. */
public String name;
/** Private key. */
public PrivateKey privateKey;
/**
* Certificates, with the first certificate containing the public key corresponding to
* {@link #privateKey}.
*/
public List<X509Certificate> certificates;
/**
* Digest algorithm used for the signature.
*/
public DigestAlgorithm signatureDigestAlgorithm;
/**
* Digest algorithm used for digests of JAR entries and MANIFEST.MF.
*/
public DigestAlgorithm contentDigestAlgorithm;
}
/** Hidden constructor to prevent instantiation. */
private V1SchemeSigner() {}
/**
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
* AndroidManifest.xml minSdkVersion attribute)
*
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
* JAR signing (aka v1 signature scheme)
*/
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Prior to API Level 18, only SHA-1 can be used with RSA.
if (minSdkVersion < 18) {
return DigestAlgorithm.SHA1;
}
return DigestAlgorithm.SHA256;
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// Prior to API Level 21, only SHA-1 can be used with DSA
if (minSdkVersion < 21) {
return DigestAlgorithm.SHA1;
} else {
return DigestAlgorithm.SHA256;
}
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
if (minSdkVersion < 18) {
throw new InvalidKeyException(
"ECDSA signatures only supported for minSdkVersion 18 and higher");
}
// Prior to API Level 21, only SHA-1 can be used with ECDSA
if (minSdkVersion < 21) {
return DigestAlgorithm.SHA1;
} else {
return DigestAlgorithm.SHA256;
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
/**
* Returns the JAR signing digest algorithm to be used for JAR entry digests.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
* AndroidManifest.xml minSdkVersion attribute)
*/
public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) {
return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1;
}
/**
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
*/
public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) {
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
try {
return MessageDigest.getInstance(jcaAlgorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e);
}
}
/**
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
* algorithm.
*/
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
return digestAlgorithm.getJcaMessageDigestAlgorithm();
}
/**
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
* manifest.
*/
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
// Entries outside of META-INF must be listed in the manifest.
if (!entryName.startsWith("META-INF/")) {
return true;
}
// Entries in subdirectories of META-INF must be listed in the manifest.
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
return true;
}
// Ignored file names (case-insensitive) in META-INF directory:
// MANIFEST.MF
// *.SF
// *.RSA
// *.DSA
// *.EC
// SIG-*
String fileNameLowerCase =
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
if (("manifest.mf".equals(fileNameLowerCase))
|| (fileNameLowerCase.endsWith(".sf"))
|| (fileNameLowerCase.endsWith(".rsa"))
|| (fileNameLowerCase.endsWith(".dsa"))
|| (fileNameLowerCase.endsWith(".ec"))
|| (fileNameLowerCase.startsWith("sig-"))) {
return false;
}
return true;
}
/**
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
* JAR entries which need to be added to the APK as part of the signature.
*
* @param signerConfigs signer configurations, one for each signer. At least one signer config
* must be provided.
*
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
* cannot be used in general
* @throws SignatureException if an error occurs when computing digests of generating
* signatures
*/
public static List<Pair<String, byte[]>> sign(
List<SignerConfig> signerConfigs,
DigestAlgorithm jarEntryDigestAlgorithm,
Map<String, byte[]> jarEntryDigests,
List<Integer> apkSigningSchemeIds,
byte[] sourceManifestBytes)
throws InvalidKeyException, CertificateEncodingException, SignatureException {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
OutputManifestFile manifest =
generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest);
}
/**
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
* JAR entries which need to be added to the APK as part of the signature.
*
* @param signerConfigs signer configurations, one for each signer. At least one signer config
* must be provided.
*
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
* cannot be used in general
* @throws SignatureException if an error occurs when computing digests of generating
* signatures
*/
public static List<Pair<String, byte[]>> signManifest(
List<SignerConfig> signerConfigs,
DigestAlgorithm digestAlgorithm,
List<Integer> apkSigningSchemeIds,
OutputManifestFile manifest)
throws InvalidKeyException, CertificateEncodingException, SignatureException {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
List<Pair<String, byte[]>> signatureJarEntries =
new ArrayList<>(2 * signerConfigs.size() + 1);
byte[] sfBytes =
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest);
for (SignerConfig signerConfig : signerConfigs) {
String signerName = signerConfig.name;
byte[] signatureBlock;
try {
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
} catch (InvalidKeyException e) {
throw new InvalidKeyException(
"Failed to sign using signer \"" + signerName + "\"", e);
} catch (CertificateEncodingException e) {
throw new CertificateEncodingException(
"Failed to sign using signer \"" + signerName + "\"", e);
} catch (SignatureException e) {
throw new SignatureException(
"Failed to sign using signer \"" + signerName + "\"", e);
}
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
String signatureBlockFileName =
"META-INF/" + signerName + "."
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
signatureJarEntries.add(
Pair.of(signatureBlockFileName, signatureBlock));
}
signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
return signatureJarEntries;
}
/**
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
*/
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
for (SignerConfig signerConfig : signerConfigs) {
String signerName = signerConfig.name;
result.add("META-INF/" + signerName + ".SF");
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
String signatureBlockFileName =
"META-INF/" + signerName + "."
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
result.add(signatureBlockFileName);
}
result.add(MANIFEST_ENTRY_NAME);
return result;
}
/**
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
*/
public static OutputManifestFile generateManifestFile(
DigestAlgorithm jarEntryDigestAlgorithm,
Map<String, byte[]> jarEntryDigests,
byte[] sourceManifestBytes) {
Manifest sourceManifest = null;
if (sourceManifestBytes != null) {
try {
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
} catch (IOException e) {
throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e);
}
}
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
Attributes mainAttrs = new Attributes();
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
if (sourceManifest != null) {
mainAttrs.putAll(sourceManifest.getMainAttributes());
} else {
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
}
try {
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
}
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
Collections.sort(sortedEntryNames);
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
for (String entryName : sortedEntryNames) {
byte[] entryDigest = jarEntryDigests.get(entryName);
Attributes entryAttrs = new Attributes();
entryAttrs.putValue(
entryDigestAttributeName,
Base64.getEncoder().encodeToString(entryDigest));
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
byte[] sectionBytes;
try {
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
sectionBytes = sectionOut.toByteArray();
manifestOut.write(sectionBytes);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
}
invidualSectionsContents.put(entryName, sectionBytes);
}
OutputManifestFile result = new OutputManifestFile();
result.contents = manifestOut.toByteArray();
result.mainSectionAttributes = mainAttrs;
result.individualSectionsContents = invidualSectionsContents;
return result;
}
public static class OutputManifestFile {
public byte[] contents;
public SortedMap<String, byte[]> individualSectionsContents;
public Attributes mainSectionAttributes;
}
private static byte[] generateSignatureFile(
List<Integer> apkSignatureSchemeIds,
DigestAlgorithm manifestDigestAlgorithm,
OutputManifestFile manifest) {
Manifest sf = new Manifest();
Attributes mainAttrs = sf.getMainAttributes();
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY);
if (!apkSignatureSchemeIds.isEmpty()) {
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
// This attribute indicates that this APK is supposed to have been signed using one or
// more APK-specific signature schemes in addition to the standard JAR signature scheme
// used by this code. APK signature verifier should reject the APK if it does not
// contain a signature for the signature scheme the verifier prefers out of this set.
StringBuilder attrValue = new StringBuilder();
for (int id : apkSignatureSchemeIds) {
if (attrValue.length() > 0) {
attrValue.append(", ");
}
attrValue.append(String.valueOf(id));
}
mainAttrs.put(
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
attrValue.toString());
}
// Add main attribute containing the digest of MANIFEST.MF.
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
mainAttrs.putValue(
getManifestDigestAttributeName(manifestDigestAlgorithm),
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
SignatureFileWriter.writeMainSection(out, mainAttrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory .SF file", e);
}
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
for (Map.Entry<String, byte[]> manifestSection
: manifest.individualSectionsContents.entrySet()) {
String sectionName = manifestSection.getKey();
byte[] sectionContents = manifestSection.getValue();
byte[] sectionDigest = md.digest(sectionContents);
Attributes attrs = new Attributes();
attrs.putValue(
entryDigestAttributeName,
Base64.getEncoder().encodeToString(sectionDigest));
try {
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory .SF file", e);
}
}
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
// cause a spurious IOException to be thrown if the length of the signature file is a
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
try {
SignatureFileWriter.writeSectionDelimiter(out);
} catch (IOException e) {
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
}
}
return out.toByteArray();
}
private static byte[] generateSignatureBlock(
SignerConfig signerConfig, byte[] signatureFileBytes)
throws InvalidKeyException, CertificateEncodingException, SignatureException {
JcaCertStore certs = new JcaCertStore(signerConfig.certificates);
X509Certificate signerCert = signerConfig.certificates.get(0);
String jcaSignatureAlgorithm =
getJcaSignatureAlgorithm(
signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm);
try {
ContentSigner signer =
new JcaContentSignerBuilder(jcaSignatureAlgorithm)
.build(signerConfig.privateKey);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addSignerInfoGenerator(
new SignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build(),
SignerInfoSignatureAlgorithmFinder.INSTANCE)
.setDirectSignature(true)
.build(signer, new JcaX509CertificateHolder(signerCert)));
gen.addCertificates(certs);
CMSSignedData sigData =
gen.generate(new CMSProcessableByteArray(signatureFileBytes), false);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
DEROutputStream dos = new DEROutputStream(out);
dos.writeObject(asn1.readObject());
}
return out.toByteArray();
} catch (OperatorCreationException | CMSException | IOException e) {
throw new SignatureException("Failed to generate signature", e);
}
}
/**
* Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo.
*/
private static class SignerInfoSignatureAlgorithmFinder
implements CMSSignatureEncryptionAlgorithmFinder {
private static final SignerInfoSignatureAlgorithmFinder INSTANCE =
new SignerInfoSignatureAlgorithmFinder();
private static final AlgorithmIdentifier DSA =
new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, DERNull.INSTANCE);
private final CMSSignatureEncryptionAlgorithmFinder mDefault =
new DefaultCMSSignatureEncryptionAlgorithmFinder();
@Override
public AlgorithmIdentifier findEncryptionAlgorithm(AlgorithmIdentifier id) {
// Use the default chooser, but replace dsaWithSha1 with dsa. This is because "dsa" is
// accepted by any Android platform whereas "dsaWithSha1" is accepted only since
// API Level 9.
id = mDefault.findEncryptionAlgorithm(id);
if (id != null) {
ASN1ObjectIdentifier oid = id.getAlgorithm();
if (X9ObjectIdentifiers.id_dsa_with_sha1.equals(oid)) {
return DSA;
}
}
return id;
}
}
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return "SHA1-Digest";
case SHA256:
return "SHA-256-Digest";
default:
throw new IllegalArgumentException(
"Unexpected content digest algorithm: " + digestAlgorithm);
}
}
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return "SHA1-Digest-Manifest";
case SHA256:
return "SHA-256-Digest-Manifest";
default:
throw new IllegalArgumentException(
"Unexpected content digest algorithm: " + digestAlgorithm);
}
}
private static String getJcaSignatureAlgorithm(
PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
String digestPrefixForSigAlg;
switch (digestAlgorithm) {
case SHA1:
digestPrefixForSigAlg = "SHA1";
break;
case SHA256:
digestPrefixForSigAlg = "SHA256";
break;
default:
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
return digestPrefixForSigAlg + "withRSA";
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
return digestPrefixForSigAlg + "withDSA";
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return digestPrefixForSigAlg + "withECDSA";
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
}