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

Commit c2c49ed0 authored by Alex Klyubin's avatar Alex Klyubin
Browse files

Unconditionally use SHA-256 when minSdkVersion is 18 or higher.

SHA-1 is deprecated, but the replacement SHA-256 is only supported
for JAR/APK and OTA update package signatures on API Level 18 and
newer. This change thus adds a --min-sdk-version command-line
parameter to signapk. When this parameter is set to 18 or higher,
SHA-256 is used instead of SHA-1. When the parameter is not provided,
SHA-1 is used same as before.

This change also removes any other digests from the MANIFEST.MF.
This is to ignore any MANIFEST.MF digests already there in the APK,
such as when re-signing an already signed APK.

Build scripts will be modified to provide the --min-sdk-version
parameter in a follow-up change. This is not done in this change
because of prebuilts which require a prebuilt version of signapk
to support this parameter before the build scripts can be modified.

Bug: 25643280
Change-Id: I6a2782e465600fe2a3ad0c10bd80db2b80a6fb76
parent 6b452b27
Loading
Loading
Loading
Loading
+65 −23
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -115,15 +116,25 @@ class SignApk {
    private static final int USE_SHA1 = 1;
    private static final int USE_SHA256 = 2;

    /**
     * Minimum Android SDK API Level which accepts JAR signatures which use SHA-256. Older platform
     * versions accept only SHA-1 signatures.
     */
    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.
     */
    private static int getDigestAlgorithm(X509Certificate cert) {
    private static int getDigestAlgorithm(X509Certificate cert, int minSdkVersion) {
        String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
        if ("SHA1WITHRSA".equals(sigAlg) ||
            "MD5WITHRSA".equals(sigAlg)) {     // see "HISTORICAL NOTE" above.
        if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) {
            // see "HISTORICAL NOTE" above.
            if (minSdkVersion < MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES) {
                return USE_SHA1;
            } else {
                return USE_SHA256;
            }
        } else if (sigAlg.startsWith("SHA256WITH")) {
            return USE_SHA256;
        } else {
@@ -133,10 +144,11 @@ class SignApk {
    }

    /** Returns the expected signature algorithm for this key type. */
    private static String getSignatureAlgorithm(X509Certificate cert) {
    private static String getSignatureAlgorithm(X509Certificate cert, int minSdkVersion) {
        String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
        if ("RSA".equalsIgnoreCase(keyType)) {
            if (getDigestAlgorithm(cert) == USE_SHA256) {
            if ((minSdkVersion >= MIN_API_LEVEL_FOR_SHA256_JAR_SIGNATURES)
                    || (getDigestAlgorithm(cert, minSdkVersion) == USE_SHA256)) {
                return "SHA256withRSA";
            } else {
                return "SHA1withRSA";
@@ -309,10 +321,24 @@ class SignApk {
                Attributes attr = null;
                if (input != null) attr = input.getAttributes(name);
                attr = attr != null ? new Attributes(attr) : new Attributes();
                // Remove any previously computed digests from this entry's attributes.
                for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext();) {
                    Object key = i.next();
                    if (!(key instanceof Attributes.Name)) {
                        continue;
                    }
                    String attributeNameLowerCase =
                            ((Attributes.Name) key).toString().toLowerCase(Locale.US);
                    if (attributeNameLowerCase.endsWith("-digest")) {
                        i.remove();
                    }
                }
                // Add SHA-1 digest if requested
                if (md_sha1 != null) {
                    attr.putValue("SHA1-Digest",
                                  new String(Base64.encode(md_sha1.digest()), "ASCII"));
                }
                // Add SHA-256 digest if requested
                if (md_sha256 != null) {
                    attr.putValue("SHA-256-Digest",
                                  new String(Base64.encode(md_sha256.digest()), "ASCII"));
@@ -438,7 +464,7 @@ class SignApk {

    /** Sign data and write the digital signature to 'out'. */
    private static void writeSignatureBlock(
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int minSdkVersion,
        OutputStream out)
        throws IOException,
               CertificateEncodingException,
@@ -449,7 +475,8 @@ class SignApk {
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
        ContentSigner signer =
                new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey, minSdkVersion))
                        .build(privateKey);
        gen.addSignerInfoGenerator(
            new JcaSignerInfoGeneratorBuilder(
@@ -631,21 +658,23 @@ class SignApk {
    }

    private static class CMSSigner implements CMSTypedData {
        private JarFile inputJar;
        private File publicKeyFile;
        private X509Certificate publicKey;
        private PrivateKey privateKey;
        private OutputStream outputStream;
        private final JarFile inputJar;
        private final File publicKeyFile;
        private final X509Certificate publicKey;
        private final PrivateKey privateKey;
        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,
                         X509Certificate publicKey, PrivateKey privateKey, int minSdkVersion,
                         OutputStream outputStream) {
            this.inputJar = inputJar;
            this.publicKeyFile = publicKeyFile;
            this.publicKey = publicKey;
            this.privateKey = privateKey;
            this.minSdkVersion = minSdkVersion;
            this.outputStream = outputStream;
            this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
        }
@@ -670,7 +699,7 @@ class SignApk {
                signer = new WholeFileSignerOutputStream(out, outputStream);
                JarOutputStream outputJar = new JarOutputStream(signer);

                int hash = getDigestAlgorithm(publicKey);
                int hash = getDigestAlgorithm(publicKey, minSdkVersion);

                // Assume the certificate is valid for at least an hour.
                long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
@@ -682,6 +711,7 @@ class SignApk {
                signFile(manifest,
                         new X509Certificate[]{ publicKey },
                         new PrivateKey[]{ privateKey },
                         minSdkVersion,
                         outputJar);

                signer.notifyClosing();
@@ -698,7 +728,7 @@ class SignApk {
                   CertificateEncodingException,
                   OperatorCreationException,
                   CMSException {
            SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
            SignApk.writeSignatureBlock(this, publicKey, privateKey, minSdkVersion, temp);
        }

        public WholeFileSignerOutputStream getSigner() {
@@ -708,9 +738,10 @@ class SignApk {

    private static void signWholeFile(JarFile inputJar, File publicKeyFile,
                                      X509Certificate publicKey, PrivateKey privateKey,
                                      int minSdkVersion,
                                      OutputStream outputStream) throws Exception {
        CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
                                         publicKey, privateKey, outputStream);
                                         publicKey, privateKey, minSdkVersion, outputStream);

        ByteArrayOutputStream temp = new ByteArrayOutputStream();

@@ -776,6 +807,7 @@ class SignApk {

    private static void signFile(Manifest manifest,
                                 X509Certificate[] publicKey, PrivateKey[] privateKey,
                                 int minSdkVersion,
                                 JarOutputStream outputJar)
        throws Exception {
        // Assume the certificate is valid for at least an hour.
@@ -795,7 +827,7 @@ class SignApk {
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
            writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k], minSdkVersion));
            byte[] signedData = baos.toByteArray();
            outputJar.write(signedData);

@@ -807,7 +839,7 @@ class SignApk {
            je.setTime(timestamp);
            outputJar.putNextEntry(je);
            writeSignatureBlock(new CMSProcessableByteArray(signedData),
                                publicKey[k], privateKey[k], outputJar);
                                publicKey[k], privateKey[k], minSdkVersion, outputJar);
        }
    }

@@ -887,6 +919,7 @@ class SignApk {
        boolean signWholeFile = false;
        String providerClass = null;
        int alignment = 4;
        int minSdkVersion = 0;

        int argstart = 0;
        while (argstart < args.length && args[argstart].startsWith("-")) {
@@ -902,6 +935,15 @@ class SignApk {
            } else if ("-a".equals(args[argstart])) {
                alignment = Integer.parseInt(args[++argstart]);
                ++argstart;
            } else if ("--min-sdk-version".equals(args[argstart])) {
                String minSdkVersionString = args[++argstart];
                try {
                    minSdkVersion = Integer.parseInt(minSdkVersionString);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(
                            "min-sdk-version must be a decimal number: " + minSdkVersionString);
                }
                ++argstart;
            } else {
                usage();
            }
@@ -931,7 +973,7 @@ class SignApk {
                for (int i = 0; i < numKeys; ++i) {
                    int argNum = argstart + i*2;
                    publicKey[i] = readPublicKey(new File(args[argNum]));
                    hashes |= getDigestAlgorithm(publicKey[i]);
                    hashes |= getDigestAlgorithm(publicKey[i], minSdkVersion);
                }
            } catch (IllegalArgumentException e) {
                System.err.println(e);
@@ -955,7 +997,7 @@ class SignApk {

            if (signWholeFile) {
                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
                                      publicKey[0], privateKey[0], outputFile);
                                      publicKey[0], privateKey[0], minSdkVersion, outputFile);
            } else {
                JarOutputStream outputJar = new JarOutputStream(outputFile);

@@ -969,7 +1011,7 @@ class SignApk {

                Manifest manifest = addDigestsToManifest(inputJar, hashes);
                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
                signFile(manifest, publicKey, privateKey, outputJar);
                signFile(manifest, publicKey, privateKey, minSdkVersion, outputJar);
                outputJar.close();
            }
        } catch (Exception e) {