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

Commit 549ce7a4 authored by Khaled Abdelmohsen's avatar Khaled Abdelmohsen
Browse files

Modify source stamp format

Modify source stamp verifier to account for APK signature schemes V1,
V2, and V3. The verifier would verify the signature schemes found in the
APK.

If the APK is signed using a signature scheme that does not have a
corresponding verified source stamp signed digest, the source stamp is
considered unverified.

Bug: 158196850
Test: atest FrameworksCoreTests:SourceStampVerifierTest
Change-Id: I07e3606a346b598d192771945335770ce964726e
parent 58fd58de
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -420,6 +420,7 @@ final class ApkSigningBlockUtils {
    static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
    static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
    static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
    static final int CONTENT_DIGEST_SHA256 = 4;

    private static final int[] V4_CONTENT_DIGEST_ALGORITHMS =
            {CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
+162 −67
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.util.apk;

import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
@@ -27,12 +28,10 @@ import android.util.Pair;
import android.util.Slog;
import android.util.jar.StrictJarFile;

import libcore.io.IoUtils;
import libcore.io.Streams;

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;
@@ -49,11 +48,13 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
@@ -74,7 +75,11 @@ 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;
    private static final int SOURCE_STAMP_BLOCK_ID = 0x6dff800d;

    private static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
    private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
    private static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;

    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
    private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
@@ -115,7 +120,8 @@ public abstract class SourceStampVerifier {
                // SourceStamp present.
                return SourceStampVerificationResult.notPresent();
            }
            return verify(apk, sourceStampCertificateDigest);
            byte[] manifestBytes = getManifestBytes(apkJar);
            return verify(apk, sourceStampCertificateDigest, manifestBytes);
        } catch (IOException e) {
            // Any exception in reading the APK returns a non-present SourceStamp outcome
            // without affecting the outcome of any of the other signature schemes.
@@ -126,22 +132,71 @@ public abstract class SourceStampVerifier {
    }

    private static SourceStampVerificationResult verify(
            RandomAccessFile apk, byte[] sourceStampCertificateDigest) {
            RandomAccessFile apk, byte[] sourceStampCertificateDigest, byte[] manifestBytes) {
        try {
            SignatureInfo signatureInfo =
                    ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID);
            Map<Integer, byte[]> apkContentDigests = getApkContentDigests(apk);
            return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest);
        } catch (IOException | SignatureNotFoundException e) {
            Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests =
                    getSignatureSchemeApkContentDigests(apk, manifestBytes);
            return verify(
                    signatureInfo,
                    getSignatureSchemeDigests(signatureSchemeApkContentDigests),
                    sourceStampCertificateDigest);
        } catch (IOException | SignatureNotFoundException | RuntimeException e) {
            return SourceStampVerificationResult.notVerified();
        }
    }

    private static SourceStampVerificationResult verify(
            SignatureInfo signatureInfo,
            Map<Integer, byte[]> apkContentDigests,
            Map<Integer, byte[]> signatureSchemeDigests,
            byte[] sourceStampCertificateDigest)
            throws SecurityException, IOException {
        ByteBuffer sourceStampBlock = signatureInfo.signatureBlock;
        ByteBuffer sourceStampBlockData =
                ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);

        X509Certificate sourceStampCertificate =
                verifySourceStampCertificate(sourceStampBlockData, sourceStampCertificateDigest);

        // Parse signed signature schemes block.
        ByteBuffer signedSignatureSchemes =
                ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
        Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
        while (signedSignatureSchemes.hasRemaining()) {
            ByteBuffer signedSignatureScheme =
                    ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
            int signatureSchemeId = signedSignatureScheme.getInt();
            signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
        }

        for (Map.Entry<Integer, byte[]> signatureSchemeDigest : signatureSchemeDigests.entrySet()) {
            if (!signedSignatureSchemeData.containsKey(signatureSchemeDigest.getKey())) {
                throw new SecurityException(
                        String.format(
                                "No signatures found for signature scheme %d",
                                signatureSchemeDigest.getKey()));
            }
            verifySourceStampSignature(
                    signedSignatureSchemeData.get(signatureSchemeDigest.getKey()),
                    sourceStampCertificate,
                    signatureSchemeDigest.getValue());
        }

        return SourceStampVerificationResult.verified(sourceStampCertificate);
    }

    /**
     * Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp
     * certificate found in the APK. It returns the verified certificate.
     *
     * @param sourceStampBlockData the source stamp block in the APK signing block which contains
     *     the certificate used to sign the stamp digests.
     * @param sourceStampCertificateDigest the source stamp certificate digest found in the APK.
     */
    private static X509Certificate verifySourceStampCertificate(
            ByteBuffer sourceStampBlockData, byte[] sourceStampCertificateDigest)
            throws IOException {
        CertificateFactory certFactory;
        try {
            certFactory = CertificateFactory.getInstance("X.509");
@@ -149,17 +204,6 @@ public abstract class SourceStampVerifier {
            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);
@@ -172,24 +216,30 @@ public abstract class SourceStampVerifier {
        } 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();
        byte[] sourceStampBlockCertificateDigest =
                computeSha256Digest(sourceStampEncodedCertificate);
        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);

        return new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate);
    }

    /**
     * Verify the SourceStamp signature found in the signing block is signed by the SourceStamp
     * certificate found in the APK.
     *
     * @param signedBlockData the source stamp block in the APK signing block which contains the
     *     stamp signed digests.
     * @param sourceStampCertificate the source stamp certificate used to sign the stamp digests.
     * @param digest the digest to be verified being signed by the source stamp certificate.
     */
    private static void verifySourceStampSignature(
            ByteBuffer signedBlockData, X509Certificate sourceStampCertificate, byte[] digest)
            throws IOException {
        // Parse the signatures block and identify supported signatures
        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedBlockData);
        int signatureCount = 0;
        int bestSigAlgorithm = -1;
        byte[] bestSigAlgorithmSignatureBytes = null;
