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

Commit e2ab8ed0 authored by Khaled Abdelmohsen's avatar Khaled Abdelmohsen Committed by Android (Google) Code Review
Browse files

Merge "Create source stamp verifier in platform" into rvc-dev

parents 42e3933e 8ce84c85
Loading
Loading
Loading
Loading
+85 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.util.apk;

import android.annotation.Nullable;

import java.security.cert.Certificate;

/**
 * A class encapsulating the result from the source stamp verifier
 *
 * <p>It indicates whether the source stamp is verified or not, and the source stamp certificate.
 *
 * @hide
 */
public final class SourceStampVerificationResult {

    private final boolean mPresent;
    private final boolean mVerified;
    private final Certificate mCertificate;

    private SourceStampVerificationResult(
            boolean present, boolean verified, @Nullable Certificate certificate) {
        this.mPresent = present;
        this.mVerified = verified;
        this.mCertificate = certificate;
    }

    public boolean isPresent() {
        return mPresent;
    }

    public boolean isVerified() {
        return mVerified;
    }

    public Certificate getCertificate() {
        return mCertificate;
    }

    /**
     * Create a non-present source stamp outcome.
     *
     * @return A non-present source stamp result.
     */
    public static SourceStampVerificationResult notPresent() {
        return new SourceStampVerificationResult(
                /* present= */ false, /* verified= */ false, /* certificate= */ null);
    }

    /**
     * Create a verified source stamp outcome.
     *
     * @param certificate The source stamp certificate.
     * @return A verified source stamp result, and the source stamp certificate.
     */
    public static SourceStampVerificationResult verified(Certificate certificate) {
        return new SourceStampVerificationResult(
                /* present= */ true, /* verified= */ true, certificate);
    }

    /**
     * Create a non-verified source stamp outcome.
     *
     * @return A non-verified source stamp result.
     */
    public static SourceStampVerificationResult notVerified() {
        return new SourceStampVerificationResult(
                /* present= */ true, /* verified= */ false, /* certificate= */ null);
    }
}
+302 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.util.apk;

import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;

import android.util.Pair;
import android.util.jar.StrictJarFile;

import libcore.io.IoUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;

/**
 * Source Stamp verifier.
 *
 * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
 *
 * <p>The stamp is part of the APK that is protected by the signing block.
 *
 * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
 * block.
 *
 * @hide for internal use only.
 */
public abstract class SourceStampVerifier {

    private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
    private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
    private static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e;

    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
    private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";

    /** Hidden constructor to prevent instantiation. */
    private SourceStampVerifier() {}

