diff --git a/core/java/android/util/apk/SourceStampVerificationResult.java b/core/java/android/util/apk/SourceStampVerificationResult.java
new file mode 100644
index 0000000000000..2edaf623fb94e
--- /dev/null
+++ b/core/java/android/util/apk/SourceStampVerificationResult.java
@@ -0,0 +1,85 @@
+/*
+ * 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
+ *
+ *
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);
+ }
+}
diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java
new file mode 100644
index 0000000000000..759c8649532bb
--- /dev/null
+++ b/core/java/android/util/apk/SourceStampVerifier.java
@@ -0,0 +1,302 @@
+/*
+ * 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.
+ *
+ *
SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ *
The stamp is part of the APK that is protected by the signing block.
+ *
+ *
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 apkContentDigests = getApkContentDigests(apk);
+ return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest);
+ }
+
+ private static SourceStampVerificationResult verify(
+ SignatureInfo signatureInfo,
+ Map 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> 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 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 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 getApkContentDigestsFromSignatureBlock(
+ ByteBuffer signatureBlock) throws IOException {
+ Map 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> apkContentDigests) {
+ int resultSize = 0;
+ for (Pair element : apkContentDigests) {
+ resultSize += 12 + element.second.length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair 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();
+ }
+}
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/original.apk b/core/tests/coretests/assets/SourceStampVerifierTest/original.apk
new file mode 100644
index 0000000000000..05f89aa80e11f
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/original.apk differ
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk
new file mode 100644
index 0000000000000..1dc1e998f1eec
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk differ
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk
new file mode 100644
index 0000000000000..562805cf67eb0
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk differ
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk
new file mode 100644
index 0000000000000..2723cc8fdbeb5
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk differ
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk
new file mode 100644
index 0000000000000..9dec2f5911334
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk differ
diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk b/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk
new file mode 100644
index 0000000000000..8056e0bf6e50d
Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk differ
diff --git a/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java
new file mode 100644
index 0000000000000..44f64079d8313
--- /dev/null
+++ b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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 org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** Unit test for {@link android.util.apk.SourceStampVerifier} */
+@RunWith(JUnit4.class)
+public class SourceStampVerifierTest {
+
+ private final Context mContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ @Test
+ public void testSourceStamp_noStamp() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/original.apk");
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertFalse(result.isPresent());
+ assertFalse(result.isVerified());
+ assertNull(result.getCertificate());
+ }
+
+ @Test
+ public void testSourceStamp_correctSignature() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/valid-stamp.apk");
+ ZipFile apkZipFile = new ZipFile(testApk);
+ ZipEntry stampCertZipEntry = apkZipFile.getEntry("stamp-cert-sha256");
+ int size = (int) stampCertZipEntry.getSize();
+ byte[] expectedStampCertHash = new byte[size];
+ try (InputStream inputStream = apkZipFile.getInputStream(stampCertZipEntry)) {
+ inputStream.read(expectedStampCertHash);
+ }
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertTrue(result.isPresent());
+ assertTrue(result.isVerified());
+ assertNotNull(result.getCertificate());
+ byte[] actualStampCertHash =
+ MessageDigest.getInstance("SHA-256").digest(result.getCertificate().getEncoded());
+ assertArrayEquals(expectedStampCertHash, actualStampCertHash);
+ }
+
+ @Test
+ public void testSourceStamp_signatureMissing() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/stamp-without-block.apk");
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertTrue(result.isPresent());
+ assertFalse(result.isVerified());
+ assertNull(result.getCertificate());
+ }
+
+ @Test
+ public void testSourceStamp_certificateMismatch() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/stamp-certificate-mismatch.apk");
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertTrue(result.isPresent());
+ assertFalse(result.isVerified());
+ assertNull(result.getCertificate());
+ }
+
+ @Test
+ public void testSourceStamp_apkHashMismatch() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch.apk");
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertTrue(result.isPresent());
+ assertFalse(result.isVerified());
+ assertNull(result.getCertificate());
+ }
+
+ @Test
+ public void testSourceStamp_malformedSignature() throws Exception {
+ File testApk = getApk("SourceStampVerifierTest/stamp-malformed-signature.apk");
+
+ SourceStampVerificationResult result =
+ SourceStampVerifier.verify(testApk.getAbsolutePath());
+
+ assertTrue(result.isPresent());
+ assertFalse(result.isVerified());
+ assertNull(result.getCertificate());
+ }
+
+ private File getApk(String apkPath) throws IOException {
+ File testApk = File.createTempFile("SourceStampApk", ".apk");
+ try (InputStream inputStream = mContext.getAssets().open(apkPath)) {
+ Files.copy(inputStream, testApk.toPath(), REPLACE_EXISTING);
+ }
+ return testApk;
+ }
+}