@@ -235,7 +285,7 @@ public abstract class SourceStampVerifier {
            if (jcaSignatureAlgorithmParams != null) {
                sig.setParameter(jcaSignatureAlgorithmParams);
            }
            sig.update(digestBytes);
            sig.update(digest);
            sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
        } catch (InvalidKeyException
                | InvalidAlgorithmParameterException
@@ -247,27 +297,44 @@ public abstract class SourceStampVerifier {
        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.
    private static Map<Integer, Map<Integer, byte[]>> getSignatureSchemeApkContentDigests(
            RandomAccessFile apk, byte[] manifestBytes) throws IOException {
        Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests = new HashMap<>();

        // Retrieve APK content digests in V3 signing block.
        try {
            SignatureInfo v3SignatureInfo =
                    ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
            return getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock);
            signatureSchemeApkContentDigests.put(
                    VERSION_APK_SIGNATURE_SCHEME_V3,
                    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.
        // Retrieve APK content digests in V2 signing block.
        try {
            SignatureInfo v2SignatureInfo =
                    ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
        return getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock);
            signatureSchemeApkContentDigests.put(
                    VERSION_APK_SIGNATURE_SCHEME_V2,
                    getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock));
        } catch (SignatureNotFoundException e) {
            // It's fine not to find a V2 signature.
        }

        // Retrieve manifest digest.
        if (manifestBytes != null) {
            Map<Integer, byte[]> jarSignatureSchemeApkContentDigests = new HashMap<>();
            jarSignatureSchemeApkContentDigests.put(
                    CONTENT_DIGEST_SHA256, computeSha256Digest(manifestBytes));
            signatureSchemeApkContentDigests.put(
                    VERSION_JAR_SIGNATURE_SCHEME, jarSignatureSchemeApkContentDigests);
        }

        return signatureSchemeApkContentDigests;
    }

    private static Map<Integer, byte[]> getApkContentDigestsFromSignatureBlock(
@@ -289,27 +356,45 @@ public abstract class SourceStampVerifier {
        return apkContentDigests;
    }

    private static Map<Integer, byte[]> getSignatureSchemeDigests(
            Map<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigests) {
        Map<Integer, byte[]> digests = new HashMap<>();
        for (Map.Entry<Integer, Map<Integer, byte[]>> signatureSchemeApkContentDigest :
                signatureSchemeApkContentDigests.entrySet()) {
            List<Pair<Integer, byte[]>> apkDigests =
                    getApkDigests(signatureSchemeApkContentDigest.getValue());
            digests.put(
                    signatureSchemeApkContentDigest.getKey(), encodeApkContentDigests(apkDigests));
        }
        return digests;
    }

    private static List<Pair<Integer, byte[]>> getApkDigests(
            Map<Integer, byte[]> apkContentDigests) {
        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
        for (Map.Entry<Integer, byte[]> apkContentDigest : apkContentDigests.entrySet()) {
            digests.add(Pair.create(apkContentDigest.getKey(), apkContentDigest.getValue()));
        }
        digests.sort(Comparator.comparing(pair -> pair.first));
        return digests;
    }

    private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException {
        InputStream inputStream = null;
        try {
        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 = apkJar.getInputStream(zipEntry);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

            // 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 Streams.readFully(apkJar.getInputStream(zipEntry));
    }

            return byteArrayOutputStream.toByteArray();
        } finally {
            IoUtils.closeQuietly(inputStream);
    private static byte[] getManifestBytes(StrictJarFile apkJar) throws IOException {
        ZipEntry zipEntry = apkJar.findEntry(JarFile.MANIFEST_NAME);
        if (zipEntry == null) {
            return null;
        }
        return Streams.readFully(apkJar.getInputStream(zipEntry));
    }

    private static byte[] encodeApkContentDigests(List<Pair<Integer, byte[]>> apkContentDigests) {
@@ -329,6 +414,16 @@ public abstract class SourceStampVerifier {
        return result.array();
    }

    private static byte[] computeSha256Digest(byte[] input) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(input);
            return messageDigest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to find SHA-256", e);
        }
    }

    private static void closeApkJar(StrictJarFile apkJar) {
        try {
            if (apkJar == null) {
+12 −0
Original line number Diff line number Diff line
{
  "presubmit": [
    {
      "name": "FrameworksCoreTests",
      "options": [
        {
          "include-filter": "android.util.apk.SourceStampVerifierTest"
        }
      ]
    }
  ]
}
(16.5 KiB)

File changed and moved.

No diff preview for this file type.

Loading