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

Commit eafce4ee authored by Alex Klyubin's avatar Alex Klyubin Committed by android-build-merger
Browse files

Merge "Make signapk sign using APK Signature Scheme v2."

am: b86b5344

* commit 'b86b5344':
  Make signapk sign using APK Signature Scheme v2.
parents 8994f9ca b86b5344
Loading
Loading
Loading
Loading
+730 −0

File added.

Preview size limit exceeded, changes collapsed.

+81 −0
Original line number Original line Diff line number Diff line
/*
 * 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.signapk;

/**
 * Pair of two elements.
 */
public final class Pair<A, B> {
    private final A mFirst;
    private final B mSecond;

    private Pair(A first, B second) {
        mFirst = first;
        mSecond = second;
    }

    public static <A, B> Pair<A, B> create(A first, B second) {
        return new Pair<A, B>(first, second);
    }

    public A getFirst() {
        return mFirst;
    }

    public B getSecond() {
        return mSecond;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
        result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        @SuppressWarnings("rawtypes")
        Pair other = (Pair) obj;
        if (mFirst == null) {
            if (other.mFirst != null) {
                return false;
            }
        } else if (!mFirst.equals(other.mFirst)) {
            return false;
        }
        if (mSecond == null) {
            if (other.mSecond != null) {
                return false;
            }
        } else if (!mSecond.equals(other.mSecond)) {
            return false;
        }
        return true;
    }
}
+148 −14
Original line number Original line Diff line number Diff line
@@ -51,13 +51,16 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.security.DigestOutputStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateFactory;
@@ -68,6 +71,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Locale;
import java.util.Map;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeMap;
@@ -102,7 +106,8 @@ import javax.crypto.spec.PBEKeySpec;
/**
/**
 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
 * SHA-256 (see historical note).
 * SHA-256 (see historical note). The tool can additionally sign APKs using
 * APK Signature Scheme v2.
 */
 */
class SignApk {
class SignApk {
    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
@@ -116,6 +121,9 @@ class SignApk {
    private static final int USE_SHA1 = 1;
    private static final int USE_SHA1 = 1;
    private static final int USE_SHA256 = 2;
    private static final int USE_SHA256 = 2;


    /** Digest algorithm used when signing the APK using APK Signature Scheme v2. */
    private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256";

    /**
    /**
     * Minimum Android SDK API Level which accepts JAR signatures which use SHA-256. Older platform
     * Minimum Android SDK API Level which accepts JAR signatures which use SHA-256. Older platform
     * versions accept only SHA-1 signatures.
     * versions accept only SHA-1 signatures.
@@ -414,12 +422,22 @@ class SignApk {


    /** Write a .SF file with a digest of the specified manifest. */
    /** Write a .SF file with a digest of the specified manifest. */
    private static void writeSignatureFile(Manifest manifest, OutputStream out,
    private static void writeSignatureFile(Manifest manifest, OutputStream out,
                                           int hash)
            int hash, boolean additionallySignedUsingAnApkSignatureScheme)
        throws IOException, GeneralSecurityException {
        throws IOException, GeneralSecurityException {
        Manifest sf = new Manifest();
        Manifest sf = new Manifest();
        Attributes main = sf.getMainAttributes();
        Attributes main = sf.getMainAttributes();
        main.putValue("Signature-Version", "1.0");
        main.putValue("Signature-Version", "1.0");
        main.putValue("Created-By", "1.0 (Android SignApk)");
        main.putValue("Created-By", "1.0 (Android SignApk)");
        if (additionallySignedUsingAnApkSignatureScheme) {
            // Add APK Signature Scheme v2 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.
            main.putValue(
                    ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
                    ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
        }


        MessageDigest md = MessageDigest.getInstance(
        MessageDigest md = MessageDigest.getInstance(
            hash == USE_SHA256 ? "SHA256" : "SHA1");
            hash == USE_SHA256 ? "SHA256" : "SHA1");
@@ -712,6 +730,7 @@ class SignApk {
                         new X509Certificate[]{ publicKey },
                         new X509Certificate[]{ publicKey },
                         new PrivateKey[]{ privateKey },
                         new PrivateKey[]{ privateKey },
                         minSdkVersion,
                         minSdkVersion,
                         false, // Don't sign using APK Signature Scheme v2
                         outputJar);
                         outputJar);


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


@@ -895,6 +919,83 @@ class SignApk {
        Security.insertProviderAt((Provider) o, 1);
        Security.insertProviderAt((Provider) o, 1);
    }
    }


    /**
     * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
     * into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
     */
    public static List<ApkSignerV2.SignerConfig> createV2SignerConfigs(
            PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms)
                    throws InvalidKeyException {
        if (privateKeys.length != certificates.length) {
            throw new IllegalArgumentException(
                    "The number of private keys must match the number of certificates: "
                            + privateKeys.length + " vs" + certificates.length);
        }
        List<ApkSignerV2.SignerConfig> result = new ArrayList<>(privateKeys.length);
        for (int i = 0; i < privateKeys.length; i++) {
            PrivateKey privateKey = privateKeys[i];
            X509Certificate certificate = certificates[i];
            PublicKey publicKey = certificate.getPublicKey();
            String keyAlgorithm = privateKey.getAlgorithm();
            if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) {
                throw new InvalidKeyException(
                        "Key algorithm of private key #" + (i + 1) + " does not match key"
                        + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm
                        + " vs " + publicKey.getAlgorithm());
            }
            ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
            signerConfig.privateKey = privateKey;
            signerConfig.certificates = Collections.singletonList(certificate);
            List<Integer> signatureAlgorithms = new ArrayList<>(digestAlgorithms.length);
            for (String digestAlgorithm : digestAlgorithms) {
                try {
                    signatureAlgorithms.add(
                            getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm));
                } catch (IllegalArgumentException e) {
                    throw new InvalidKeyException(
                            "Unsupported key and digest algorithm combination for signer #"
                                    + (i + 1),
                            e);
                }
            }
            signerConfig.signatureAlgorithms = signatureAlgorithms;
            result.add(signerConfig);
        }
        return result;
    }

    private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) {
        if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) {
            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
                // deterministic signatures which make life easier for OTA updates (fewer files
                // changed when deterministic signature schemes are used).
                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256;
            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
                return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256;
            } else {
                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
            }
        } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) {
            if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
                // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
                // deterministic signatures which make life easier for OTA updates (fewer files
                // changed when deterministic signature schemes are used).
                return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
            } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
                return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512;
            } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
                return ApkSignerV2.SIGNATURE_DSA_WITH_SHA512;
            } else {
                throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm);
            }
        } else {
            throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
        }
    }

    private static void usage() {
    private static void usage() {
        System.err.println("Usage: signapk [-w] " +
        System.err.println("Usage: signapk [-w] " +
                           "[-a <alignment>] " +
                           "[-a <alignment>] " +
@@ -922,6 +1023,7 @@ class SignApk {
        String providerClass = null;
        String providerClass = null;
        int alignment = 4;
        int alignment = 4;
        int minSdkVersion = 0;
        int minSdkVersion = 0;
        boolean signUsingApkSignatureSchemeV2 = true;


        int argstart = 0;
        int argstart = 0;
        while (argstart < args.length && args[argstart].startsWith("-")) {
        while (argstart < args.length && args[argstart].startsWith("-")) {
@@ -947,6 +1049,7 @@ class SignApk {
                }
                }
                ++argstart;
                ++argstart;
            } else if ("--disable-v2".equals(args[argstart])) {
            } else if ("--disable-v2".equals(args[argstart])) {
                signUsingApkSignatureSchemeV2 = false;
                ++argstart;
                ++argstart;
            } else {
            } else {
                usage();
                usage();
@@ -998,25 +1101,56 @@ class SignApk {


            outputFile = new FileOutputStream(outputFilename);
            outputFile = new FileOutputStream(outputFilename);



            // NOTE: Signing currently recompresses any compressed entries using Deflate (default
            // compression level for OTA update files and maximum compession level for APKs).
            if (signWholeFile) {
            if (signWholeFile) {
                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
                SignApk.signWholeFile(inputJar, firstPublicKeyFile,
                                      publicKey[0], privateKey[0], minSdkVersion, outputFile);
                                      publicKey[0], privateKey[0], minSdkVersion, outputFile);
            } else {
            } else {
                JarOutputStream outputJar = new JarOutputStream(outputFile);
                // Generate, in memory, an APK signed using standard JAR Signature Scheme.

                ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
                // For signing .apks, use the maximum compression to make
                JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
                // them as small as possible (since they live forever on
                // Use maximum compression for compressed entries because the APK lives forever on
                // the system partition).  For OTA packages, use the
                // the system partition.
                // default compression level, which is much much faster
                // and produces output that is only a tiny bit larger
                // (~0.1% on full OTA packages I tested).
                outputJar.setLevel(9);
                outputJar.setLevel(9);

                Manifest manifest = addDigestsToManifest(inputJar, hashes);
                Manifest manifest = addDigestsToManifest(inputJar, hashes);
                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
                copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
                signFile(manifest, publicKey, privateKey, minSdkVersion, outputJar);
                signFile(
                        manifest,
                        publicKey, privateKey, minSdkVersion, signUsingApkSignatureSchemeV2,
                        outputJar);
                outputJar.close();
                outputJar.close();
                ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
                v1SignedApkBuf.reset();

                ByteBuffer[] outputChunks;
                if (signUsingApkSignatureSchemeV2) {
                    // Additionally sign the APK using the APK Signature Scheme v2.
                    ByteBuffer apkContents = v1SignedApk;
                    List<ApkSignerV2.SignerConfig> signerConfigs =
                            createV2SignerConfigs(
                                    privateKey,
                                    publicKey,
                                    new String[] {APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
                    outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
                } else {
                    // Output the JAR-signed APK as is.
                    outputChunks = new ByteBuffer[] {v1SignedApk};
                }

                // This assumes outputChunks are array-backed. To avoid this assumption, the
                // code could be rewritten to use FileChannel.
                for (ByteBuffer outputChunk : outputChunks) {
                    outputFile.write(
                            outputChunk.array(),
                            outputChunk.arrayOffset() + outputChunk.position(),
                            outputChunk.remaining());
                    outputChunk.position(outputChunk.limit());
                }

                outputFile.close();
                outputFile = null;
                return;
            }
            }
        } catch (Exception e) {
        } catch (Exception e) {
            e.printStackTrace();
            e.printStackTrace();
+163 −0
Original line number Original line Diff line number Diff line
/*
 * 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.signapk;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Assorted ZIP format helpers.
 *
 * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
 * order of these buffers is little-endian.
 */
public abstract class ZipUtils {
    private ZipUtils() {}

    private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
    private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
    private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
    private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
    private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;

    private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
    private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50;

    private static final int UINT32_MAX_VALUE = 0xffff;

    /**
     * Returns the position at which ZIP End of Central Directory record starts in the provided
     * buffer or {@code -1} if the record is not present.
     *
     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
     */
    public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
        assertByteOrderLittleEndian(zipContents);

        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
        // The record can be identified by its 4-byte signature/magic which is located at the very
        // beginning of the record. A complication is that the record is variable-length because of
        // the comment field.
        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
        // the candidate record's comment length is such that the remainder of the record takes up
        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
        // size of the comment field is 65535 bytes because the field is an unsigned 32-bit number.

        int archiveSize = zipContents.capacity();
        if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
            System.out.println("File size smaller than EOCD min size");
            return -1;
        }
        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT32_MAX_VALUE);
        int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
        for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
                expectedCommentLength++) {
            int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
            if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
                int actualCommentLength =
                        getUnsignedInt16(
                                zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
                if (actualCommentLength == expectedCommentLength) {
                    return eocdStartPos;
                }
            }
        }

        return -1;
    }

    /**
     * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
     * Locator.
     *
     * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
     */
    public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
            ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) {
        assertByteOrderLittleEndian(zipContents);

        // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
        // Directory Record.

        int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
        if (locatorPosition < 0) {
            return false;
        }

        return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG;
    }

    /**
     * Returns the offset of the start of the ZIP Central Directory in the archive.
     *
     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
     */
    public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        return getUnsignedInt32(
                zipEndOfCentralDirectory,
                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
    }

    /**
     * Sets the offset of the start of the ZIP Central Directory in the archive.
     *
     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
     */
    public static void setZipEocdCentralDirectoryOffset(
            ByteBuffer zipEndOfCentralDirectory, long offset) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        setUnsignedInt32(
                zipEndOfCentralDirectory,
                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
                offset);
    }

    /**
     * Returns the size (in bytes) of the ZIP Central Directory.
     *
     * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
     */
    public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        return getUnsignedInt32(
                zipEndOfCentralDirectory,
                zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
    }

    private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
        }
    }

    private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
        return buffer.getShort(offset) & 0xffff;
    }

    private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
        return buffer.getInt(offset) & 0xffffffffL;
    }

    private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
        if ((value < 0) || (value > 0xffffffffL)) {
            throw new IllegalArgumentException("uint32 value of out range: " + value);
        }
        buffer.putInt(buffer.position() + offset, (int) value);
    }
}