Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7f770c09 authored by Alex Klyubin's avatar Alex Klyubin
Browse files

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
parent dca955a9
Loading
Loading
Loading
Loading
+6 −3
Original line number Diff line number Diff line
@@ -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)
+3 −3
Original line number Diff line number Diff line
@@ -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 {
+150 −101
Original line number Diff line number Diff line
@@ -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);

            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());
            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);
        }
            return out.toByteArray();
        } catch (OperatorCreationException | CMSException | IOException e) {
            throw new SignatureException("Failed to generate signature", e);

        X500Name issuerName;
        try {
            issuerName = new X500Name(signerCert.getIssuerX500Principal().getName());
        } catch (IOException e) {
            throw new CertificateParsingException(
                    "Failed to parse signer certificate issuer name", 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");

    /**
     * Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo.
     * Returns the {@code SignerInfo} {@code DigestAlgorithm} to use for {@code SignerInfo} signing
     * using the specified digest algorithm.
     */
    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);
    @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);
        }
    }

        private final CMSSignatureEncryptionAlgorithmFinder mDefault =
                new DefaultCMSSignatureEncryptionAlgorithmFinder();
    /**
     * 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.
     */
    @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).

        @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);
        }
    }
}