    /** Verifies SourceStamp present in the provided APK. */
    public static SourceStampVerificationResult verify(String apkFile) {
        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
            return verify(apk);
        } catch (Exception e) {
            // Any exception in the SourceStamp verification returns a non-verified SourceStamp
            // outcome without affecting the outcome of any of the other signature schemes.
            return SourceStampVerificationResult.notVerified();
        }
    }

    private static SourceStampVerificationResult verify(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
        byte[] sourceStampCertificateDigest = getSourceStampCertificateDigest(apk);
        if (sourceStampCertificateDigest == null) {
            // SourceStamp certificate hash file not found, which means that there is not
            // SourceStamp present.
            return SourceStampVerificationResult.notPresent();
        }
        SignatureInfo signatureInfo =
                ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID);
        Map<Integer, byte[]> apkContentDigests = getApkContentDigests(apk);
        return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest);
    }

    private static SourceStampVerificationResult verify(
            SignatureInfo signatureInfo,
            Map<Integer, byte[]> apkContentDigests,
            byte[] sourceStampCertificateDigest)
            throws SecurityException, IOException {
        CertificateFactory certFactory;
        try {
            certFactory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
        }

        List<Pair<Integer, byte[]>> digests =
                apkContentDigests.entrySet().stream()
                        .sorted(Map.Entry.comparingByKey())
                        .map(e -> Pair.create(e.getKey(), e.getValue()))
                        .collect(Collectors.toList());
        byte[] digestBytes = encodeApkContentDigests(digests);

        ByteBuffer sourceStampBlock = signatureInfo.signatureBlock;
        ByteBuffer sourceStampBlockData =
                ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);

        // Parse the SourceStamp certificate.
        byte[] sourceStampEncodedCertificate =
                ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
        X509Certificate sourceStampCertificate;
        try {
            sourceStampCertificate =
                    (X509Certificate)
                            certFactory.generateCertificate(
                                    new ByteArrayInputStream(sourceStampEncodedCertificate));
        } catch (CertificateException e) {
            throw new SecurityException("Failed to decode certificate", e);
        }
        sourceStampCertificate =
                new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate);

        // Verify the SourceStamp certificate found in the signing block is the same as the
        // SourceStamp certificate found in the APK.
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(sourceStampEncodedCertificate);
            byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
            if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
                throw new SecurityException("Certificate mismatch between APK and signature block");
            }
        } catch (NoSuchAlgorithmException e) {
            throw new SecurityException("Failed to find SHA-256", e);
        }

        // Parse the signatures block and identify supported signatures
        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
        int signatureCount = 0;
        int bestSigAlgorithm = -1;
        byte[] bestSigAlgorithmSignatureBytes = null;
        while (signatures.hasRemaining()) {
            signatureCount++;
            try {
                ByteBuffer signature = getLengthPrefixedSlice(signatures);
                if (signature.remaining() < 8) {
                    throw new SecurityException("Signature record too short");
                }
                int sigAlgorithm = signature.getInt();
                if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
                    continue;
                }
                if ((bestSigAlgorithm == -1)
                        || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
                    bestSigAlgorithm = sigAlgorithm;
                    bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
                }
            } catch (IOException | BufferUnderflowException e) {
                throw new SecurityException(
                        "Failed to parse signature record #" + signatureCount, e);
            }
        }
        if (bestSigAlgorithm == -1) {
            if (signatureCount == 0) {
                throw new SecurityException("No signatures found");
            } else {
                throw new SecurityException("No supported signatures found");
            }
        }

        // Verify signatures over digests using the SourceStamp's certificate.
        Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
                getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
        String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
        AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
        PublicKey publicKey = sourceStampCertificate.getPublicKey();
        boolean sigVerified;
        try {
            Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
            sig.initVerify(publicKey);
            if (jcaSignatureAlgorithmParams != null) {
                sig.setParameter(jcaSignatureAlgorithmParams);
            }
            sig.update(digestBytes);
            sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
        } catch (InvalidKeyException
                | InvalidAlgorithmParameterException
                | SignatureException
                | NoSuchAlgorithmException e) {
            throw new SecurityException(
                    "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
        }
        if (!sigVerified) {
            throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
        }

        return SourceStampVerificationResult.verified(sourceStampCertificate);
    }

    private static Map<Integer, byte[]> getApkContentDigests(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
        // Retrieve APK content digests in V3 signing block. If a V3 signature is not found, the APK
        // content digests would be re-tried from V2 signature.
        try {
            SignatureInfo v3SignatureInfo =
                    ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
            return getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock);
        } catch (SignatureNotFoundException e) {
            // It's fine not to find a V3 signature.
        }

        // Retrieve APK content digests in V2 signing block. If a V2 signature is not found, the
        // process of retrieving APK content digests stops, and the stamp is considered un-verified.
        SignatureInfo v2SignatureInfo =
                ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
        return getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock);
    }

    private static Map<Integer, byte[]> getApkContentDigestsFromSignatureBlock(
            ByteBuffer signatureBlock) throws IOException {
        Map<Integer, byte[]> apkContentDigests = new HashMap<>();
        ByteBuffer signers = getLengthPrefixedSlice(signatureBlock);
        while (signers.hasRemaining()) {
            ByteBuffer signer = getLengthPrefixedSlice(signers);
            ByteBuffer signedData = getLengthPrefixedSlice(signer);
            ByteBuffer digests = getLengthPrefixedSlice(signedData);
            while (digests.hasRemaining()) {
                ByteBuffer digest = getLengthPrefixedSlice(digests);
                int sigAlgorithm = digest.getInt();
                byte[] contentDigest = readLengthPrefixedByteArray(digest);
                int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm);
                apkContentDigests.put(digestAlgorithm, contentDigest);
            }
        }
        return apkContentDigests;
    }

    private static byte[] getSourceStampCertificateDigest(RandomAccessFile apk) throws IOException {
        StrictJarFile apkJar =
                new StrictJarFile(
                        apk.getFD(),
                        /* verify= */ false,
                        /* signatureSchemeRollbackProtectionsEnforced= */ false);
        ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME);
        if (zipEntry == null) {
            // SourceStamp certificate hash file not found, which means that there is not
            // SourceStamp present.
            return null;
        }
        InputStream inputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            inputStream = apkJar.getInputStream(zipEntry);

            // Trying to read the certificate digest, which should be less than 1024 bytes.
            byte[] buffer = new byte[1024];
            int count = inputStream.read(buffer, 0, buffer.length);
            byteArrayOutputStream.write(buffer, 0, count);

            return byteArrayOutputStream.toByteArray();
        } finally {
            IoUtils.closeQuietly(inputStream);
        }
    }

    private static byte[] encodeApkContentDigests(List<Pair<Integer, byte[]>> apkContentDigests) {
        int resultSize = 0;
        for (Pair<Integer, byte[]> element : apkContentDigests) {
            resultSize += 12 + element.second.length;
        }
        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);
        for (Pair<Integer, byte[]> element : apkContentDigests) {
            byte[] second = element.second;
            result.putInt(8 + second.length);
            result.putInt(element.first);
            result.putInt(second.length);
            result.put(second);
        }
        return result.array();
    }
}
+5.94 KiB

File added.

No diff preview for this file type.

+16.5 KiB

File added.

No diff preview for this file type.

Loading