Don't depend on Bouncy Castle.
This switches PKCS#7 SignedData generation code from Bouncy Castle to OpenJDK's proprietary internal API. This is to avoid depending on a huge library that's not really needed. In the longer term, it's best to add our own implementation of PKCS#7 SignedData building, parsing, and verification. This will give the code more power to mimic what the Android platform does. Bug: 27461702 Change-Id: I29f1ceea1293e35424fde69c0f2969d551345100
This commit is contained in:
parent
dca955a9bd
commit
7f770c0963
3 changed files with 158 additions and 106 deletions
|
@ -20,7 +20,10 @@ LOCAL_PATH := $(call my-dir)
|
|||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := apksigner-core
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
LOCAL_JAVA_LIBRARIES = \
|
||||
bouncycastle-host \
|
||||
bouncycastle-bcpkix-host
|
||||
|
||||
# Disable warnnings about our use of internal proprietary OpenJDK API.
|
||||
# TODO: Remove this workaround by moving to our own implementation of PKCS #7
|
||||
# SignedData block generation, parsing, and verification.
|
||||
LOCAL_JAVACFLAGS := -XDignore.symbol.file
|
||||
|
||||
include $(BUILD_HOST_JAVA_LIBRARY)
|
||||
|
|
|
@ -33,7 +33,7 @@ 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.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -342,7 +342,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
|
|||
mOutputJarEntryDigests,
|
||||
apkSigningSchemeIds,
|
||||
inputJarManifest);
|
||||
} catch (CertificateEncodingException e) {
|
||||
} catch (CertificateException e) {
|
||||
throw new SignatureException("Failed to generate v1 signature", e);
|
||||
}
|
||||
} else {
|
||||
|
@ -360,7 +360,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
|
|||
mV1ContentDigestAlgorithm,
|
||||
apkSigningSchemeIds,
|
||||
newManifest);
|
||||
} catch (CertificateEncodingException e) {
|
||||
} catch (CertificateException e) {
|
||||
throw new SignatureException("Failed to generate v1 signature", e);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -24,8 +24,10 @@ import java.security.MessageDigest;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
|
@ -40,25 +42,11 @@ 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 sun.security.pkcs.ContentInfo;
|
||||
import sun.security.pkcs.PKCS7;
|
||||
import sun.security.pkcs.SignerInfo;
|
||||
import sun.security.x509.AlgorithmId;
|
||||
import sun.security.x509.X500Name;
|
||||
|
||||
import com.android.apksigner.core.internal.jar.ManifestWriter;
|
||||
import com.android.apksigner.core.internal.jar.SignatureFileWriter;
|
||||
|
@ -237,7 +225,7 @@ public abstract class V1SchemeSigner {
|
|||
Map<String, byte[]> jarEntryDigests,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
byte[] sourceManifestBytes)
|
||||
throws InvalidKeyException, CertificateEncodingException, SignatureException {
|
||||
throws InvalidKeyException, CertificateException, SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
|
@ -264,7 +252,7 @@ public abstract class V1SchemeSigner {
|
|||
DigestAlgorithm digestAlgorithm,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
OutputManifestFile manifest)
|
||||
throws InvalidKeyException, CertificateEncodingException, SignatureException {
|
||||
throws InvalidKeyException, CertificateException, SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
|
@ -282,8 +270,8 @@ public abstract class V1SchemeSigner {
|
|||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new CertificateEncodingException(
|
||||
} catch (CertificateException e) {
|
||||
throw new CertificateException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(
|
||||
|
@ -455,69 +443,156 @@ public abstract class V1SchemeSigner {
|
|||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@SuppressWarnings("restriction")
|
||||
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);
|
||||
throws InvalidKeyException, CertificateException, SignatureException {
|
||||
List<X509Certificate> signerCerts = signerConfig.certificates;
|
||||
X509Certificate signerCert = signerCerts.get(0);
|
||||
PublicKey signerPublicKey = signerCert.getPublicKey();
|
||||
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
||||
Pair<String, AlgorithmId> signatureAlgs =
|
||||
getSignerInfoSignatureAlgorithm(signerPublicKey, digestAlgorithm);
|
||||
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
||||
byte[] signatureBytes;
|
||||
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);
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initSign(signerConfig.privateKey);
|
||||
signature.update(signatureFileBytes);
|
||||
signatureBytes = signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SignatureException(
|
||||
jcaSignatureAlgorithm + " Signature implementation not found", e);
|
||||
}
|
||||
|
||||
CMSSignedData sigData =
|
||||
gen.generate(new CMSProcessableByteArray(signatureFileBytes), false);
|
||||
X500Name issuerName;
|
||||
try {
|
||||
issuerName = new X500Name(signerCert.getIssuerX500Principal().getName());
|
||||
} catch (IOException e) {
|
||||
throw new CertificateParsingException(
|
||||
"Failed to parse signer certificate issuer name", e);
|
||||
}
|
||||
|
||||
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);
|
||||
AlgorithmId digestAlgorithmId = getSignerInfoDigestAlgorithm(digestAlgorithm);
|
||||
SignerInfo signerInfo =
|
||||
new SignerInfo(
|
||||
issuerName,
|
||||
signerCert.getSerialNumber(),
|
||||
digestAlgorithmId,
|
||||
signatureAlgs.getSecond(),
|
||||
signatureBytes);
|
||||
PKCS7 pkcs7 =
|
||||
new PKCS7(
|
||||
new AlgorithmId[] {digestAlgorithmId},
|
||||
new ContentInfo(ContentInfo.DATA_OID, null),
|
||||
signerCerts.toArray(new X509Certificate[signerCerts.size()]),
|
||||
new SignerInfo[] {signerInfo});
|
||||
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
try {
|
||||
pkcs7.encodeSignedData(result);
|
||||
} catch (IOException e) {
|
||||
throw new SignatureException("Failed to encode PKCS#7 signed data", e);
|
||||
}
|
||||
return result.toByteArray();
|
||||
}
|
||||
|
||||
@SuppressWarnings("restriction")
|
||||
private static final AlgorithmId OID_DIGEST_SHA1 = getSupportedAlgorithmId("1.3.14.3.2.26");
|
||||
@SuppressWarnings("restriction")
|
||||
private static final AlgorithmId OID_DIGEST_SHA256 =
|
||||
getSupportedAlgorithmId("2.16.840.1.101.3.4.2.1");
|
||||
|
||||
/**
|
||||
* Returns the {@code SignerInfo} {@code DigestAlgorithm} to use for {@code SignerInfo} signing
|
||||
* using the specified digest algorithm.
|
||||
*/
|
||||
@SuppressWarnings("restriction")
|
||||
private static AlgorithmId getSignerInfoDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return OID_DIGEST_SHA1;
|
||||
case SHA256:
|
||||
return OID_DIGEST_SHA256;
|
||||
default:
|
||||
throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo.
|
||||
* Returns the JCA {@link Signature} algorithm and {@code SignerInfo} {@code SignatureAlgorithm}
|
||||
* to use for {@code SignerInfo} which signs with the specified key and digest algorithms.
|
||||
*/
|
||||
private static class SignerInfoSignatureAlgorithmFinder
|
||||
implements CMSSignatureEncryptionAlgorithmFinder {
|
||||
private static final SignerInfoSignatureAlgorithmFinder INSTANCE =
|
||||
new SignerInfoSignatureAlgorithmFinder();
|
||||
@SuppressWarnings("restriction")
|
||||
private static Pair<String, AlgorithmId> getSignerInfoSignatureAlgorithm(
|
||||
PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
|
||||
// NOTE: This method on purpose uses hard-coded OIDs instead of
|
||||
// Algorithm.getId(JCA Signature Algorithm). This is to ensure that the generated SignedData
|
||||
// is compatible with all targeted Android platforms and is not dependent on changes in the
|
||||
// JCA Signature Algorithm -> OID mappings maintained by AlgorithmId.get(String).
|
||||
|
||||
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;
|
||||
}
|
||||
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 Pair.of(
|
||||
digestPrefixForSigAlg + "withRSA",
|
||||
getSupportedAlgorithmId("1.2.840.113549.1.1.1") // RSA encryption
|
||||
);
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
AlgorithmId sigAlgId;
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10040.4.1"); // DSA
|
||||
break;
|
||||
case SHA256:
|
||||
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
|
||||
// 21 and higher. However, there are two ways to specify their SignedData
|
||||
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
|
||||
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
|
||||
// the former.
|
||||
sigAlgId = getSupportedAlgorithmId("2.16.840.1.101.3.4.3.2"); // DSA with SHA-256
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
return Pair.of(digestPrefixForSigAlg + "withDSA", sigAlgId);
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
AlgorithmId sigAlgId;
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10045.4.1"); // ECDSA with SHA-1
|
||||
break;
|
||||
case SHA256:
|
||||
sigAlgId = getSupportedAlgorithmId("1.2.840.10045.4.3.2"); // ECDSA with SHA-256
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
return Pair.of(digestPrefixForSigAlg + "withECDSA", sigAlgId);
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
@SuppressWarnings("restriction")
|
||||
private static AlgorithmId getSupportedAlgorithmId(String oid) {
|
||||
try {
|
||||
return AlgorithmId.get(oid);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Unsupported OID: " + oid, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -544,30 +619,4 @@ public abstract class V1SchemeSigner {
|
|||
"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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue