[automerger skipped] Generate SourceStamp file before JAR signature am: abcdbd2a0b am: ef5579fcbc -s ours
am skip reason: Change-Id I6af5c1a7805ef216416581d2811554adfacee884 with SHA-1 abcdbd2a0b is in history

Change-Id: Ie097ff70b2884119ed3bc6a65c14de6d2f149cb3
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 9ccd267..556c0eb 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -16,6 +16,8 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkSigningBlockNotFoundException;
 import com.android.apksig.apk.ApkUtils;
@@ -39,7 +41,6 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.security.InvalidKeyException;
-import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.SignatureException;
@@ -83,9 +84,6 @@
     /** Name of the Android manifest ZIP entry in APKs. */
     private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
 
-    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
-    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
-
     private final List<SignerConfig> mSignerConfigs;
     private final SignerConfig mSourceStampSignerConfig;
     private final Integer mMinSdkVersion;
@@ -463,12 +461,7 @@
         // more Local File Header + data entries and add to the list of output Central Directory
         // records.
         if (signerEngine.isEligibleForSourceStamp()) {
-            if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
-                throw new SignatureException("No certificates configured for stamp");
-            }
-            byte[] uncompressedData =
-                    computeSha256DigestBytes(
-                            mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+            byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
             outputOffset +=
                     outputDataToOutputApk(
                             SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
@@ -915,17 +908,6 @@
         return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
     }
 
-    private static byte[] computeSha256DigestBytes(byte[] data) {
-        MessageDigest messageDigest;
-        try {
-            messageDigest = MessageDigest.getInstance("SHA-256");
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException("SHA-256 is not found", e);
-        }
-        messageDigest.update(data);
-        return messageDigest.digest();
-    }
-
     /**
      * Configuration of a signer.
      *
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 6d768d5..49a136b 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -331,6 +331,11 @@
         return false;
     }
 
+    /** Generates the digest of the certificate used to sign the source stamp. */
+    default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        return new byte[0];
+    }
+
     /**
      * Indicates to this engine that it will no longer be used. Invoking this on an already closed
      * engine is OK.
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 2671bcf..3d98a38 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -16,18 +16,22 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.AndroidBinXmlParser;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.SourceStampVerifier;
 import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
 import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
 import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 import com.android.apksig.util.RunnablesExecutor;
@@ -191,6 +195,7 @@
         }
 
         Result result = new Result();
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
 
         // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
         // name, but the verifiers use this parameter as the schemes supported by the target SDK
@@ -230,6 +235,10 @@
                                     maxSdkVersion);
                     foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
                     result.mergeFrom(v3Result);
+                    if (apkContentDigests.isEmpty()) {
+                        apkContentDigests.putAll(
+                                getApkContentDigestsFromSigningSchemeResult(v3Result));
+                    }
                 } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
                     // v3 signature not required
                 }
@@ -267,6 +276,10 @@
                                     maxSdkVersion);
                     foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
                     result.mergeFrom(v2Result);
+                    if (apkContentDigests.isEmpty()) {
+                        apkContentDigests.putAll(
+                                getApkContentDigestsFromSigningSchemeResult(v2Result));
+                    }
                 } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
                     // v2 signature not required
                 }
@@ -274,6 +287,46 @@
                     return result;
                 }
             }
+
+            if (maxSdkVersion >= AndroidSdkVersion.R) {
+                try {
+                    List<CentralDirectoryRecord> cdRecords =
+                            V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+                    CentralDirectoryRecord sourceStampCdRecord =
+                            cdRecords.stream()
+                                    .filter(
+                                            cdRecord ->
+                                                    SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME
+                                                            .equals(cdRecord.getName()))
+                                    .findAny()
+                                    .orElse(null);
+                    // If SourceStamp file is found inside the APK, there must be a SourceStamp
+                    // block in the APK signing block as well.
+                    if (sourceStampCdRecord != null) {
+                        byte[] sourceStampCertificateDigest =
+                                LocalFileRecord.getUncompressedData(
+                                        apk,
+                                        sourceStampCdRecord,
+                                        zipSections.getZipCentralDirectoryOffset());
+                        ApkSigningBlockUtils.Result sourceStampResult =
+                                SourceStampVerifier.verify(
+                                        apk,
+                                        zipSections,
+                                        sourceStampCertificateDigest,
+                                        apkContentDigests,
+                                        Math.max(minSdkVersion, AndroidSdkVersion.R),
+                                        maxSdkVersion);
+                        result.mergeFrom(sourceStampResult);
+                    }
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                    result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+                } catch (ZipFormatException e) {
+                    throw new ApkFormatException("Failed to read APK", e);
+                }
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
         }
 
         // Android O and newer requires that APKs targeting security sandbox version 2 and higher
@@ -477,6 +530,25 @@
         return result;
     }
 
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+            ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+        for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+            for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+                    signerInfo.contentDigests) {
+                SignatureAlgorithm signatureAlgorithm =
+                        SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                ContentDigestAlgorithm contentDigestAlgorithm =
+                        signatureAlgorithm.getContentDigestAlgorithm();
+                apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+            }
+        }
+        return apkContentDigests;
+    }
+
     private static ByteBuffer getAndroidManifestFromApk(
             DataSource apk, ApkUtils.ZipSections zipSections)
                     throws IOException, ApkFormatException {
@@ -698,6 +770,10 @@
             mErrors.add(new IssueWithParams(msg, parameters));
         }
 
+        void addWarning(Issue msg, Object... parameters) {
+            mWarnings.add(new IssueWithParams(msg, parameters));
+        }
+
         /**
          * Returns errors encountered while verifying the APK's signatures.
          */
@@ -750,6 +826,7 @@
                     if (!source.signers.isEmpty()) {
                         mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
                     }
+                    break;
                 default:
                     throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
             }
@@ -2182,9 +2259,6 @@
         /** SourceStamp offers an unsupported signature. */
         SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
 
-        /** SourceStamp offers no certificates. */
-        SOURCE_STAMP_NO_CERTIFICATE("No certificates"),
-
         /**
          * SourceStamp's certificate listed in the APK signing block does not match the certificate
          * listed in the SourceStamp file in the APK.
@@ -2198,21 +2272,7 @@
          */
         SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
                 "Certificate mismatch between SourceStamp block in APK signing block and"
-                        + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
-
-        /**
-         * The APK's digest in APK signing block does not match the digest contained in the
-         * SourceStamp signature.
-         *
-         * <ul>
-         *   <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
-         *   <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})
-         *   <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})
-         * </ul>
-         */
-        SOURCE_STAMP_APK_DIGEST_DID_NOT_VERIFY(
-                "APK integrity check failed. %1$s digest mismatch."
-                        + " Expected: <%2$s>, actual: <%3$s>");
+                        + " SourceStamp file in APK: <%1$s> vs <%2$s>");
 
         private final String mFormat;
 
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 54da524..bbd6933 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -16,6 +16,8 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
@@ -47,6 +49,7 @@
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -197,8 +200,8 @@
                     if (subLineage.size() != 1) {
                         throw new IllegalArgumentException(
                                 "v1 signing enabled but the oldest signer in the"
-                                        + " SigningCertificateLineage is missing.  Please provide the"
-                                        + " oldest signer to enable v1 signing");
+                                    + " SigningCertificateLineage is missing.  Please provide the"
+                                    + " oldest signer to enable v1 signing");
                     }
                 }
                 createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
@@ -248,7 +251,7 @@
                 v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
             } else {
                 if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
-                        v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+                                v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
                         > 0) {
                     v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
                 }
@@ -484,10 +487,10 @@
 
                 Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
                         V1SchemeVerifier.getDigestsToVerify(
-                                entry.getValue(),
-                                "-Digest",
-                                mMinSdkVersion,
-                                Integer.MAX_VALUE)
+                                        entry.getValue(),
+                                        "-Digest",
+                                        mMinSdkVersion,
+                                        Integer.MAX_VALUE)
                                 .stream()
                                 .filter(d -> d.jcaDigestAlgorithm == alg)
                                 .findFirst();
@@ -655,7 +658,7 @@
     @Override
     public OutputJarSignatureRequest outputJarEntries()
             throws ApkFormatException, InvalidKeyException, SignatureException,
-            NoSuchAlgorithmException {
+                    NoSuchAlgorithmException {
         checkNotClosed();
 
         if (!mV1SignaturePending) {
@@ -677,6 +680,14 @@
             }
             mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
         }
+        if (isEligibleForSourceStamp()) {
+            MessageDigest messageDigest =
+                    MessageDigest.getInstance(
+                            V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+            messageDigest.update(generateSourceStampCertificateDigest());
+            mOutputJarEntryDigests.put(
+                    SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+        }
         mOutputJarEntryDigestRequests.clear();
 
         for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
@@ -697,6 +708,14 @@
                 (mInputJarManifestEntryDataRequest != null)
                         ? mInputJarManifestEntryDataRequest.getData()
                         : null;
+        if (isEligibleForSourceStamp()) {
+            inputJarManifest =
+                    V1SchemeSigner.generateManifestFile(
+                                    mV1ContentDigestAlgorithm,
+                                    mOutputJarEntryDigests,
+                                    inputJarManifest)
+                            .contents;
+        }
 
         // Check whether the most recently used signature (if present) is still fine.
         checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
@@ -893,6 +912,19 @@
     }
 
     @Override
+    public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+            throw new SignatureException("No certificates configured for stamp");
+        }
+        try {
+            return computeSha256DigestBytes(
+                    mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException("Failed to encode source stamp certificate", e);
+        }
+    }
+
+    @Override
     public void close() {
         mClosed = true;
 
@@ -1051,6 +1083,17 @@
         return InputJarEntryInstructions.OutputPolicy.SKIP;
     }
 
+    private static byte[] computeSha256DigestBytes(byte[] data) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("SHA-256 is not found", e);
+        }
+        messageDigest.update(data);
+        return messageDigest.digest();
+    }
+
     private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
         private final List<JarEntry> mAdditionalJarEntries;
         private volatile boolean mDone;
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 135d815..870971f 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -41,6 +41,9 @@
      */
     public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
 
+    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+
     private ApkUtils() {}
 
     /**
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 0662ada..143ee3f 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -1368,6 +1368,20 @@
             return false;
         }
 
+        public boolean containsWarnings() {
+            if (!mWarnings.isEmpty()) {
+                return true;
+            }
+            if (!signers.isEmpty()) {
+                for (SignerInfo signer : signers) {
+                    if (signer.containsWarnings()) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
         public void addError(ApkVerifier.Issue msg, Object... parameters) {
             mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
         }
@@ -1412,6 +1426,10 @@
                 return !mErrors.isEmpty();
             }
 
+            public boolean containsWarnings() {
+                return !mWarnings.isEmpty();
+            }
+
             public List<ApkVerifier.IssueWithParams> getErrors() {
                 return mErrors;
             }
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
index 894d6f1..0749ef8 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
@@ -29,6 +29,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -59,6 +60,7 @@
 
         List<Pair<Integer, byte[]>> digests =
                 digestInfo.entrySet().stream()
+                        .sorted(Comparator.comparing(e -> e.getKey().getId()))
                         .map(e -> Pair.of(e.getKey().getId(), e.getValue()))
                         .collect(Collectors.toList());
 
@@ -71,12 +73,12 @@
             throw new SignatureException(
                     "Retrieving the encoded form of the stamp certificate failed", e);
         }
-        // TODO: Sort digests
-        sourceStampBlock.digests =
+
+        byte[] digestBytes =
                 encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
         sourceStampBlock.signedDigests =
                 ApkSigningBlockUtils.generateSignaturesOverData(
-                        sourceStampSignerConfig, sourceStampBlock.digests);
+                        sourceStampSignerConfig, digestBytes);
 
         // FORMAT:
         // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
@@ -99,7 +101,6 @@
 
     private static final class SourceStampBlock {
         public byte[] stampCertificate;
-        public byte[] digests;
         public List<Pair<Integer, byte[]>> signedDigests;
     }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
new file mode 100644
index 0000000..68d225c
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -0,0 +1,277 @@
+/*
+ * 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 com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampSigner.SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+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.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 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.
+ */
+public abstract class SourceStampVerifier {
+
+    /** Hidden constructor to prevent instantiation. */
+    private SourceStampVerifier() {}
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+     * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see {@link
+     * ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *     required cryptographic algorithm implementation is missing
+     * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+     *     found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException,
+                    ApkSigningBlockUtils.SignatureNotFoundException {
+        ApkSigningBlockUtils.Result result =
+                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(apk, zipSections, SOURCE_STAMP_BLOCK_ID, result);
+
+        verify(
+                signatureInfo.signatureBlock,
+                sourceStampCertificateDigest,
+                apkContentDigests,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+     * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+     * more information about the contract of this method.
+     */
+    private static void verify(
+            ByteBuffer sourceStampBlock,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result)
+            throws NoSuchAlgorithmException {
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        result.signers.add(signerInfo);
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            ByteBuffer sourceStampBlockData =
+                    ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+            parseSourceStamp(
+                    sourceStampBlockData,
+                    certFactory,
+                    signerInfo,
+                    apkContentDigests,
+                    sourceStampCertificateDigest,
+                    minSdkVersion,
+                    maxSdkVersion);
+            result.verified = !result.containsErrors() && !result.containsWarnings();
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        } catch (ApkFormatException | BufferUnderflowException e) {
+            signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+        }
+    }
+
+    /**
+     * Parses the SourceStamp block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over digests contained in the APK signing block.
+     *
+     * <p>This method adds one or more errors to the {@code result} if a verification error is
+     * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+     * maxSdkVersion]} range.
+     */
+    private static void parseSourceStamp(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            byte[] sourceStampCertificateDigest,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        List<Pair<Integer, byte[]>> digests =
+                apkContentDigests.entrySet().stream()
+                        .sorted(Comparator.comparing(e -> e.getKey().getId()))
+                        .map(e -> Pair.of(e.getKey().getId(), e.getValue()))
+                        .collect(Collectors.toList());
+        byte[] digestBytes =
+                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+        // Parse the SourceStamp certificate.
+        byte[] sourceStampEncodedCertificate =
+                ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+        X509Certificate sourceStampCertificate;
+        try {
+            sourceStampCertificate =
+                    X509CertificateUtils.generateCertificate(
+                            sourceStampEncodedCertificate, certFactory);
+        } catch (CertificateException e) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+            return;
+        }
+        // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+        // form. Without this, getEncoded may return a different form from what was stored in
+        // the signature. This is because some X509Certificate(Factory) implementations
+        // re-encode certificates.
+        sourceStampCertificate =
+                new GuaranteedEncodedFormX509Certificate(
+                        sourceStampCertificate, sourceStampEncodedCertificate);
+        result.certs.add(sourceStampCertificate);
+
+        // Verify the SourceStamp certificate found in the signing block is the same as the
+        // SourceStamp certificate found in the APK.
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+        messageDigest.update(sourceStampEncodedCertificate);
+        byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+        if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+            result.addWarning(
+                    ApkVerifier.Issue
+                            .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+                    ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
+                    ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
+            return;
+        }
+
+        // Parse the signatures block and identify supported signatures
+        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+        int signatureCount = 0;
+        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+                result.signatures.add(
+                        new ApkSigningBlockUtils.Result.SignerInfo.Signature(
+                                sigAlgorithmId, sigBytes));
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addWarning(
+                            ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                    continue;
+                }
+                supportedSignatures.add(
+                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (result.signatures.isEmpty()) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+            return;
+        }
+
+        // Verify signatures over digests using the SourceStamp's certificate.
+        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+        try {
+            signaturesToVerify =
+                    ApkSigningBlockUtils.getSignaturesToVerify(
+                            supportedSignatures, minSdkVersion, maxSdkVersion);
+        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+            return;
+        }
+        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            PublicKey publicKey = sourceStampCertificate.getPublicKey();
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                sig.update(digestBytes);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addWarning(
+                            ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+                result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+            } catch (InvalidKeyException
+                    | InvalidAlgorithmParameterException
+                    | SignatureException e) {
+                result.addWarning(
+                        ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+    }
+}
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index fba292c..3b77b26 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.apksig;
 
-import static com.android.apksig.ApkSigner.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
 import static com.android.apksig.apk.ApkUtils.findZipSections;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -969,7 +968,7 @@
                 cdRecords.stream()
                         .filter(
                                 cdRecord ->
-                                        SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+                                        ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
                                                 cdRecord.getName()))
                         .findAny()
                         .orElse(null);
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 351d0a8..9369333 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -17,6 +17,7 @@
 package com.android.apksig;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNoException;
 
@@ -27,6 +28,12 @@
 import com.android.apksig.internal.util.HexEncoding;
 import com.android.apksig.internal.util.Resources;
 import com.android.apksig.util.DataSources;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
@@ -37,12 +44,12 @@
 import java.security.Signature;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 @RunWith(JUnit4.class)
 public class ApkVerifierTest {
@@ -52,8 +59,9 @@
     private static final String[] DSA_KEY_NAMES_2048_AND_LARGER = {"2048", "3072"};
     private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"};
     private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"};
-    private static final String[] RSA_KEY_NAMES_2048_AND_LARGER =
-            {"2048", "3072", "4096", "8192", "16384"};
+    private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = {
+        "2048", "3072", "4096", "8192", "16384"
+    };
 
     @Test
     public void testOriginalAccepted() throws Exception {
@@ -121,46 +129,36 @@
     @Test
     public void testV1OneSignerSHA1withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA224withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA256withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA384withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA512withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
@@ -233,8 +231,7 @@
     @Test
     public void testV1OneSignerSHA256withDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
         assertVerifiedForEach(
                 "v1-only-with-dsa-sha256-2.16.840.1.101.3.4.3.2-%s.apk", DSA_KEY_NAMES);
     }
@@ -246,8 +243,7 @@
         // This should fail because the v1 signature indicates that the APK was supposed to be
         // signed with v2 scheme as well, making the platform's anti-stripping protections reject
         // the APK.
-        assertVerificationFailure(
-                "v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
+        assertVerificationFailure("v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
 
         // Similar to above, but the X-Android-APK-Signed anti-stripping header in v1 signature
         // lists unknown signature schemes in addition to APK Signature Scheme v2. Unknown schemes
@@ -262,8 +258,7 @@
         // APK signed with v2 and v3 schemes, but v3 signature was stripped from the file by
         // modifying the v3 block ID to be the verity padding block ID. Without the stripping
         // protection this modification ignores the v3 signing scheme block.
-        assertVerificationFailure(
-                "v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
+        assertVerificationFailure("v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
     }
 
     @Test
@@ -271,10 +266,12 @@
         // The V2 signature scheme was introduced in N, and V3 was introduced in P. This test
         // verifies a max SDK of pre-P ignores the V3 signature and a max SDK of pre-N ignores both
         // the V2 and V3 signatures.
-        assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
-                AndroidSdkVersion.O));
-        assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
-                AndroidSdkVersion.M));
+        assertVerified(
+                verifyForMaxSdkVersion(
+                        "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.O));
+        assertVerified(
+                verifyForMaxSdkVersion(
+                        "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.M));
     }
 
     @Test
@@ -390,13 +387,11 @@
 
         // Based on v2-only-with-rsa-pkcs1-sha512-4096.apk. Obtained by modifying APK signer to
         // flip the leftmost bit in content digest before signing signed-data.
-        assertVerificationFailure(
-                "v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
+        assertVerificationFailure("v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
 
         // Based on v2-only-with-ecdsa-sha256-p256.apk. Obtained by modifying APK signer to flip the
         // leftmost bit in content digest before signing signed-data.
-        assertVerificationFailure(
-                "v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
+        assertVerificationFailure("v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
     }
 
     @Test
@@ -425,21 +420,18 @@
         // Obtained from v2-only-with-rsa-pkcs1-sha512-4096.apk by flipping a bit in the magic
         // field in the footer of APK Signing Block. This makes the APK Signing Block disappear.
         assertVerificationFailure(
-                "v2-only-wrong-apk-sig-block-magic.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-wrong-apk-sig-block-magic.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying APK signer to insert "GARBAGE" between ZIP Central Directory and
         // End of Central Directory. The APK is otherwise fine and is signed with APK Signature
         // Scheme v2. Based on v2-only-with-rsa-pkcs1-sha256.apk.
         assertVerificationFailure(
-                "v2-only-garbage-between-cd-and-eocd.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-garbage-between-cd-and-eocd.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying the size in APK Signature Block header. Based on
         // v2-only-with-ecdsa-sha512-p521.apk.
         assertVerificationFailure(
-                "v2-only-apk-sig-block-size-mismatch.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-apk-sig-block-size-mismatch.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying the ID under which APK Signature Scheme v2 Block is stored in
         // APK Signing Block and by modifying the APK signer to not insert anti-stripping
@@ -539,7 +531,8 @@
         // APK's v2 and v3 signatures contain unknown additional attributes before and after the
         // anti-stripping and lineage attributes.
         assertVerified(
-                verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));    }
+                verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));
+    }
 
     @Test
     public void testV2MismatchBetweenSignaturesAndDigestsBlockRejected() throws Exception {
@@ -585,16 +578,14 @@
     public void testV2SignerBlockWithNoCertificatesRejected() throws Exception {
         // APK is signed with v2 only. There are no certificates listed in the signer block.
         // Obtained by modifying APK signer to output no certificates.
-        assertVerificationFailure(
-                "v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
+        assertVerificationFailure("v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
     }
 
     @Test
     public void testV3SignerBlockWithNoCertificatesRejected() throws Exception {
         // APK is signed with v3 only. There are no certificates listed in the signer block.
         // Obtained by modifying APK signer to output no certificates.
-        assertVerificationFailure(
-                "v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
+        assertVerificationFailure("v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
     }
 
     @Test
@@ -739,25 +730,29 @@
         try {
             verifyForMinSdkVersion("empty-unsigned.apk", 1);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // JAR-signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v1-only-empty.apk", 18);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // APK Signature Scheme v2 signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v2-only-empty.apk", AndroidSdkVersion.N);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // APK Signature Scheme v3 signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v3-only-empty.apk", AndroidSdkVersion.P);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
     }
 
     @Test
@@ -954,8 +949,7 @@
                 verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
                 Issue.JAR_SIG_VERIFY_EXCEPTION);
         assertVerificationFailure(
-                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
-                Issue.JAR_SIG_VERIFY_EXCEPTION);
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
         // Assert that this issue fails verification of the entire signature block, rather than
         // skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
         // verification does not get there.
@@ -964,8 +958,7 @@
                 verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
                 Issue.JAR_SIG_VERIFY_EXCEPTION);
         assertVerificationFailure(
-                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
-                Issue.JAR_SIG_VERIFY_EXCEPTION);
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
     }
 
     @Test
@@ -1020,6 +1013,50 @@
         assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
     }
 
+    @Test
+    public void testSourceStampBlock_correctSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verify("valid-stamp.apk");
+        // Verifies the signature of the APK.
+        assertVerified(verificationResult);
+        // Verifies the signature of source stamp.
+        assertTrue(verificationResult.isSourceStampVerified());
+    }
+
+    @Test
+    public void testSourceStampBlock_signatureMissing() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-without-block.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING);
+    }
+
+    @Test
+    public void testSourceStampBlock_certificateMismatch() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(
+                verificationResult,
+                Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+    }
+
+    @Test
+    public void testSourceStampBlock_apkHashMismatch() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void testSourceStampBlock_malformedSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(
+                verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+    }
+
     private ApkVerifier.Result verify(String apkFilenameInResources)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, null);
@@ -1027,13 +1064,13 @@
 
     private ApkVerifier.Result verifyForMinSdkVersion(
             String apkFilenameInResources, int minSdkVersion)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, minSdkVersion, null);
     }
 
     private ApkVerifier.Result verifyForMaxSdkVersion(
             String apkFilenameInResources, int maxSdkVersion)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, maxSdkVersion);
     }
 
@@ -1041,7 +1078,7 @@
             String apkFilenameInResources,
             Integer minSdkVersionOverride,
             Integer maxSdkVersionOverride)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
 
         ApkVerifier.Builder builder =
@@ -1077,8 +1114,12 @@
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
-                msg.append("JAR signer ").append(signerName).append(": ")
-                        .append(issue.getIssue()).append(": ").append(issue);
+                msg.append("JAR signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1088,8 +1129,25 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v2 signer ")
-                        .append(signerName).append(": ")
-                        .append(issue.getIssue()).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
+            }
+        }
+        for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+            String signerName = "signer #" + (signer.getIndex() + 1);
+            for (IssueWithParams issue : signer.getErrors()) {
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK Signature Scheme v3 signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
             }
         }
 
@@ -1099,7 +1157,8 @@
     private void assertVerified(
             String apkFilenameInResources,
             Integer minSdkVersionOverride,
-            Integer maxSdkVersionOverride) throws Exception {
+            Integer maxSdkVersionOverride)
+            throws Exception {
         assertVerified(
                 verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride),
                 apkFilenameInResources);
@@ -1130,8 +1189,12 @@
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
-                msg.append("JAR signer ").append(signerName).append(": ")
-                        .append(issue.getIssue()).append(" ").append(issue);
+                msg.append("JAR signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(" ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1144,7 +1207,9 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v2 signer ")
-                        .append(signerName).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
@@ -1157,22 +1222,78 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v3 signer ")
-                        .append(signerName).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue);
             }
         }
 
-        fail("APK failed verification for the wrong reason"
-                + ". Expected: " + expectedIssue + ", actual: " + msg);
+        fail(
+                "APK failed verification for the wrong reason"
+                        + ". Expected: "
+                        + expectedIssue
+                        + ", actual: "
+                        + msg);
+    }
+
+    private static void assertSourceStampVerificationFailure(
+            ApkVerifier.Result result, Issue expectedIssue) {
+        if (result.isSourceStampVerified()) {
+            fail(
+                    "APK source stamp verification succeeded instead of failing with "
+                            + expectedIssue);
+            return;
+        }
+
+        StringBuilder msg = new StringBuilder();
+        List<IssueWithParams> resultIssueWithParams =
+                Stream.of(result.getErrors(), result.getWarnings())
+                        .filter(Objects::nonNull)
+                        .flatMap(Collection::stream)
+                        .collect(Collectors.toList());
+        for (IssueWithParams issue : resultIssueWithParams) {
+            if (expectedIssue.equals(issue.getIssue())) {
+                return;
+            }
+            if (msg.length() > 0) {
+                msg.append('\n');
+            }
+            msg.append(issue);
+        }
+
+        ApkVerifier.Result.SourceStampInfo signer = result.getSourceStampInfo();
+        if (signer != null) {
+            List<IssueWithParams> sourceStampIssueWithParams =
+                    Stream.of(signer.getErrors(), signer.getWarnings())
+                            .filter(Objects::nonNull)
+                            .flatMap(Collection::stream)
+                            .collect(Collectors.toList());
+            for (IssueWithParams issue : sourceStampIssueWithParams) {
+                if (expectedIssue.equals(issue.getIssue())) {
+                    return;
+                }
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK SourceStamp signer").append(": ").append(issue);
+            }
+        }
+
+        fail(
+                "APK source stamp failed verification for the wrong reason"
+                        + ". Expected: "
+                        + expectedIssue
+                        + ", actual: "
+                        + msg);
     }
 
     private void assertVerificationFailure(
-            String apkFilenameInResources, ApkVerifier.Issue expectedIssue)
-                    throws Exception {
+            String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
         assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
     }
 
-    private void assertVerifiedForEach(
-            String apkFilenamePatternInResources, String[] args) throws Exception {
+    private void assertVerifiedForEach(String apkFilenamePatternInResources, String[] args)
+            throws Exception {
         assertVerifiedForEach(apkFilenamePatternInResources, args, null, null);
     }
 
@@ -1180,7 +1301,8 @@
             String apkFilenamePatternInResources,
             String[] args,
             Integer minSdkVersionOverride,
-            Integer maxSdkVersionOverride) throws Exception {
+            Integer maxSdkVersionOverride)
+            throws Exception {
         for (String arg : args) {
             String apkFilenameInResources =
                     String.format(Locale.US, apkFilenamePatternInResources, arg);
@@ -1189,12 +1311,11 @@
     }
 
     private void assertVerifiedForEachForMinSdkVersion(
-            String apkFilenameInResources, String[] args, int minSdkVersion)
-                    throws Exception {
+            String apkFilenameInResources, String[] args, int minSdkVersion) throws Exception {
         assertVerifiedForEach(apkFilenameInResources, args, minSdkVersion, null);
     }
 
-    private static byte[] sha256(byte[] msg) throws Exception {
+    private static byte[] sha256(byte[] msg) {
         try {
             return MessageDigest.getInstance("SHA-256").digest(msg);
         } catch (NoSuchAlgorithmException e) {
@@ -1202,7 +1323,7 @@
         }
     }
 
-    private static void assumeThatRsaPssAvailable() throws Exception {
+    private static void assumeThatRsaPssAvailable() {
         Assume.assumeTrue(Security.getProviders("Signature.SHA256withRSA/PSS") != null);
     }
 }
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
new file mode 100644
index 0000000..1dc1e99
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
new file mode 100644
index 0000000..562805c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
new file mode 100644
index 0000000..2723cc8
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk
new file mode 100644
index 0000000..9dec2f5
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk
new file mode 100644
index 0000000..8056e0b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/valid-stamp.apk
Binary files differ