Merge branch 'dev/11/fp3/security-aosp-rvc-release' into int/11/fp3

* dev/11/fp3/security-aosp-rvc-release:
  Limit the number of supported v1 and v2 signers

Change-Id: Ia883c59258569dc81455f19257b48bc93634d561
diff --git a/Android.bp b/Android.bp
index c46629f..ec57fb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -21,6 +21,7 @@
     srcs: [
         "src/main/java/**/*.java",
     ],
+    java_version: "1.8",
 }
 
 // apksigner command-line tool for signing APKs and verifying their signatures
@@ -36,4 +37,5 @@
         "conscrypt-unbundled",
     ],
     required: ["libconscrypt_openjdk_jni"],
+    java_version: "1.8",
 }
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 2f4e680..c7cb660 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -147,8 +147,9 @@
         int maxSdkVersion = Integer.MAX_VALUE;
         List<SignerParams> signers = new ArrayList<>(1);
         SignerParams signerParams = new SignerParams();
-        SignerParams sourceStampSignerParams = new SignerParams();
         SigningCertificateLineage lineage = null;
+        SignerParams sourceStampSignerParams = new SignerParams();
+        SigningCertificateLineage sourceStampLineage = null;
         List<ProviderInstallSpec> providers = new ArrayList<>();
         ProviderInstallSpec providerParams = new ProviderInstallSpec();
         OptionsParser optionsParser = new OptionsParser(params);
@@ -252,6 +253,10 @@
             } else if ("stamp-signer".equals(optionName)) {
                 sourceStampFlagFound = true;
                 sourceStampSignerParams = processSignerParams(optionsParser);
+            } else if ("stamp-lineage".equals(optionName)) {
+                File stampLineageFile = new File(
+                        optionsParser.getRequiredValue("Stamp Lineage File"));
+                sourceStampLineage = getLineageFromInputFile(stampLineageFile);
             } else {
                 throw new ParameterException(
                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -358,7 +363,8 @@
             apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
         }
         if (sourceStampSignerConfig != null) {
-            apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
+            apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig)
+                    .setSourceStampSigningCertificateLineage(sourceStampLineage);
         }
         ApkSigner apkSigner = apkSignerBuilder.build();
         try {
@@ -435,10 +441,12 @@
         boolean printCerts = false;
         boolean verbose = false;
         boolean warningsTreatedAsErrors = false;
+        boolean verifySourceStamp = false;
         File v4SignatureFile = null;
         OptionsParser optionsParser = new OptionsParser(params);
         String optionName;
         String optionOriginalForm = null;
+        String sourceCertDigest = null;
         while ((optionName = optionsParser.nextOption()) != null) {
             optionOriginalForm = optionsParser.getOptionOriginalForm();
             if ("min-sdk-version".equals(optionName)) {
@@ -461,6 +469,11 @@
                         "Input V4 Signature File"));
             } else if ("in".equals(optionName)) {
                 inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
+            } else if ("verify-source-stamp".equals(optionName)) {
+                verifySourceStamp = optionsParser.getOptionalBooleanValue(true);
+            } else if ("stamp-cert-digest".equals(optionName)) {
+                sourceCertDigest = optionsParser.getRequiredValue(
+                        "Expected source stamp certificate digest");
             } else {
                 throw new ParameterException(
                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -513,7 +526,9 @@
         ApkVerifier apkVerifier = apkVerifierBuilder.build();
         ApkVerifier.Result result;
         try {
-            result = apkVerifier.verify();
+            result = verifySourceStamp
+                    ? apkVerifier.verifySourceStamp(sourceCertDigest)
+                    : apkVerifier.verify();
         } catch (MinSdkVersionException e) {
             String msg = e.getMessage();
             if (!msg.endsWith(".")) {
@@ -524,8 +539,9 @@
                             + ". Use --min-sdk-version to override",
                     e);
         }
-        boolean verified = result.isVerified();
 
+        boolean verified = result.isVerified();
+        ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
         boolean warningsEncountered = false;
         if (verified) {
             List<X509Certificate> signerCerts = result.getSignerCertificates();
@@ -544,7 +560,9 @@
                         "Verified using v4 scheme (APK Signature Scheme v4): "
                                 + result.isVerifiedUsingV4Scheme());
                 System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
-                System.out.println("Number of signers: " + signerCerts.size());
+                if (!verifySourceStamp) {
+                    System.out.println("Number of signers: " + signerCerts.size());
+                }
             }
             if (printCerts) {
                 int signerNumber = 0;
@@ -552,6 +570,10 @@
                     signerNumber++;
                     printCertificate(signerCert, "Signer #" + signerNumber, verbose);
                 }
+                if (sourceStampInfo != null) {
+                    printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
+                            verbose);
+                }
             }
         } else {
             System.err.println("DOES NOT VERIFY");
@@ -562,7 +584,7 @@
         }
 
         @SuppressWarnings("resource") // false positive -- this resource is not opened here
-        PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out;
+                PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out;
         for (ApkVerifier.IssueWithParams warning : result.getWarnings()) {
             warningsEncountered = true;
             warningsOut.println("WARNING: " + warning);
@@ -602,7 +624,6 @@
             }
         }
 
-        ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
         if (sourceStampInfo != null) {
             for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
                 System.err.println("ERROR: SourceStamp: " + error);
@@ -961,10 +982,10 @@
 
     private static void printUsage(String page) {
         try (BufferedReader in =
-                new BufferedReader(
-                        new InputStreamReader(
-                                ApkSignerTool.class.getResourceAsStream(page),
-                                StandardCharsets.UTF_8))) {
+                     new BufferedReader(
+                             new InputStreamReader(
+                                     ApkSignerTool.class.getResourceAsStream(page),
+                                     StandardCharsets.UTF_8))) {
             String line;
             while ((line = in.readLine()) != null) {
                 System.out.println(line);
@@ -981,7 +1002,6 @@
      * @param name    the name to be used to identify the certificate.
      * @param verbose boolean indicating whether public key details from the certificate should be
      *                displayed.
-     *
      * @throws NoSuchAlgorithmException     if an instance of MD5, SHA-1, or SHA-256 cannot be
      *                                      obtained.
      * @throws CertificateEncodingException if an error is encountered when encoding the
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 154e917..d4da569 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -86,6 +86,7 @@
 
     private final List<SignerConfig> mSignerConfigs;
     private final SignerConfig mSourceStampSignerConfig;
+    private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
     private final boolean mForceSourceStampOverwrite;
     private final Integer mMinSdkVersion;
     private final boolean mV1SigningEnabled;
@@ -114,6 +115,7 @@
     private ApkSigner(
             List<SignerConfig> signerConfigs,
             SignerConfig sourceStampSignerConfig,
+            SigningCertificateLineage sourceStampSigningCertificateLineage,
             boolean forceSourceStampOverwrite,
             Integer minSdkVersion,
             boolean v1SigningEnabled,
@@ -136,6 +138,7 @@
 
         mSignerConfigs = signerConfigs;
         mSourceStampSignerConfig = sourceStampSignerConfig;
+        mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
         mForceSourceStampOverwrite = forceSourceStampOverwrite;
         mMinSdkVersion = minSdkVersion;
         mV1SigningEnabled = v1SigningEnabled;
@@ -304,6 +307,10 @@
                                         mSourceStampSignerConfig.getCertificates())
                                 .build());
             }
+            if (mSourceStampSigningCertificateLineage != null) {
+                signerEngineBuilder.setSourceStampSigningCertificateLineage(
+                        mSourceStampSigningCertificateLineage);
+            }
             signerEngine = signerEngineBuilder.build();
         }
 
@@ -1022,6 +1029,7 @@
     public static class Builder {
         private final List<SignerConfig> mSignerConfigs;
         private SignerConfig mSourceStampSignerConfig;
+        private SigningCertificateLineage mSourceStampSigningCertificateLineage;
         private boolean mForceSourceStampOverwrite = false;
         private boolean mV1SigningEnabled = true;
         private boolean mV2SigningEnabled = true;
@@ -1101,6 +1109,16 @@
         }
 
         /**
+         * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+         * signing certificate rotation for certificates previously used to sign source stamps.
+         */
+        public Builder setSourceStampSigningCertificateLineage(
+                SigningCertificateLineage sourceStampSigningCertificateLineage) {
+            mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+            return this;
+        }
+
+        /**
          * Sets whether the APK should overwrite existing source stamp, if found.
          *
          * @param force {@code true} to require the APK to be overwrite existing source stamp
@@ -1465,6 +1483,7 @@
             return new ApkSigner(
                     mSignerConfigs,
                     mSourceStampSignerConfig,
+                    mSourceStampSigningCertificateLineage,
                     mForceSourceStampOverwrite,
                     mMinSdkVersion,
                     mV1SigningEnabled,
diff --git a/src/main/java/com/android/apksig/ApkVerificationIssue.java b/src/main/java/com/android/apksig/ApkVerificationIssue.java
new file mode 100644
index 0000000..2aa9d0b
--- /dev/null
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+/**
+ * This class is intended as a lightweight representation of an APK signature verification issue
+ * where the client does not require the additional textual details provided by a subclass.
+ */
+public class ApkVerificationIssue {
+    /* The V2 signer(s) could not be read from the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNERS = 1;
+    /* A V2 signature block exists without any V2 signers */
+    public static final int V2_SIG_NO_SIGNERS = 2;
+    /* Failed to parse a signer's block in the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNER = 3;
+    /* Failed to parse the signer's signature record in the V2 signature block */
+    public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
+    /* The V2 signer contained no signatures */
+    public static final int V2_SIG_NO_SIGNATURES = 5;
+    /* The V2 signer's certificate could not be parsed */
+    public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
+    /* No signing certificates exist for the V2 signer */
+    public static final int V2_SIG_NO_CERTIFICATES = 7;
+    /* Failed to parse the V2 signer's digest record */
+    public static final int V2_SIG_MALFORMED_DIGEST = 8;
+    /* The V3 signer(s) could not be read from the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNERS = 9;
+    /* A V3 signature block exists without any V3 signers */
+    public static final int V3_SIG_NO_SIGNERS = 10;
+    /* Failed to parse a signer's block in the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNER = 11;
+    /* Failed to parse the signer's signature record in the V3 signature block */
+    public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
+    /* The V3 signer contained no signatures */
+    public static final int V3_SIG_NO_SIGNATURES = 13;
+    /* The V3 signer's certificate could not be parsed */
+    public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
+    /* No signing certificates exist for the V3 signer */
+    public static final int V3_SIG_NO_CERTIFICATES = 15;
+    /* Failed to parse the V3 signer's digest record */
+    public static final int V3_SIG_MALFORMED_DIGEST = 16;
+    /* The source stamp signer contained no signatures */
+    public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
+    /* The source stamp signer's certificate could not be parsed */
+    public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
+    /* The source stamp contains a signature produced using an unknown algorithm */
+    public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
+    /* Failed to parse the signer's signature in the source stamp signature block */
+    public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
+    /* The source stamp's signature block failed verification */
+    public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
+    /* An exception was encountered when verifying the source stamp */
+    public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
+    /* The certificate digest in the APK does not match the expected digest */
+    public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
+    /*
+     * The APK contains a source stamp signature block without a corresponding stamp certificate
+     * digest in the APK contents.
+     */
+    public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
+    /*
+     * The APK does not contain the source stamp certificate digest file nor the source stamp
+     * signature block.
+     */
+    public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
+    /*
+     * None of the signatures provided by the source stamp were produced with a known signature
+     * algorithm.
+     */
+    public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
+    /*
+     * The source stamp signer's certificate in the signing block does not match the certificate in
+     * the APK.
+     */
+    public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
+    /* The APK could not be properly parsed due to a ZIP or APK format exception */
+    public static final int MALFORMED_APK = 28;
+    /* An unexpected exception was caught when attempting to verify the APK's signatures */
+    public static final int UNEXPECTED_EXCEPTION = 29;
+    /* The APK contains the certificate digest file but does not contain a stamp signature block */
+    public static final int SOURCE_STAMP_SIG_MISSING = 30;
+    /* Source stamp block contains a malformed attribute. */
+    public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
+    /* Source stamp block contains an unknown attribute. */
+    public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
+    /**
+     * Failed to parse the SigningCertificateLineage structure in the source stamp
+     * attributes section.
+     */
+    public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
+    /**
+     * The source stamp certificate does not match the terminal node in the provided
+     * proof-of-rotation structure describing the stamp certificate history.
+     */
+    public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
+    /**
+     * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+     * with signature(s) that did not verify.
+     */
+    public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+    /** No V1 / jar signing signature blocks were found in the APK. */
+    public static final int JAR_SIG_NO_SIGNATURES = 36;
+    /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+    public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+
+    private final int mIssueId;
+    private final String mFormat;
+    private final Object[] mParams;
+
+    /**
+     * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
+     * {@code params}.
+     */
+    public ApkVerificationIssue(String format, Object... params) {
+        mIssueId = -1;
+        mFormat = format;
+        mParams = params;
+    }
+
+    /**
+     * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
+     * params}.
+     */
+    public ApkVerificationIssue(int issueId, Object... params) {
+        mIssueId = issueId;
+        mFormat = null;
+        mParams = params;
+    }
+
+    /**
+     * Returns the numeric ID for this issue.
+     */
+    public int getIssueId() {
+        return mIssueId;
+    }
+
+    /**
+     * Returns the optional parameters for this issue.
+     */
+    public Object[] getParams() {
+        return mParams;
+    }
+
+    @Override
+    public String toString() {
+        // If this instance was created by a subclass with a format string then return the same
+        // formatted String as the subclass.
+        if (mFormat != null) {
+            return String.format(mFormat, mParams);
+        }
+        StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
+        for (Object param : mParams) {
+            result.append(", ").append(param.toString());
+        }
+        return result.toString();
+    }
+}
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index baf7f68..c0cc9c5 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -18,20 +18,28 @@
 
 import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
 import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest;
+import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
-import static com.android.apksig.internal.apk.v1.V1SchemeSigner.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_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.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
 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.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
 import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
 import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
 import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
 import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
 import com.android.apksig.internal.util.AndroidSdkVersion;
@@ -53,6 +61,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -75,7 +84,7 @@
     private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
             loadSupportedApkSigSchemeNames();
 
-    private static Map<Integer,String> loadSupportedApkSigSchemeNames() {
+    private static Map<Integer, String> loadSupportedApkSigSchemeNames() {
         Map<Integer, String> supportedMap = new HashMap<>(2);
         supportedMap.put(
                 ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2");
@@ -116,12 +125,12 @@
      * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method
      * throws an exception.
      *
-     * @throws IOException if an I/O error is encountered while reading the APK
-     * @throws ApkFormatException if the APK is malformed
+     * @throws IOException              if an I/O error is encountered while reading the APK
+     * @throws ApkFormatException       if the APK is malformed
      * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
-     *         required cryptographic algorithm implementation is missing
-     * @throws IllegalStateException if this verifier's configuration is missing required
-     *         information.
+     *                                  required cryptographic algorithm implementation is missing
+     * @throws IllegalStateException    if this verifier's configuration is missing required
+     *                                  information.
      */
     public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException,
             IllegalStateException {
@@ -151,25 +160,13 @@
      * The verification result also includes errors, warnings, and information about signers.
      *
      * @param apk APK file contents
-     *
-     * @throws IOException if an I/O error is encountered while reading the APK
-     * @throws ApkFormatException if the APK is malformed
+     * @throws IOException              if an I/O error is encountered while reading the APK
+     * @throws ApkFormatException       if the APK is malformed
      * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
-     *         required cryptographic algorithm implementation is missing
+     *                                  required cryptographic algorithm implementation is missing
      */
     private Result verify(DataSource apk)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
-        if (mMinSdkVersion != null) {
-            if (mMinSdkVersion < 0) {
-                throw new IllegalArgumentException(
-                        "minSdkVersion must not be negative: " + mMinSdkVersion);
-            }
-            if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
-                throw new IllegalArgumentException(
-                        "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
-                                + ")");
-            }
-        }
         int maxSdkVersion = mMaxSdkVersion;
 
         ApkUtils.ZipSections zipSections;
@@ -181,23 +178,7 @@
 
         ByteBuffer androidManifest = null;
 
-        int minSdkVersion;
-        if (mMinSdkVersion != null) {
-            // No need to obtain minSdkVersion from the APK's AndroidManifest.xml
-            minSdkVersion = mMinSdkVersion;
-        } else {
-            // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
-            if (androidManifest == null) {
-                androidManifest = getAndroidManifestFromApk(apk, zipSections);
-            }
-            minSdkVersion =
-                    ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
-            if (minSdkVersion > mMaxSdkVersion) {
-                throw new IllegalArgumentException(
-                        "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
-                                + mMaxSdkVersion + ")");
-            }
-        }
+        int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
 
         Result result = new Result();
         Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
@@ -212,17 +193,8 @@
         // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2
         // verification is performed it would see the stripping protection attribute, see that V3
         // is in the list of supported signatures, and report a stripped signature.
-        Map<Integer, String> supportedSchemeNames;
-        if (maxSdkVersion >= AndroidSdkVersion.P) {
-            supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
-        } else if (maxSdkVersion >= AndroidSdkVersion.N) {
-            supportedSchemeNames = new HashMap<>(1);
-            supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
-                    SUPPORTED_APK_SIG_SCHEME_NAMES.get(
-                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
-        } else {
-            supportedSchemeNames = Collections.emptyMap();
-        }
+        Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion);
+
         // Android N and newer attempts to verify APKs using the APK Signing Block, which can
         // include v2 and/or v3 signatures.  If none is found, it falls back to JAR signature
         // verification. If the signature is found but does not verify, the APK is rejected.
@@ -353,7 +325,7 @@
                                 apk,
                                 sourceStampCdRecord,
                                 zipSections.getZipCentralDirectoryOffset());
-                ApkSigningBlockUtils.Result sourceStampResult =
+                ApkSigResult sourceStampResult =
                         V2SourceStampVerifier.verify(
                                 apk,
                                 zipSections,
@@ -363,7 +335,7 @@
                                 maxSdkVersion);
                 result.mergeFrom(sourceStampResult);
             }
-        } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+        } catch (SignatureNotFoundException ignored) {
             result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
         } catch (ZipFormatException e) {
             throw new ApkFormatException("Failed to read APK", e);
@@ -453,7 +425,7 @@
                 }
                 try {
                     if (!Arrays.equals(oldSignerCert.getEncoded(),
-                           v3Signers.get(0).mCerts.get(0).getEncoded())) {
+                            v3Signers.get(0).mCerts.get(0).getEncoded())) {
                         result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
                     }
                 } catch (CertificateEncodingException e) {
@@ -530,29 +502,38 @@
 
         // If the targetSdkVersion has a minimum required signature scheme version then verify
         // that the APK was signed with at least that version.
-        if (androidManifest == null) {
-            androidManifest = getAndroidManifestFromApk(apk, zipSections);
+        try {
+            if (androidManifest == null) {
+                androidManifest = getAndroidManifestFromApk(apk, zipSections);
+            }
+        } catch (ApkFormatException e) {
+            // If the manifest is not available then skip the minimum signature scheme requirement
+            // to support bundle verification.
         }
-        int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
-                androidManifest.slice());
-        int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
-        // The platform currently only enforces a single minimum signature scheme version, but when
-        // later platform versions support another minimum version this will need to be expanded to
-        // verify the minimum based on the target and maximum SDK version.
-        if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME && maxSdkVersion >= targetSdkVersion) {
-            switch(minSchemeVersion) {
-                case VERSION_APK_SIGNATURE_SCHEME_V2:
-                    if (result.isVerifiedUsingV2Scheme()) {
-                        break;
-                    }
-                    // Allow this case to fall through to the next as a signature satisfying a later
-                    // scheme version will also satisfy this requirement.
-                case VERSION_APK_SIGNATURE_SCHEME_V3:
-                    if (result.isVerifiedUsingV3Scheme()) {
-                        break;
-                    }
-                    result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion,
-                            minSchemeVersion);
+        if (androidManifest != null) {
+            int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+                    androidManifest.slice());
+            int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+            // The platform currently only enforces a single minimum signature scheme version, but
+            // when later platform versions support another minimum version this will need to be
+            // expanded to verify the minimum based on the target and maximum SDK version.
+            if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME
+                    && maxSdkVersion >= targetSdkVersion) {
+                switch (minSchemeVersion) {
+                    case VERSION_APK_SIGNATURE_SCHEME_V2:
+                        if (result.isVerifiedUsingV2Scheme()) {
+                            break;
+                        }
+                        // Allow this case to fall through to the next as a signature satisfying a
+                        // later scheme version will also satisfy this requirement.
+                    case VERSION_APK_SIGNATURE_SCHEME_V3:
+                        if (result.isVerifiedUsingV3Scheme()) {
+                            break;
+                        }
+                        result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+                                targetSdkVersion,
+                                minSchemeVersion);
+                }
             }
         }
 
@@ -581,7 +562,341 @@
         return result;
     }
 
-    private static void checkV4Certificate(List<X509Certificate> v4Certs, List<X509Certificate> v2v3Certs, Result result) {
+    /**
+     * Verifies and returns the minimum SDK version, either as provided to the builder or as read
+     * from the {@code apk}'s AndroidManifest.xml.
+     */
+    private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws ApkFormatException, IOException {
+        if (mMinSdkVersion != null) {
+            if (mMinSdkVersion < 0) {
+                throw new IllegalArgumentException(
+                        "minSdkVersion must not be negative: " + mMinSdkVersion);
+            }
+            if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
+                throw new IllegalArgumentException(
+                        "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
+                                + ")");
+            }
+            return mMinSdkVersion;
+        }
+
+        ByteBuffer androidManifest = null;
+        // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+        if (androidManifest == null) {
+            androidManifest = getAndroidManifestFromApk(apk, zipSections);
+        }
+        int minSdkVersion =
+                ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+        if (minSdkVersion > mMaxSdkVersion) {
+            throw new IllegalArgumentException(
+                    "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+                            + mMaxSdkVersion + ")");
+        }
+        return minSdkVersion;
+    }
+
+    /**
+     * Returns the mapping of signature scheme version to signature scheme name for all signature
+     * schemes starting from V2 supported by the {@code maxSdkVersion}.
+     */
+    private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) {
+        Map<Integer, String> supportedSchemeNames;
+        if (maxSdkVersion >= AndroidSdkVersion.P) {
+            supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
+        } else if (maxSdkVersion >= AndroidSdkVersion.N) {
+            supportedSchemeNames = new HashMap<>(1);
+            supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+                    SUPPORTED_APK_SIG_SCHEME_NAMES.get(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+        } else {
+            supportedSchemeNames = Collections.emptyMap();
+        }
+        return supportedSchemeNames;
+    }
+
+    /**
+     * Verifies the APK's source stamp signature and returns the result of the verification.
+     *
+     * <p>The APK's source stamp can be considered verified if the result's {@link
+     * Result#isVerified} returns {@code true}. The details of the source stamp verification can
+     * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or
+     * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the
+     * verification fails additional details regarding the failure can be obtained from {@link
+     * Result#getAllErrors()}}.
+     */
+    public Result verifySourceStamp() {
+        return verifySourceStamp(null);
+    }
+
+    /**
+     * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+     * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+     * of the verification.
+     *
+     * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+     * if present, without verifying the actual source stamp certificate used to sign the source
+     * stamp. This can be used to verify an APK contains a properly signed source stamp without
+     * verifying a particular signer.
+     *
+     * @see #verifySourceStamp()
+     */
+    public Result verifySourceStamp(String expectedCertDigest) {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verifySourceStamp(apk, expectedCertDigest);
+        } catch (IOException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.UNEXPECTED_EXCEPTION, e);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+     * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+     * returns the result of the verification.
+     *
+     * @see #verifySourceStamp(String)
+     */
+    private Result verifySourceStamp(DataSource apk, String expectedCertDigest) {
+        try {
+            ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+            int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+            // Attempt to obtain the source stamp's certificate digest from the APK.
+            List<CentralDirectoryRecord> cdRecords =
+                    V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+
+            // If the source stamp's certificate digest is not available within the APK then the
+            // source stamp cannot be verified; check if a source stamp signing block is in the
+            // APK's signature block to determine the appropriate status to return.
+            if (sourceStampCdRecord == null) {
+                boolean stampSigningBlockFound;
+                try {
+                    ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                            ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
+                    stampSigningBlockFound = true;
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+                    stampSigningBlockFound = false;
+                }
+                if (stampSigningBlockFound) {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+                            Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+                } else {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING,
+                            Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+                }
+            }
+
+            // Verify that the contents of the source stamp certificate digest match the expected
+            // value, if provided.
+            byte[] sourceStampCertificateDigest =
+                    LocalFileRecord.getUncompressedData(
+                            apk,
+                            sourceStampCdRecord,
+                            zipSections.getZipCentralDirectoryOffset());
+            if (expectedCertDigest != null) {
+                String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest);
+                if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+                    return createSourceStampResultWithError(
+                            Result.SourceStampInfo.SourceStampVerificationStatus
+                                    .CERT_DIGEST_MISMATCH,
+                            Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest,
+                            expectedCertDigest);
+                }
+            }
+
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                    new HashMap<>();
+            Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion);
+            Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+            Result result = new Result();
+            ApkSigningBlockUtils.Result v3Result = null;
+            if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+                v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+                        supportedSchemeNames, signatureSchemeApkContentDigests,
+                        VERSION_APK_SIGNATURE_SCHEME_V3,
+                        Math.max(minSdkVersion, AndroidSdkVersion.P));
+                if (v3Result != null && v3Result.containsErrors()) {
+                    result.mergeFrom(v3Result);
+                    return mergeSourceStampResult(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                            result);
+                }
+            }
+
+            ApkSigningBlockUtils.Result v2Result = null;
+            if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P
+                    || foundApkSigSchemeIds.isEmpty())) {
+                v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+                        supportedSchemeNames, signatureSchemeApkContentDigests,
+                        VERSION_APK_SIGNATURE_SCHEME_V2,
+                        Math.max(minSdkVersion, AndroidSdkVersion.N));
+                if (v2Result != null && v2Result.containsErrors()) {
+                    result.mergeFrom(v2Result);
+                    return mergeSourceStampResult(
+                            Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                            result);
+                }
+            }
+
+            if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) {
+                signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+                        getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+            }
+
+            ApkSigResult sourceStampResult =
+                    V2SourceStampVerifier.verify(
+                            apk,
+                            zipSections,
+                            sourceStampCertificateDigest,
+                            signatureSchemeApkContentDigests,
+                            minSdkVersion,
+                            mMaxSdkVersion);
+            result.mergeFrom(sourceStampResult);
+            // Since the caller is only seeking to verify the source stamp the Result can be marked
+            // as verified if the source stamp verification was successful.
+            if (sourceStampResult.verified) {
+                result.setVerified();
+            } else {
+                // To prevent APK signature verification with a failed / missing source stamp the
+                // source stamp verification will only log warnings; to allow the caller to capture
+                // the failure reason treat all warnings as errors.
+                result.setWarningsAsErrors(true);
+            }
+            return result;
+        } catch (ApkFormatException | IOException | ZipFormatException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.MALFORMED_APK, e);
+        } catch (NoSuchAlgorithmException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+                    Issue.UNEXPECTED_EXCEPTION, e);
+        } catch (SignatureNotFoundException e) {
+            return createSourceStampResultWithError(
+                    Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+                    Issue.SOURCE_STAMP_SIG_MISSING);
+        }
+    }
+
+    /**
+     * Creates and returns a {@code Result} that can be returned for source stamp verification
+     * with the provided source stamp {@code verificationStatus}, and logs an error for the
+     * specified {@code issue} and {@code params}.
+     */
+    private static Result createSourceStampResultWithError(
+            Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue,
+            Object... params) {
+        Result result = new Result();
+        result.addError(issue, params);
+        return mergeSourceStampResult(verificationStatus, result);
+    }
+
+    /**
+     * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the
+     * source stamp status to the provided {@code verificationStatus}.
+     */
+    private static Result mergeSourceStampResult(
+            Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus,
+            Result result) {
+        result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus);
+        return result;
+    }
+
+    /**
+     * Obtains the APK content digest(s) and adds them to the provided {@code
+     * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+     * merged with a {@code Result} to notify the client of any errors.
+     *
+     * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+     * content digests for V1 signatures use {@link
+     * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+     * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+     * returned.
+     */
+    private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+            ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+            Map<Integer, String> supportedSchemeNames,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+            int apkSigSchemeVersion, int minSdkVersion)
+            throws IOException, NoSuchAlgorithmException {
+        if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
+                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) {
+            return null;
+        }
+        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion);
+        SignatureInfo signatureInfo;
+        try {
+            int sigSchemeBlockId = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
+                    ? V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID
+                    : V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+            signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
+                    sigSchemeBlockId, result);
+        } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+            return null;
+        }
+        foundApkSigSchemeIds.add(apkSigSchemeVersion);
+
+        Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+            V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+                    contentDigestsToVerify, supportedSchemeNames,
+                    foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result);
+        } else {
+            V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+                    contentDigestsToVerify, result);
+        }
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                ContentDigestAlgorithm.class);
+        for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) {
+            for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+                    signerInfo.contentDigests) {
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+                        contentDigest.getSignatureAlgorithmId());
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(),
+                        contentDigest.getValue());
+            }
+        }
+        sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests);
+        return result;
+    }
+
+    private static void checkV4Certificate(List<X509Certificate> v4Certs,
+            List<X509Certificate> v2v3Certs, Result result) {
         try {
             byte[] v4Cert = v4Certs.get(0).getEncoded();
             byte[] cert = v2v3Certs.get(0).getEncoded();
@@ -593,7 +908,8 @@
         }
     }
 
-    private static byte[] pickBestDigestForV4(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+    private static byte[] pickBestDigestForV4(
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
         Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
         collectApkContentDigests(contentDigests, apkContentDigests);
         return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
@@ -614,7 +930,8 @@
             ApkUtils.ZipSections zipSections)
             throws IOException, ApkFormatException {
         CentralDirectoryRecord manifestCdRecord = null;
-        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new HashMap<>();
+        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+                ContentDigestAlgorithm.class);
         for (CentralDirectoryRecord cdRecord : cdRecords) {
             if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
                 manifestCdRecord = cdRecord;
@@ -639,7 +956,9 @@
         }
     }
 
-    private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+    private static void collectApkContentDigests(
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
         for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
             SignatureAlgorithm signatureAlgorithm =
                     SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
@@ -655,7 +974,7 @@
 
     private static ByteBuffer getAndroidManifestFromApk(
             DataSource apk, ApkUtils.ZipSections zipSections)
-                    throws IOException, ApkFormatException {
+            throws IOException, ApkFormatException {
         List<CentralDirectoryRecord> cdRecords =
                 V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
         try {
@@ -667,120 +986,6 @@
         }
     }
 
-    /**
-     * Android resource ID of the {@code android:targetSandboxVersion} attribute in
-     * AndroidManifest.xml.
-     */
-    private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
-    private static final String TARGET_SANDBOX_VERSION_ELEMENT_NAME = "manifest";
-
-    /**
-     * Android resource ID of the {@code android:targetSdkVersion} attribute in
-     * AndroidManifest.xml.
-     */
-    private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
-    private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
-    private static final String USES_SDK_ELEMENT_NAME = "uses-sdk";
-
-    /**
-     * Returns the security sandbox version targeted by an APK with the provided
-     * {@code AndroidManifest.xml}.
-     *
-     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
-     *        resource format
-     *
-     * @throws ApkFormatException if an error occurred while determining the version
-     */
-    private static int getTargetSandboxVersionFromBinaryAndroidManifest(
-            ByteBuffer androidManifestContents) throws ApkFormatException {
-        return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
-                TARGET_SANDBOX_VERSION_ELEMENT_NAME, TARGET_SANDBOX_VERSION_ATTR_ID);
-    }
-
-    /**
-     * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
-     *
-     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
-     *                                resource format
-     * @throws ApkFormatException if an error occurred while determining the version
-     */
-    private static int getTargetSdkVersionFromBinaryAndroidManifest(
-            ByteBuffer androidManifestContents) {
-        // If the targetSdkVersion is not specified then the platform will use the value of the
-        // minSdkVersion; if neither is specified then the platform will use a value of 1.
-        int minSdkVersion = 1;
-        try {
-            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
-                    USES_SDK_ELEMENT_NAME, TARGET_SDK_VERSION_ATTR_ID);
-        } catch (ApkFormatException e) {
-            // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
-            // element is not specified at all.
-        }
-        androidManifestContents.rewind();
-        try {
-            minSdkVersion = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
-                    USES_SDK_ELEMENT_NAME, MIN_SDK_VERSION_ATTR_ID);
-        } catch (ApkFormatException e) {
-            // Similar to above, expected if the APK does not contain a minSdkVersion attribute or
-            // the uses-sdk element is not specified at all.
-        }
-        return minSdkVersion;
-    }
-
-    /**
-     * Returns the integer value of the requested {@code attributeId} in the specified {@code
-     * elementName} from the provided {@code androidManifestContents} in binary Android resource
-     * format.
-     *
-     * @throws ApkFormatException if an error occurred while attempting to obtain the attribute
-     */
-    private static int getAttributeValueFromBinaryAndroidManifest(
-            ByteBuffer androidManifestContents, String elementName, int attributeId)
-            throws ApkFormatException {
-        // Return the value of the requested attribute from the specified element.
-        try {
-            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
-            int eventType = parser.getEventType();
-            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
-                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
-                        && (elementName.equals(parser.getName()))
-                        && (parser.getNamespace().isEmpty())) {
-                    int result = 1;
-                    for (int i = 0; i < parser.getAttributeCount(); i++) {
-                        if (parser.getAttributeNameResourceId(i) == attributeId) {
-                            int valueType = parser.getAttributeValueType(i);
-                            switch (valueType) {
-                                case AndroidBinXmlParser.VALUE_TYPE_INT:
-                                    result = parser.getAttributeIntValue(i);
-                                    break;
-                                default:
-                                    throw new ApkFormatException(
-                                            "Failed to determine APK's "
-                                                    + elementName + " attribute"
-                                                    + ": unsupported value type of"
-                                                    + " AndroidManifest.xml "
-                                                    + String.format("0x%08X", attributeId)
-                                                    + ". Only integer values supported.");
-                            }
-                            break;
-                        }
-                    }
-                    return result;
-                }
-                eventType = parser.next();
-            }
-            throw new ApkFormatException(
-                    "Failed to determine APK's " + elementName + " attribute "
-                            + String.format("0x%08X", attributeId)
-                            + " : no " + elementName + " element in AndroidManifest.xml");
-        } catch (AndroidBinXmlParser.XmlParserException e) {
-            throw new ApkFormatException(
-                    "Failed to determine APK's " + elementName + " attribute "
-                            + String.format("0x%08X", attributeId)
-                            + ": malformed AndroidManifest.xml", e);
-        }
-    }
-
     private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
         if (targetSdkVersion >= AndroidSdkVersion.R) {
             return VERSION_APK_SIGNATURE_SCHEME_V2;
@@ -809,6 +1014,7 @@
         private boolean mVerifiedUsingV3Scheme;
         private boolean mVerifiedUsingV4Scheme;
         private boolean mSourceStampVerified;
+        private boolean mWarningsAsErrors;
         private SigningCertificateLineage mSigningCertificateLineage;
 
         /**
@@ -937,10 +1143,24 @@
         }
 
         /**
+         * Sets whether warnings should be treated as errors.
+         */
+        void setWarningsAsErrors(boolean value) {
+            mWarningsAsErrors = value;
+        }
+
+        /**
          * Returns errors encountered while verifying the APK's signatures.
          */
         public List<IssueWithParams> getErrors() {
-            return mErrors;
+            if (!mWarningsAsErrors) {
+                return mErrors;
+            } else {
+                List<IssueWithParams> allErrors = new ArrayList<>();
+                allErrors.addAll(mErrors);
+                allErrors.addAll(mWarnings);
+                return allErrors;
+            }
         }
 
         /**
@@ -962,6 +1182,21 @@
             }
         }
 
+        private void mergeFrom(ApkSigResult source) {
+            switch (source.signatureSchemeVersion) {
+                case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                    mSourceStampVerified = source.verified;
+                    if (!source.mSigners.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown ApkSigResult Signing Block Scheme Id "
+                                    + source.signatureSchemeVersion);
+            }
+        }
+
         private void mergeFrom(ApkSigningBlockUtils.Result source) {
             if (source == null) {
                 return;
@@ -1001,8 +1236,6 @@
                 default:
                     throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
             }
-            mErrors.addAll(source.getErrors());
-            mWarnings.addAll(source.getWarnings());
         }
 
         /**
@@ -1013,11 +1246,17 @@
             if (!mErrors.isEmpty()) {
                 return true;
             }
+            if (mWarningsAsErrors && !mWarnings.isEmpty()) {
+                return true;
+            }
             if (!mV1SchemeSigners.isEmpty()) {
                 for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
                     if (signer.containsErrors()) {
                         return true;
                     }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
                 }
             }
             if (!mV2SchemeSigners.isEmpty()) {
@@ -1025,6 +1264,9 @@
                     if (signer.containsErrors()) {
                         return true;
                     }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
                 }
             }
             if (!mV3SchemeSigners.isEmpty()) {
@@ -1032,16 +1274,67 @@
                     if (signer.containsErrors()) {
                         return true;
                     }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
                 }
             }
-            if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) {
-                return true;
+            if (mSourceStampInfo != null) {
+                if (mSourceStampInfo.containsErrors()) {
+                    return true;
+                }
+                if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) {
+                    return true;
+                }
             }
 
             return false;
         }
 
         /**
+         * Returns all errors for this result, including any errors from signature scheme signers
+         * and the source stamp.
+         */
+        public List<IssueWithParams> getAllErrors() {
+            List<IssueWithParams> errors = new ArrayList<>();
+            errors.addAll(mErrors);
+            if (mWarningsAsErrors) {
+                errors.addAll(mWarnings);
+            }
+            if (!mV1SchemeSigners.isEmpty()) {
+                for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (!mV2SchemeSigners.isEmpty()) {
+                for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (!mV3SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
+            if (mSourceStampInfo != null) {
+                errors.addAll(mSourceStampInfo.getErrors());
+                if (mWarningsAsErrors) {
+                    errors.addAll(mSourceStampInfo.getWarnings());
+                }
+            }
+            return errors;
+        }
+
+        /**
          * Information about a JAR signer associated with the APK's signature.
          */
         public static class V1SchemeSignerInfo {
@@ -1337,15 +1630,50 @@
          * Information about SourceStamp associated with the APK's signature.
          */
         public static class SourceStampInfo {
+            public enum SourceStampVerificationStatus {
+                /** The stamp is present and was successfully verified. */
+                STAMP_VERIFIED,
+                /** The stamp is present but failed verification. */
+                STAMP_VERIFICATION_FAILED,
+                /** The expected cert digest did not match the digest in the APK. */
+                CERT_DIGEST_MISMATCH,
+                /** The stamp is not present at all. */
+                STAMP_MISSING,
+                /** The stamp is at least partially present, but was not able to be verified. */
+                STAMP_NOT_VERIFIED,
+                /** The stamp was not able to be verified due to an unexpected error. */
+                VERIFICATION_ERROR
+            }
+
             private final List<X509Certificate> mCertificates;
+            private final List<X509Certificate> mCertificateLineage;
 
             private final List<IssueWithParams> mErrors;
             private final List<IssueWithParams> mWarnings;
 
-            private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+            private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+
+            private SourceStampInfo(ApkSignerInfo result) {
                 mCertificates = result.certs;
-                mErrors = result.getErrors();
-                mWarnings = result.getWarnings();
+                mCertificateLineage = result.certificateLineage;
+                mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getErrors());
+                mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getWarnings());
+                if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+                    mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+                } else {
+                    mSourceStampVerificationStatus =
+                            SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+                }
+            }
+
+            SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+                mCertificates = Collections.emptyList();
+                mCertificateLineage = Collections.emptyList();
+                mErrors = Collections.emptyList();
+                mWarnings = Collections.emptyList();
+                mSourceStampVerificationStatus = sourceStampVerificationStatus;
             }
 
             /**
@@ -1359,6 +1687,13 @@
                 return mCertificates.isEmpty() ? null : mCertificates.get(0);
             }
 
+            /**
+             * Returns a list containing all of the certificates in the stamp certificate lineage.
+             */
+            public List<X509Certificate> getCertificatesInLineage() {
+                return mCertificateLineage;
+            }
+
             public boolean containsErrors() {
                 return !mErrors.isEmpty();
             }
@@ -1370,6 +1705,14 @@
             public List<IssueWithParams> getWarnings() {
                 return mWarnings;
             }
+
+            /**
+             * Returns the reason for any source stamp verification failures, or {@code
+             * STAMP_VERIFIED} if the source stamp was successfully verified.
+             */
+            public SourceStampVerificationStatus getSourceStampVerificationStatus() {
+                return mSourceStampVerificationStatus;
+            }
         }
     }
 
@@ -2367,6 +2710,14 @@
                 "V4 signature format version %1$d is different from the tool's current "
                         + "version %2$d"),
 
+        /**
+         * The APK does not contain the source stamp certificate digest file nor the signature block
+         * when verification expected a source stamp to be present.
+         */
+        SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING(
+                "Neither the source stamp certificate digest file nor the signature block are "
+                        + "present in the APK"),
+
         /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
         SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
 
@@ -2413,8 +2764,16 @@
         /** SourceStamp offers no signatures. */
         SOURCE_STAMP_NO_SIGNATURE("No signature"),
 
-        /** SourceStamp offers an unsupported signature. */
-        SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
+        /**
+         * SourceStamp offers an unsupported signature.
+         * <ul>
+         *     <li>Parameter 1: list of {@link SignatureAlgorithm}s  in the source stamp
+         *     signing block.
+         *     <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of
+         *     supported signatures.
+         * </ul>
+         */
+        SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"),
 
         /**
          * SourceStamp's certificate listed in the APK signing block does not match the certificate
@@ -2429,7 +2788,87 @@
          */
         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>");
+                        + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
+
+        /**
+         * The APK contains a source stamp signature block without the expected certificate digest
+         * in the APK contents.
+         */
+        SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST(
+                "A source stamp signature block was found without a corresponding certificate "
+                        + "digest in the APK"),
+
+        /**
+         * When verifying just the source stamp, the certificate digest in the APK does not match
+         * the expected digest.
+         * <ul>
+         *     <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK.
+         *     <li>Parameter 2: SHA-256 digest of the expected source stamp certificate.
+         * </ul>
+         */
+        SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH(
+                "The source stamp certificate digest in the APK, %1$s, does not match the "
+                        + "expected digest, %2$s"),
+
+        /**
+         * Source stamp block contains a malformed attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+         * </ul>
+         */
+        SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"),
+
+        /**
+         * Source stamp block contains an unknown attribute.
+         *
+         * <ul>
+         * <li>Parameter 1: attribute ID ({@code Integer})</li>
+         * </ul>
+         */
+        SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"),
+
+        /**
+         * Failed to parse the SigningCertificateLineage structure in the source stamp
+         * attributes section.
+         */
+        SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage "
+                + "structure in the source stamp attributes section."),
+
+        /**
+         * The source stamp certificate does not match the terminal node in the provided
+         * proof-of-rotation structure describing the stamp certificate history.
+         */
+        SOURCE_STAMP_POR_CERT_MISMATCH(
+                "APK signing certificate differs from the associated certificate found in the "
+                        + "signer's SigningCertificateLineage."),
+
+        /**
+         * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+         * with signature(s) that did not verify.
+         */
+        SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute "
+                + "contains a proof-of-rotation record with signature(s) that did not verify."),
+
+        /**
+         * The APK could not be properly parsed due to a ZIP or APK format exception.
+         * <ul>
+         *     <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK.
+         * </ul>
+         */
+        MALFORMED_APK(
+                "Malformed APK; the following exception was caught when attempting to parse the "
+                        + "APK: %1$s"),
+
+        /**
+         * An unexpected exception was caught when attempting to verify the signature(s) within the
+         * APK.
+         * <ul>
+         *     <li>Parameter 1: The {@code Exception} caught during verification.
+         * </ul>
+         */
+        UNEXPECTED_EXCEPTION(
+                "An unexpected exception was caught when verifying the signature: %1$s");
 
         private final String mFormat;
 
@@ -2450,7 +2889,7 @@
      * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
      * form.
      */
-    public static class IssueWithParams {
+    public static class IssueWithParams extends ApkVerificationIssue {
         private final Issue mIssue;
         private final Object[] mParams;
 
@@ -2459,6 +2898,7 @@
          * parameters.
          */
         public IssueWithParams(Issue issue, Object[] params) {
+            super(issue.mFormat, params);
             mIssue = issue;
             mParams = params;
         }
@@ -2573,7 +3013,6 @@
          * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}.
          *
          * @param minSdkVersion API Level of the oldest platform for which to verify the APK
-         *
          * @see #setMinCheckedPlatformVersion(int)
          */
         public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
@@ -2589,7 +3028,6 @@
          * {@link #setMinCheckedPlatformVersion(int)}.
          *
          * @param maxSdkVersion API Level of the newest platform for which to verify the APK
-         *
          * @see #setMinCheckedPlatformVersion(int)
          */
         public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
@@ -2615,4 +3053,118 @@
                     mMaxSdkVersion);
         }
     }
+
+    /**
+     * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link
+     * IssueWithParams} equivalent.
+     */
+    public static class ApkVerificationIssueAdapter {
+        private ApkVerificationIssueAdapter() {
+        }
+
+        // This field is visible for testing
+        static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
+
+        static {
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
+                    Issue.V2_SIG_MALFORMED_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS,
+                    Issue.V2_SIG_NO_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER,
+                    Issue.V2_SIG_MALFORMED_SIGNER);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE,
+                    Issue.V2_SIG_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES,
+                    Issue.V2_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE,
+                    Issue.V2_SIG_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES,
+                    Issue.V2_SIG_NO_CERTIFICATES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST,
+                    Issue.V2_SIG_MALFORMED_DIGEST);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS,
+                    Issue.V3_SIG_MALFORMED_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS,
+                    Issue.V3_SIG_NO_SIGNERS);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER,
+                    Issue.V3_SIG_MALFORMED_SIGNER);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE,
+                    Issue.V3_SIG_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES,
+                    Issue.V3_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE,
+                    Issue.V3_SIG_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES,
+                    Issue.V3_SIG_NO_CERTIFICATES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST,
+                    Issue.V3_SIG_MALFORMED_DIGEST);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE,
+                    Issue.SOURCE_STAMP_NO_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE,
+                    Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+                    Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE,
+                    Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY,
+                    Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION,
+                    Issue.SOURCE_STAMP_VERIFY_EXCEPTION);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+                    Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST,
+                    Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING,
+                    Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+                    Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+            sVerificationIssueIdToIssue.put(
+                    ApkVerificationIssue
+                            .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+                    Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK,
+                    Issue.MALFORMED_APK);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION,
+                    Issue.UNEXPECTED_EXCEPTION);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING,
+                    Issue.SOURCE_STAMP_SIG_MISSING);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+                    Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE,
+                    Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE,
+                    Issue.SOURCE_STAMP_MALFORMED_LINEAGE);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH,
+                    Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
+                    Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+                    Issue.JAR_SIG_NO_SIGNATURES);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+                    Issue.JAR_SIG_PARSE_EXCEPTION);
+        }
+
+        /**
+         * Converts the provided {@code verificationIssues} to a {@code List} of corresponding
+         * {@link IssueWithParams} instances.
+         */
+        public static List<IssueWithParams> getIssuesFromVerificationIssues(
+                List<? extends ApkVerificationIssue> verificationIssues) {
+            List<IssueWithParams> result = new ArrayList<>(verificationIssues.size());
+            for (ApkVerificationIssue issue : verificationIssues) {
+                if (issue instanceof IssueWithParams) {
+                    result.add((IssueWithParams) issue);
+                } else {
+                    result.add(
+                            new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()),
+                                    issue.getParams()));
+                }
+            }
+            return result;
+        }
+    }
 }
diff --git a/src/main/java/com/android/apksig/Constants.java b/src/main/java/com/android/apksig/Constants.java
index b0d1e76..abe0992 100644
--- a/src/main/java/com/android/apksig/Constants.java
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -16,6 +16,11 @@
 
 package com.android.apksig;
 
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+
 /**
  * Exports internally defined constants to allow clients to reference these values without relying
  * on internal code.
@@ -23,6 +28,25 @@
 public class Constants {
     private Constants() {}
 
+    public static final int VERSION_SOURCE_STAMP = 0;
+    public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+    public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+    public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+    public static final int V1_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+    public static final int V2_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
     /**
      * The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
      */
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index f0796fb..90f2a6d 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -29,6 +29,7 @@
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
 import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
 import com.android.apksig.internal.apk.v1.V1SchemeSigner;
 import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksig.internal.apk.v2.V2SchemeSigner;
@@ -98,6 +99,7 @@
     private final String mCreatedBy;
     private final List<SignerConfig> mSignerConfigs;
     private final SignerConfig mSourceStampSignerConfig;
+    private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
     private final int mMinSdkVersion;
     private final SigningCertificateLineage mSigningCertificateLineage;
 
@@ -160,6 +162,7 @@
     private DefaultApkSignerEngine(
             List<SignerConfig> signerConfigs,
             SignerConfig sourceStampSignerConfig,
+            SigningCertificateLineage sourceStampSigningCertificateLineage,
             int minSdkVersion,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
@@ -190,6 +193,7 @@
         mCreatedBy = createdBy;
         mSignerConfigs = signerConfigs;
         mSourceStampSignerConfig = sourceStampSignerConfig;
+        mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
         mMinSdkVersion = minSdkVersion;
         mSigningCertificateLineage = signingCertificateLineage;
 
@@ -307,13 +311,8 @@
         }
     }
 
-    private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
-            boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
-        List<ApkSigningBlockUtils.SignerConfig> rawConfigs =
-                createSigningBlockSignerConfigs(
-                        apkSigningBlockPaddingSupported,
-                        ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
-
+    private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
+            List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
         List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
 
         // we have our configs, now touch them up to appropriately cover all SDK levels since APK
@@ -361,26 +360,40 @@
                     "Provided key algorithms not supported on all desired "
                             + "Android SDK versions");
         }
+
         return processedConfigs;
     }
 
-    private ApkSigningBlockUtils.SignerConfig createV4SignerConfig()
-            throws InvalidKeyException, IllegalStateException {
-        List<ApkSigningBlockUtils.SignerConfig> configs =
-                createSigningBlockSignerConfigs(
-                        true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+    private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
+            boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+        return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+    }
+
+    private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+        List<ApkSigningBlockUtils.SignerConfig> configs = createSigningBlockSignerConfigs(true,
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
         if (configs.size() != 1) {
-            throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
+            // V4 only uses signer config to connect back to v3. Use the same filtering logic.
+            configs = processV3Configs(configs);
+        }
+        if (configs.size() != 1) {
+            throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
         }
         return configs.get(0);
     }
 
     private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
             throws InvalidKeyException {
-        return createSigningBlockSignerConfig(
+        ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
                 mSourceStampSignerConfig,
                 /* apkSigningBlockPaddingSupported= */ false,
                 ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        if (mSourceStampSigningCertificateLineage != null) {
+            config.mSigningCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+                    config.certificates.get(0));
+        }
+        return config;
     }
 
     private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) {
@@ -549,7 +562,7 @@
             case OUTPUT:
                 return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
             case OUTPUT_BY_ENGINE:
-                if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+                if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
                     // We copy the main section of the JAR manifest from input to output. Thus, this
                     // invalidates v1 signature and we need to see the entry's data.
                     mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
@@ -617,7 +630,7 @@
             // the entry's data is as output by the engine.
             invalidateV1Signature();
             GetJarEntryDataRequest dataRequest;
-            if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+            if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
                 dataRequest = new GetJarEntryDataRequest(entryName);
                 mInputJarManifestEntryDataRequest = dataRequest;
             } else {
@@ -752,7 +765,7 @@
                     V1SchemeSigner.generateManifestFile(
                             mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
             byte[] emittedSignatureManifest =
-                    mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
+                    mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME);
             if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
                 // Emitted v1 signature is no longer valid.
                 try {
@@ -1473,6 +1486,7 @@
     public static class Builder {
         private List<SignerConfig> mSignerConfigs;
         private SignerConfig mStampSignerConfig;
+        private SigningCertificateLineage mSourceStampSigningCertificateLineage;
         private final int mMinSdkVersion;
 
         private boolean mV1SigningEnabled = true;
@@ -1564,6 +1578,7 @@
             return new DefaultApkSignerEngine(
                     mSignerConfigs,
                     mStampSignerConfig,
+                    mSourceStampSigningCertificateLineage,
                     mMinSdkVersion,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
@@ -1582,6 +1597,16 @@
         }
 
         /**
+         * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+         * signing certificate rotation for certificates previously used to sign source stamps.
+         */
+        public Builder setSourceStampSigningCertificateLineage(
+                SigningCertificateLineage sourceStampSigningCertificateLineage) {
+            mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+            return this;
+        }
+
+        /**
          * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
          *
          * <p>By default, the APK will be signed using this scheme.
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 54340d7..b8f1f8b 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -23,6 +23,7 @@
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
 import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
@@ -192,7 +193,7 @@
                     ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
             signatureInfo =
                     ApkSigningBlockUtils.findSignature(apk, zipSections,
-                            V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
         } catch (ZipFormatException e) {
             throw new ApkFormatException(e.getMessage());
         } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
@@ -263,7 +264,7 @@
         while (additionalAttributes.hasRemaining()) {
             ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
             int id = attribute.getInt();
-            if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
+            if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
                 byte[] value = ByteBufferUtils.toByteArray(attribute);
                 SigningCertificateLineage lineage = readFromV3AttributeValue(value);
                 lineages.add(lineage);
@@ -491,20 +492,8 @@
         return result;
     }
 
-    public byte[] generateV3SignerAttribute() {
-        // FORMAT (little endian):
-        // * length-prefixed bytes: attribute pair
-        //   * uint32: ID
-        //   * bytes: value - encoded V3 SigningCertificateLineage
-        byte[] encodedLineage =
-                V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
-        int payloadSize = 4 + 4 + encodedLineage.length;
-        ByteBuffer result = ByteBuffer.allocate(payloadSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        result.putInt(4 + encodedLineage.length);
-        result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID);
-        result.put(encodedLineage);
-        return result.array();
+    public byte[] encodeSigningCertificateLineage() {
+        return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
     }
 
     public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs(
diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java
new file mode 100644
index 0000000..587cbd3
--- /dev/null
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,882 @@
+/*
+ * 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;
+
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+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.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK source stamp verifier intended only to verify the validity of the stamp signature.
+ *
+ * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
+ * when obtaining the digests for verification. This verifier should only be used in cases where
+ * another mechanism has already been used to verify the APK signatures.
+ */
+public class SourceStampVerifier {
+    private final File mApkFile;
+    private final DataSource mApkDataSource;
+
+    private final int mMinSdkVersion;
+    private final int mMaxSdkVersion;
+
+    private SourceStampVerifier(
+            File apkFile,
+            DataSource apkDataSource,
+            int minSdkVersion,
+            int maxSdkVersion) {
+        mApkFile = apkFile;
+        mApkDataSource = apkDataSource;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+    }
+
+    /**
+     * Verifies the APK's source stamp signature and returns the result of the verification.
+     *
+     * <p>The APK's source stamp can be considered verified if the result's {@link
+     * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
+     * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
+     * can be obtained as follows:
+     * <ul>
+     *     <li>Obtain the generic errors via {@link Result#getErrors()}
+     *     <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
+     *     query for any errors with {@link Result.SignerInfo#getErrors()}
+     *     <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
+     *     query for any errors with {@link Result.SignerInfo#getErrors()}
+     *     <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
+     *     for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
+     * </ul>
+     */
+    public SourceStampVerifier.Result verifySourceStamp() {
+        return verifySourceStamp(null);
+    }
+
+    /**
+     * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+     * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+     * of the verification.
+     *
+     * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+     * if present, without verifying the actual source stamp certificate used to sign the source
+     * stamp. This can be used to verify an APK contains a properly signed source stamp without
+     * verifying a particular signer.
+     *
+     * @see #verifySourceStamp()
+     */
+    public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
+        Closeable in = null;
+        try {
+            DataSource apk;
+            if (mApkDataSource != null) {
+                apk = mApkDataSource;
+            } else if (mApkFile != null) {
+                RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+                in = f;
+                apk = DataSources.asDataSource(f, 0, f.length());
+            } else {
+                throw new IllegalStateException("APK not provided");
+            }
+            return verifySourceStamp(apk, expectedCertDigest);
+        } catch (IOException e) {
+            Result result = new Result();
+            result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+            return result;
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+     * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+     * returns the result of the verification.
+     *
+     * @see #verifySourceStamp(String)
+     */
+    private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
+            String expectedCertDigest) {
+        Result result = new Result();
+        try {
+            ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+            // Attempt to obtain the source stamp's certificate digest from the APK.
+            List<CentralDirectoryRecord> cdRecords =
+                    ZipUtils.parseZipCentralDirectory(apk, zipSections);
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+
+            // If the source stamp's certificate digest is not available within the APK then the
+            // source stamp cannot be verified; check if a source stamp signing block is in the
+            // APK's signature block to determine the appropriate status to return.
+            if (sourceStampCdRecord == null) {
+                boolean stampSigningBlockFound;
+                try {
+                    ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+                    stampSigningBlockFound = true;
+                } catch (SignatureNotFoundException e) {
+                    stampSigningBlockFound = false;
+                }
+                result.addVerificationError(stampSigningBlockFound
+                        ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
+                        : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+                return result;
+            }
+
+            // Verify that the contents of the source stamp certificate digest match the expected
+            // value, if provided.
+            byte[] sourceStampCertificateDigest =
+                    LocalFileRecord.getUncompressedData(
+                            apk,
+                            sourceStampCdRecord,
+                            zipSections.getZipCentralDirectoryOffset());
+            if (expectedCertDigest != null) {
+                String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
+                        sourceStampCertificateDigest);
+                if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+                    result.addVerificationError(
+                            ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+                            actualCertDigest, expectedCertDigest);
+                    return result;
+                }
+            }
+
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                    new HashMap<>();
+            if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+                SignatureInfo signatureInfo;
+                try {
+                    signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                } catch (SignatureNotFoundException e) {
+                    signatureInfo = null;
+                }
+                if (signatureInfo != null) {
+                    Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                            ContentDigestAlgorithm.class);
+                    parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
+                            apkContentDigests, result);
+                    signatureSchemeApkContentDigests.put(
+                            VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
+                }
+            }
+
+            if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
+                    signatureSchemeApkContentDigests.isEmpty())) {
+                SignatureInfo signatureInfo;
+                try {
+                    signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+                            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+                } catch (SignatureNotFoundException e) {
+                    signatureInfo = null;
+                }
+                if (signatureInfo != null) {
+                    Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+                            ContentDigestAlgorithm.class);
+                    parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
+                            apkContentDigests, result);
+                    signatureSchemeApkContentDigests.put(
+                            VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
+                }
+            }
+
+            if (mMinSdkVersion < AndroidSdkVersion.N
+                    || signatureSchemeApkContentDigests.isEmpty()) {
+                Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
+                        getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
+                signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+                        apkContentDigests);
+            }
+
+            ApkSigResult sourceStampResult =
+                    V2SourceStampVerifier.verify(
+                            apk,
+                            zipSections,
+                            sourceStampCertificateDigest,
+                            signatureSchemeApkContentDigests,
+                            mMinSdkVersion,
+                            mMaxSdkVersion);
+            result.mergeFrom(sourceStampResult);
+            return result;
+        } catch (ApkFormatException | IOException | ZipFormatException e) {
+            result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
+        } catch (NoSuchAlgorithmException e) {
+            result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+        } catch (SignatureNotFoundException e) {
+            result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+        }
+        return result;
+    }
+
+    /**
+     * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
+     * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
+     *
+     * <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.
+     */
+    public static void parseSigners(
+            ByteBuffer apkSignatureSchemeBlock,
+            int apkSigSchemeVersion,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            Result result) {
+        boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+        // Both the V2 and V3 signature blocks contain the following:
+        // * length-prefixed sequence of length-prefixed signers
+        ByteBuffer signers;
+        try {
+            signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
+        } catch (ApkFormatException e) {
+            result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+                    : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+            return;
+        }
+        if (!signers.hasRemaining()) {
+            result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+                    : ApkVerificationIssue.V3_SIG_NO_SIGNERS);
+            return;
+        }
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+        }
+        while (signers.hasRemaining()) {
+            Result.SignerInfo signerInfo = new Result.SignerInfo();
+            if (isV2Block) {
+                result.addV2Signer(signerInfo);
+            } else {
+                result.addV3Signer(signerInfo);
+            }
+            try {
+                ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
+                parseSigner(
+                        signer,
+                        apkSigSchemeVersion,
+                        certFactory,
+                        apkContentDigests,
+                        signerInfo);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Parses the provided signer block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+     * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+     * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+     * integrity of the APK.
+     *
+     * <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 parseSigner(
+            ByteBuffer signerBlock,
+            int apkSigSchemeVersion,
+            CertificateFactory certFactory,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            Result.SignerInfo signerInfo)
+            throws ApkFormatException {
+        boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+        // Both the V2 and V3 signer blocks contain the following:
+        // * length-prefixed signed data
+        //   * length-prefixed sequence of length-prefixed digests:
+        //     * uint32: signature algorithm ID
+        //     * length-prefixed bytes: digest of contents
+        //   * length-prefixed sequence of certificates:
+        //     * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+        ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
+        ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+        ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+
+        // Parse the digests block
+        while (digests.hasRemaining()) {
+            try {
+                ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
+                int sigAlgorithmId = digest.getInt();
+                byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    continue;
+                }
+                apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
+                return;
+            }
+        }
+
+        // Parse the certificates block
+        if (certificates.hasRemaining()) {
+            byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
+            X509Certificate certificate;
+            try {
+                certificate = (X509Certificate) certFactory.generateCertificate(
+                        new ByteArrayInputStream(encodedCert));
+            } catch (CertificateException e) {
+                signerInfo.addVerificationWarning(
+                        isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
+                                : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
+                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.
+            certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+            signerInfo.setSigningCertificate(certificate);
+        }
+
+        if (signerInfo.getSigningCertificate() == null) {
+            signerInfo.addVerificationWarning(
+                    isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+                            : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+            return;
+        }
+    }
+
+    /**
+     * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
+     * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
+     * returned.
+     *
+     * <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
+     * will be updated to include a warning, but the source stamp verification can still proceed.
+     */
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+            List<CentralDirectoryRecord> cdRecords,
+            DataSource apk,
+            ZipSections zipSections,
+            Result result)
+            throws IOException, ApkFormatException {
+        CentralDirectoryRecord manifestCdRecord = null;
+        List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
+        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+                ContentDigestAlgorithm.class);
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            String cdRecordName = cdRecord.getName();
+            if (cdRecordName == null) {
+                continue;
+            }
+            if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
+                manifestCdRecord = cdRecord;
+                continue;
+            }
+            if (cdRecordName.startsWith("META-INF/")
+                    && (cdRecordName.endsWith(".RSA")
+                        || cdRecordName.endsWith(".DSA")
+                        || cdRecordName.endsWith(".EC"))) {
+                signatureBlockRecords.add(cdRecord);
+            }
+        }
+        if (manifestCdRecord == null) {
+            // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+            // digest is enough since this would affect the final digest signed by the stamp, and
+            // thus an empty digest will invalidate that signature.
+            return v1ContentDigest;
+        }
+        if (signatureBlockRecords.isEmpty()) {
+            result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+        } else {
+            for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
+                try {
+                    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                    byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
+                            signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
+                    for (Certificate certificate : certFactory.generateCertificates(
+                            new ByteArrayInputStream(signatureBlockBytes))) {
+                        // If multiple certificates are found within the signature block only the
+                        // first is used as the signer of this block.
+                        if (certificate instanceof X509Certificate) {
+                            Result.SignerInfo signerInfo = new Result.SignerInfo();
+                            signerInfo.setSigningCertificate((X509Certificate) certificate);
+                            result.addV1Signer(signerInfo);
+                            break;
+                        }
+                    }
+                } catch (CertificateException e) {
+                    // Log a warning for the parsing exception but still proceed with the stamp
+                    // verification.
+                    result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+                            signatureBlockRecord.getName(), e);
+                    break;
+                } catch (ZipFormatException e) {
+                    throw new ApkFormatException("Failed to read APK", e);
+                }
+            }
+        }
+        try {
+            byte[] manifestBytes =
+                    LocalFileRecord.getUncompressedData(
+                            apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+            v1ContentDigest.put(
+                    ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+            return v1ContentDigest;
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read APK", e);
+        }
+    }
+
+    /**
+     * Result of verifying the APK's source stamp signature; this signature can only be considered
+     * verified if {@link #isVerified()} returns true.
+     */
+    public static class Result {
+        private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
+        private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
+        private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
+        private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
+                mV2SchemeSigners, mV3SchemeSigners);
+        private SourceStampInfo mSourceStampInfo;
+
+        private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+        private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+        private boolean mVerified;
+
+        void addVerificationError(int errorId, Object... params) {
+            mErrors.add(new ApkVerificationIssue(errorId, params));
+        }
+
+        void addVerificationWarning(int warningId, Object... params) {
+            mWarnings.add(new ApkVerificationIssue(warningId, params));
+        }
+
+        private void addV1Signer(SignerInfo signerInfo) {
+            mV1SchemeSigners.add(signerInfo);
+        }
+
+        private void addV2Signer(SignerInfo signerInfo) {
+            mV2SchemeSigners.add(signerInfo);
+        }
+
+        private void addV3Signer(SignerInfo signerInfo) {
+            mV3SchemeSigners.add(signerInfo);
+        }
+
+        /**
+         * Returns {@code true} if the APK's source stamp signature
+         */
+        public boolean isVerified() {
+            return mVerified;
+        }
+
+        private void mergeFrom(ApkSigResult source) {
+            switch (source.signatureSchemeVersion) {
+                case Constants.VERSION_SOURCE_STAMP:
+                    mVerified = source.verified;
+                    if (!source.mSigners.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown ApkSigResult Signing Block Scheme Id "
+                                    + source.signatureSchemeVersion);
+            }
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV1SchemeSigners() {
+            return mV1SchemeSigners;
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV2SchemeSigners() {
+            return mV2SchemeSigners;
+        }
+
+        /**
+         * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
+         * provided APK.
+         */
+        public List<SignerInfo> getV3SchemeSigners() {
+            return mV3SchemeSigners;
+        }
+
+        /**
+         * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
+         * APK, or null if the source stamp signature verification failed before the stamp signature
+         * block could be fully parsed.
+         */
+        public SourceStampInfo getSourceStampInfo() {
+            return mSourceStampInfo;
+        }
+
+        /**
+         * Returns {@code true} if an error was encountered while verifying the APK.
+         *
+         * <p>Any error prevents the APK from being considered verified.
+         */
+        public boolean containsErrors() {
+            if (!mErrors.isEmpty()) {
+                return true;
+            }
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                }
+            }
+            if (mSourceStampInfo != null) {
+                if (mSourceStampInfo.containsErrors()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns the errors encountered while verifying the APK's source stamp.
+         */
+        public List<ApkVerificationIssue> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * Returns the warnings encountered while verifying the APK's source stamp.
+         */
+        public List<ApkVerificationIssue> getWarnings() {
+            return mWarnings;
+        }
+
+        /**
+         * Returns all errors for this result, including any errors from signature scheme signers
+         * and the source stamp.
+         */
+        public List<ApkVerificationIssue> getAllErrors() {
+            List<ApkVerificationIssue> errors = new ArrayList<>();
+            errors.addAll(mErrors);
+
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    errors.addAll(signer.getErrors());
+                }
+            }
+            if (mSourceStampInfo != null) {
+                errors.addAll(mSourceStampInfo.getErrors());
+            }
+            return errors;
+        }
+
+        /**
+         * Returns all warnings for this result, including any warnings from signature scheme
+         * signers and the source stamp.
+         */
+        public List<ApkVerificationIssue> getAllWarnings() {
+            List<ApkVerificationIssue> warnings = new ArrayList<>();
+            warnings.addAll(mWarnings);
+
+            for (List<SignerInfo> signers : mAllSchemeSigners) {
+                for (SignerInfo signer : signers) {
+                    warnings.addAll(signer.getWarnings());
+                }
+            }
+            if (mSourceStampInfo != null) {
+                warnings.addAll(mSourceStampInfo.getWarnings());
+            }
+            return warnings;
+        }
+
+        /**
+         * Contains information about an APK's signer and any errors encountered while parsing the
+         * corresponding signature block.
+         */
+        public static class SignerInfo {
+            private X509Certificate mSigningCertificate;
+            private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+            private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+            void setSigningCertificate(X509Certificate signingCertificate) {
+                mSigningCertificate = signingCertificate;
+            }
+
+            void addVerificationError(int errorId, Object... params) {
+                mErrors.add(new ApkVerificationIssue(errorId, params));
+            }
+
+            void addVerificationWarning(int warningId, Object... params) {
+                mWarnings.add(new ApkVerificationIssue(warningId, params));
+            }
+
+            /**
+             * Returns the current signing certificate used by this signer.
+             */
+            public X509Certificate getSigningCertificate() {
+                return mSigningCertificate;
+            }
+
+            /**
+             * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
+             * encountered during processing of this signer's signature block.
+             */
+            public List<ApkVerificationIssue> getErrors() {
+                return mErrors;
+            }
+
+            /**
+             * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
+             * encountered during processing of this signer's signature block.
+             */
+            public List<ApkVerificationIssue> getWarnings() {
+                return mWarnings;
+            }
+
+            /**
+             * Returns {@code true} if any errors were encountered while parsing this signer's
+             * signature block.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+        }
+
+        /**
+         * Contains information about an APK's source stamp and any errors encountered while
+         * parsing the stamp signature block.
+         */
+        public static class SourceStampInfo {
+            private final List<X509Certificate> mCertificates;
+            private final List<X509Certificate> mCertificateLineage;
+
+            private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+            private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+            /*
+             * Since this utility is intended just to verify the source stamp, and the source stamp
+             * currently only logs warnings to prevent failing the APK signature verification, treat
+             * all warnings as errors. If the stamp verification is updated to log errors this
+             * should be set to false to ensure only errors trigger a failure verifying the source
+             * stamp.
+             */
+            private static final boolean mWarningsAsErrors = true;
+
+            private SourceStampInfo(ApkSignerInfo result) {
+                mCertificates = result.certs;
+                mCertificateLineage = result.certificateLineage;
+                mErrors.addAll(result.getErrors());
+                mWarnings.addAll(result.getWarnings());
+            }
+
+            /**
+             * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+             * certificate is guaranteed to be available if no errors were encountered during
+             * verification (see {@link #containsErrors()}.
+             *
+             * <p>This certificate contains the SourceStamp's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCertificates.isEmpty() ? null : mCertificates.get(0);
+            }
+
+            /**
+             * Returns a {@code List} of {@link X509Certificate} instances representing the source
+             * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
+             * if the stamp's signing certificate has not been rotated.
+             */
+            public List<X509Certificate> getCertificatesInLineage() {
+                return mCertificateLineage;
+            }
+
+            /**
+             * Returns whether any errors were encountered during the source stamp verification.
+             */
+            public boolean containsErrors() {
+                return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
+            }
+
+            /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
+             * encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getErrors() {
+                if (!mWarningsAsErrors) {
+                    return mErrors;
+                }
+                List<ApkVerificationIssue> result = new ArrayList<>();
+                result.addAll(mErrors);
+                result.addAll(mWarnings);
+                return result;
+            }
+
+            /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
+             * were encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getWarnings() {
+                return mWarnings;
+            }
+        }
+    }
+
+    /**
+     * Builder of {@link SourceStampVerifier} instances.
+     *
+     * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
+     * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
+     * queried to determine the APK's minimum supported level, so the caller should specify a lower
+     * bound with {@link #setMinCheckedPlatformVersion(int)}.
+     */
+    public static class Builder {
+        private final File mApkFile;
+        private final DataSource mApkDataSource;
+
+        private int mMinSdkVersion = 1;
+        private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+        /**
+         * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+         * apk}.
+         */
+        public Builder(File apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkFile = apk;
+            mApkDataSource = null;
+        }
+
+        /**
+         * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+         * apk}.
+         */
+        public Builder(DataSource apk) {
+            if (apk == null) {
+                throw new NullPointerException("apk == null");
+            }
+            mApkDataSource = apk;
+            mApkFile = null;
+        }
+
+        /**
+         * Sets the oldest Android platform version for which the APK's source stamp is verified.
+         *
+         * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+         * on all Android platforms starting from the platform version with the provided {@code
+         * minSdkVersion}. The upper end of the platform versions range can be modified via
+         * {@link #setMaxCheckedPlatformVersion(int)}.
+         *
+         * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+         */
+        public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+            mMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
+         * Sets the newest Android platform version for which the APK's source stamp  is verified.
+         *
+         * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+         * on all platform versions up to and including the proviced {@code maxSdkVersion}. The
+         * lower end of the platform versions range can be modified via {@link
+         * #setMinCheckedPlatformVersion(int)}.
+         *
+         * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+         * @see #setMinCheckedPlatformVersion(int)
+         */
+        public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+            mMaxSdkVersion = maxSdkVersion;
+            return this;
+        }
+
+        /**
+         * Returns a {@link SourceStampVerifier} initialized according to the configuration of this
+         * builder.
+         */
+        public SourceStampVerifier build() {
+            return new SourceStampVerifier(
+                    mApkFile,
+                    mApkDataSource,
+                    mMinSdkVersion,
+                    mMaxSdkVersion);
+        }
+    }
+}
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index c6cbf5f..69399a7 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -17,6 +17,7 @@
 package com.android.apksig.apk;
 
 import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
 import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -24,11 +25,10 @@
 import com.android.apksig.internal.zip.ZipUtils;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.zip.ZipFormatException;
+
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
@@ -44,7 +44,8 @@
     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";
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
+            SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
 
     private ApkUtils() {}
 
@@ -56,101 +57,27 @@
      */
     public static ZipSections findZipSections(DataSource apk)
             throws IOException, ZipFormatException {
-        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
-                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
-        if (eocdAndOffsetInFile == null) {
-            throw new ZipFormatException("ZIP End of Central Directory record not found");
-        }
-
-        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
-        long eocdOffset = eocdAndOffsetInFile.getSecond();
-        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
-        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
-        if (cdStartOffset > eocdOffset) {
-            throw new ZipFormatException(
-                    "ZIP Central Directory start offset out of range: " + cdStartOffset
-                        + ". ZIP End of Central Directory offset: " + eocdOffset);
-        }
-
-        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
-        long cdEndOffset = cdStartOffset + cdSizeBytes;
-        if (cdEndOffset > eocdOffset) {
-            throw new ZipFormatException(
-                    "ZIP Central Directory overlaps with End of Central Directory"
-                            + ". CD end: " + cdEndOffset
-                            + ", EoCD start: " + eocdOffset);
-        }
-
-        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
-
+        com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
         return new ZipSections(
-                cdStartOffset,
-                cdSizeBytes,
-                cdRecordCount,
-                eocdOffset,
-                eocdBuf);
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipCentralDirectorySizeBytes(),
+                zipSections.getZipCentralDirectoryRecordCount(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectory());
     }
 
     /**
      * Information about the ZIP sections of an APK.
      */
-    public static class ZipSections {
-        private final long mCentralDirectoryOffset;
-        private final long mCentralDirectorySizeBytes;
-        private final int mCentralDirectoryRecordCount;
-        private final long mEocdOffset;
-        private final ByteBuffer mEocd;
-
+    public static class ZipSections extends com.android.apksig.zip.ZipSections {
         public ZipSections(
                 long centralDirectoryOffset,
                 long centralDirectorySizeBytes,
                 int centralDirectoryRecordCount,
                 long eocdOffset,
                 ByteBuffer eocd) {
-            mCentralDirectoryOffset = centralDirectoryOffset;
-            mCentralDirectorySizeBytes = centralDirectorySizeBytes;
-            mCentralDirectoryRecordCount = centralDirectoryRecordCount;
-            mEocdOffset = eocdOffset;
-            mEocd = eocd;
-        }
-
-        /**
-         * Returns the start offset of the ZIP Central Directory. This value is taken from the
-         * ZIP End of Central Directory record.
-         */
-        public long getZipCentralDirectoryOffset() {
-            return mCentralDirectoryOffset;
-        }
-
-        /**
-         * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
-         * ZIP End of Central Directory record.
-         */
-        public long getZipCentralDirectorySizeBytes() {
-            return mCentralDirectorySizeBytes;
-        }
-
-        /**
-         * Returns the number of records in the ZIP Central Directory. This value is taken from the
-         * ZIP End of Central Directory record.
-         */
-        public int getZipCentralDirectoryRecordCount() {
-            return mCentralDirectoryRecordCount;
-        }
-
-        /**
-         * Returns the start offset of the ZIP End of Central Directory record. The record extends
-         * until the very end of the APK.
-         */
-        public long getZipEndOfCentralDirectoryOffset() {
-            return mEocdOffset;
-        }
-
-        /**
-         * Returns the contents of the ZIP End of Central Directory.
-         */
-        public ByteBuffer getZipEndOfCentralDirectory() {
-            return mEocd;
+            super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
+                    eocdOffset, eocd);
         }
     }
 
@@ -169,85 +96,26 @@
         ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
     }
 
-    // See https://source.android.com/security/apksigning/v2.html
-    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
-    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
-    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
-
     /**
      * Returns the APK Signing Block of the provided APK.
      *
      * @throws IOException if an I/O error occurs
      * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
      *
-     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+     * </a>
      */
     public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
             throws IOException, ApkSigningBlockNotFoundException {
-        // FORMAT (see https://source.android.com/security/apksigning/v2.html):
-        // OFFSET       DATA TYPE  DESCRIPTION
-        // * @+0  bytes uint64:    size in bytes (excluding this field)
-        // * @+8  bytes payload
-        // * @-24 bytes uint64:    size in bytes (same as the one above)
-        // * @-16 bytes uint128:   magic
-
-        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
-        long centralDirEndOffset =
-                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
-        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
-        if (centralDirEndOffset != eocdStartOffset) {
-            throw new ApkSigningBlockNotFoundException(
-                    "ZIP Central Directory is not immediately followed by End of Central Directory"
-                            + ". CD end: " + centralDirEndOffset
-                            + ", EoCD start: " + eocdStartOffset);
-        }
-
-        if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
-            throw new ApkSigningBlockNotFoundException(
-                    "APK too small for APK Signing Block. ZIP Central Directory offset: "
-                            + centralDirStartOffset);
-        }
-        // Read the magic and offset in file from the footer section of the block:
-        // * uint64:   size of block
-        // * 16 bytes: magic
-        ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
-        footer.order(ByteOrder.LITTLE_ENDIAN);
-        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
-                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
-            throw new ApkSigningBlockNotFoundException(
-                    "No APK Signing Block before ZIP Central Directory");
-        }
-        // Read and compare size fields
-        long apkSigBlockSizeInFooter = footer.getLong(0);
-        if ((apkSigBlockSizeInFooter < footer.capacity())
-                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
-            throw new ApkSigningBlockNotFoundException(
-                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
-        }
-        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
-        long apkSigBlockOffset = centralDirStartOffset - totalSize;
-        if (apkSigBlockOffset < 0) {
-            throw new ApkSigningBlockNotFoundException(
-                    "APK Signing Block offset out of range: " + apkSigBlockOffset);
-        }
-        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
-        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
-        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
-        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
-            throw new ApkSigningBlockNotFoundException(
-                    "APK Signing Block sizes in header and footer do not match: "
-                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
-        }
-        return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+        ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
+                zipSections);
+        return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
     }
 
     /**
      * Information about the location of the APK Signing Block inside an APK.
      */
-    public static class ApkSigningBlock {
-        private final long mStartOffsetInApk;
-        private final DataSource mContents;
-
+    public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
         /**
          * Constructs a new {@code ApkSigningBlock}.
          *
@@ -256,23 +124,7 @@
          * @param contents contents of the APK Signing Block
          */
         public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
-            mStartOffsetInApk = startOffsetInApk;
-            mContents = contents;
-        }
-
-        /**
-         * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
-         */
-        public long getStartOffset() {
-            return mStartOffsetInApk;
-        }
-
-        /**
-         * Returns the data source which provides the full contents of the APK Signing Block,
-         * including its footer.
-         */
-        public DataSource getContents() {
-            return mContents;
+            super(startOffsetInApk, contents);
         }
     }
 
@@ -324,6 +176,30 @@
     private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
 
     /**
+     * Android resource ID of the {@code android:targetSandboxVersion} attribute in
+     * AndroidManifest.xml.
+     */
+    private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+
+    /**
+     * Android resource ID of the {@code android:targetSdkVersion} attribute in
+     * AndroidManifest.xml.
+     */
+    private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
+    private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
+
+    /**
+     * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
+     */
+    private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
+    private static final String MANIFEST_ELEMENT_TAG = "manifest";
+
+    /**
+     * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
+     */
+    private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
+
+    /**
      * Returns the lowest Android platform version (API Level) supported by an APK with the
      * provided {@code AndroidManifest.xml}.
      *
@@ -607,14 +483,156 @@
         }
     }
 
-    public static byte[] computeSha256DigestBytes(byte[] data) {
-        MessageDigest messageDigest;
+    /**
+     * Returns the security sandbox version targeted by an APK with the provided
+     * {@code AndroidManifest.xml}.
+     *
+     * <p>If the security sandbox version is not specified in the manifest a default value of 1 is
+     * returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     */
+    public static int getTargetSandboxVersionFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) {
         try {
-            messageDigest = MessageDigest.getInstance("SHA-256");
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException("SHA-256 is not found", e);
+            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // An ApkFormatException indicates the target sandbox is not specified in the manifest;
+            // return a default value of 1.
+            return 1;
         }
-        messageDigest.update(data);
-        return messageDigest.digest();
+    }
+
+    /**
+     * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
+     *
+     * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
+     * value is specified then a value of 1 is returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     */
+    public static int getTargetSdkVersionFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) {
+        // If the targetSdkVersion is not specified then the platform will use the value of the
+        // minSdkVersion; if neither is specified then the platform will use a value of 1.
+        int minSdkVersion = 1;
+        try {
+            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
+            // element is not specified at all.
+        }
+        androidManifestContents.rewind();
+        try {
+            minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
+        } catch (ApkFormatException e) {
+            // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
+            // the uses-sdk element is not specified at all.
+        }
+        return minSdkVersion;
+    }
+
+    /**
+     * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
+     *
+     * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
+     * integer an ApkFormatException is thrown.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     * @throws ApkFormatException if an error occurred while determining the versionCode, or if the
+     *                            versionCode attribute value is not available.
+     */
+    public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
+            throws ApkFormatException {
+        return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
+    }
+
+    /**
+     * Returns the versionCode and versionCodeMajor of the APK according to its {@code
+     * AndroidManifest.xml} combined together as a single long value.
+     *
+     * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
+     * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
+     *
+     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+     *                                resource format
+     * @throws ApkFormatException if an error occurred while determining the version, or if the
+     *                            versionCode attribute value is not available.
+     */
+    public static long getLongVersionCodeFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents) throws ApkFormatException {
+        // If the versionCode is not found then allow the ApkFormatException to be thrown to notify
+        // the caller that the versionCode is not available.
+        int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
+        long versionCodeMajor = 0;
+        try {
+            androidManifestContents.rewind();
+            versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+                    MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
+        } catch (ApkFormatException e) {
+            // This is expected if the versionCodeMajor has not been defined for the APK; in this
+            // case the return value is just the versionCode.
+        }
+        return (versionCodeMajor << 32) | versionCode;
+    }
+
+    /**
+     * Returns the integer value of the requested {@code attributeId} in the specified {@code
+     * elementName} from the provided {@code androidManifestContents} in binary Android resource
+     * format.
+     *
+     * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
+     *                            if the requested attribute is not found.
+     */
+    private static int getAttributeValueFromBinaryAndroidManifest(
+            ByteBuffer androidManifestContents, String elementName, int attributeId)
+            throws ApkFormatException {
+        if (elementName == null) {
+            throw new NullPointerException("elementName cannot be null");
+        }
+        try {
+            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+            int eventType = parser.getEventType();
+            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+                        && (elementName.equals(parser.getName()))) {
+                    for (int i = 0; i < parser.getAttributeCount(); i++) {
+                        if (parser.getAttributeNameResourceId(i) == attributeId) {
+                            int valueType = parser.getAttributeValueType(i);
+                            switch (valueType) {
+                                case AndroidBinXmlParser.VALUE_TYPE_INT:
+                                case AndroidBinXmlParser.VALUE_TYPE_STRING:
+                                    return parser.getAttributeIntValue(i);
+                                default:
+                                    throw new ApkFormatException(
+                                            "Unsupported value type, " + valueType
+                                                    + ", for attribute " + String.format("0x%08X",
+                                                    attributeId) + " under element " + elementName);
+
+                            }
+                        }
+                    }
+                }
+                eventType = parser.next();
+            }
+            throw new ApkFormatException(
+                    "Failed to determine APK's " + elementName + " attribute "
+                            + String.format("0x%08X", attributeId) + " value");
+        } catch (AndroidBinXmlParser.XmlParserException e) {
+            throw new ApkFormatException(
+                    "Unable to determine value for attribute " + String.format("0x%08X",
+                            attributeId) + " under element " + elementName
+                            + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+        }
+    }
+
+    public static byte[] computeSha256DigestBytes(byte[] data) {
+        return ApkUtilsLite.computeSha256DigestBytes(data);
     }
 }
diff --git a/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
new file mode 100644
index 0000000..13f2301
--- /dev/null
+++ b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
@@ -0,0 +1,199 @@
+/*
+ * 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.apk;
+
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Lightweight version of the ApkUtils for clients that only require a subset of the utility
+ * functionality.
+ */
+public class ApkUtilsLite {
+    private ApkUtilsLite() {}
+
+    /**
+     * Finds the main ZIP sections of the provided APK.
+     *
+     * @throws IOException if an I/O error occurred while reading the APK
+     * @throws ZipFormatException if the APK is malformed
+     */
+    public static ZipSections findZipSections(DataSource apk)
+            throws IOException, ZipFormatException {
+        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+                ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+        if (eocdAndOffsetInFile == null) {
+            throw new ZipFormatException("ZIP End of Central Directory record not found");
+        }
+
+        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+        long eocdOffset = eocdAndOffsetInFile.getSecond();
+        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
+        if (cdStartOffset > eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory start offset out of range: " + cdStartOffset
+                            + ". ZIP End of Central Directory offset: " + eocdOffset);
+        }
+
+        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
+        long cdEndOffset = cdStartOffset + cdSizeBytes;
+        if (cdEndOffset > eocdOffset) {
+            throw new ZipFormatException(
+                    "ZIP Central Directory overlaps with End of Central Directory"
+                            + ". CD end: " + cdEndOffset
+                            + ", EoCD start: " + eocdOffset);
+        }
+
+        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
+
+        return new ZipSections(
+                cdStartOffset,
+                cdSizeBytes,
+                cdRecordCount,
+                eocdOffset,
+                eocdBuf);
+    }
+
+    // See https://source.android.com/security/apksigning/v2.html
+    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+    /**
+     * Returns the APK Signing Block of the provided APK.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+     *
+     * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+     * </a>
+     */
+    public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+            throws IOException, ApkSigningBlockNotFoundException {
+        // FORMAT (see https://source.android.com/security/apksigning/v2.html):
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes payload
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+
+        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+        long centralDirEndOffset =
+                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+        if (centralDirEndOffset != eocdStartOffset) {
+            throw new ApkSigningBlockNotFoundException(
+                    "ZIP Central Directory is not immediately followed by End of Central Directory"
+                            + ". CD end: " + centralDirEndOffset
+                            + ", EoCD start: " + eocdStartOffset);
+        }
+
+        if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK too small for APK Signing Block. ZIP Central Directory offset: "
+                            + centralDirStartOffset);
+        }
+        // Read the magic and offset in file from the footer section of the block:
+        // * uint64:   size of block
+        // * 16 bytes: magic
+        ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
+        footer.order(ByteOrder.LITTLE_ENDIAN);
+        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+            throw new ApkSigningBlockNotFoundException(
+                    "No APK Signing Block before ZIP Central Directory");
+        }
+        // Read and compare size fields
+        long apkSigBlockSizeInFooter = footer.getLong(0);
+        if ((apkSigBlockSizeInFooter < footer.capacity())
+                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+        }
+        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+        long apkSigBlockOffset = centralDirStartOffset - totalSize;
+        if (apkSigBlockOffset < 0) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block offset out of range: " + apkSigBlockOffset);
+        }
+        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
+        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+            throw new ApkSigningBlockNotFoundException(
+                    "APK Signing Block sizes in header and footer do not match: "
+                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+        }
+        return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+    }
+
+    /**
+     * Information about the location of the APK Signing Block inside an APK.
+     */
+    public static class ApkSigningBlock {
+        private final long mStartOffsetInApk;
+        private final DataSource mContents;
+
+        /**
+         * Constructs a new {@code ApkSigningBlock}.
+         *
+         * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+         *        Signing Block inside the APK file
+         * @param contents contents of the APK Signing Block
+         */
+        public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+            mStartOffsetInApk = startOffsetInApk;
+            mContents = contents;
+        }
+
+        /**
+         * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
+         */
+        public long getStartOffset() {
+            return mStartOffsetInApk;
+        }
+
+        /**
+         * Returns the data source which provides the full contents of the APK Signing Block,
+         * including its footer.
+         */
+        public DataSource getContents() {
+            return mContents;
+        }
+    }
+
+    public 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();
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
new file mode 100644
index 0000000..6151351
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signature verification result.
+ */
+public class ApkSigResult {
+    public final int signatureSchemeVersion;
+
+    /** Whether the APK's Signature Scheme signature verifies. */
+    public boolean verified;
+
+    public final List<ApkSignerInfo> mSigners = new ArrayList<>();
+    private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+    private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+    public ApkSigResult(int signatureSchemeVersion) {
+        this.signatureSchemeVersion = signatureSchemeVersion;
+    }
+
+    /**
+     * Returns {@code true} if this result encountered errors during verification.
+     */
+    public boolean containsErrors() {
+        if (!mErrors.isEmpty()) {
+            return true;
+        }
+        if (!mSigners.isEmpty()) {
+            for (ApkSignerInfo signer : mSigners) {
+                if (signer.containsErrors()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if this result encountered warnings during verification.
+     */
+    public boolean containsWarnings() {
+        if (!mWarnings.isEmpty()) {
+            return true;
+        }
+        if (!mSigners.isEmpty()) {
+            for (ApkSignerInfo signer : mSigners) {
+                if (signer.containsWarnings()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addError(int issueId, Object... parameters) {
+        mErrors.add(new ApkVerificationIssue(issueId, parameters));
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addWarning(int issueId, Object... parameters) {
+        mWarnings.add(new ApkVerificationIssue(issueId, parameters));
+    }
+
+    /**
+     * Returns the errors encountered during verification.
+     */
+    public List<? extends ApkVerificationIssue> getErrors() {
+        return mErrors;
+    }
+
+    /**
+     * Returns the warnings encountered during verification.
+     */
+    public List<? extends ApkVerificationIssue> getWarnings() {
+        return mWarnings;
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
new file mode 100644
index 0000000..e0ea365
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signer.
+ */
+public class ApkSignerInfo {
+    public int index;
+    public List<X509Certificate> certs = new ArrayList<>();
+    public List<X509Certificate> certificateLineage = new ArrayList<>();
+
+    private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+    private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addError(int issueId, Object... params) {
+        mErrors.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
+     * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
+     * issueId} and {@code params}.
+     */
+    public void addWarning(int issueId, Object... params) {
+        mWarnings.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
+     * Returns {@code true} if any errors were encountered during verification for this signer.
+     */
+    public boolean containsErrors() {
+        return !mErrors.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if any warnings were encountered during verification for this signer.
+     */
+    public boolean containsWarnings() {
+        return !mWarnings.isEmpty();
+    }
+
+    /**
+     * Returns the errors encountered during verification for this signer.
+     */
+    public List<? extends ApkVerificationIssue> getErrors() {
+        return mErrors;
+    }
+
+    /**
+     * Returns the warnings encountered during verification for this signer.
+     */
+    public List<? extends ApkVerificationIssue> getWarnings() {
+        return mWarnings;
+    }
+}
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 f027525..e8f6fc0 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -23,7 +23,6 @@
 import com.android.apksig.ApkVerifier;
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.apk.ApkSigningBlockNotFoundException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.asn1.Asn1BerParser;
 import com.android.apksig.internal.asn1.Asn1DecodingException;
@@ -53,7 +52,6 @@
 
 import java.io.IOException;
 import java.math.BigInteger;
-import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.security.DigestException;
@@ -86,7 +84,6 @@
 
 public class ApkSigningBlockUtils {
 
-    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
     private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
     public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
     private static final byte[] APK_SIGNING_BLOCK_MAGIC =
@@ -110,58 +107,10 @@
      * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
      */
     public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
-        ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
-        ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
-        return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+        return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2);
     }
 
     /**
-     * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
-     * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
-     */
-    private static int compareContentDigestAlgorithm(
-            ContentDigestAlgorithm alg1,
-            ContentDigestAlgorithm alg2) {
-        switch (alg1) {
-            case CHUNKED_SHA256:
-                switch (alg2) {
-                    case CHUNKED_SHA256:
-                        return 0;
-                    case CHUNKED_SHA512:
-                    case VERITY_CHUNKED_SHA256:
-                        return -1;
-                    default:
-                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
-                }
-            case CHUNKED_SHA512:
-                switch (alg2) {
-                    case CHUNKED_SHA256:
-                    case VERITY_CHUNKED_SHA256:
-                        return 1;
-                    case CHUNKED_SHA512:
-                        return 0;
-                    default:
-                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
-                }
-            case VERITY_CHUNKED_SHA256:
-                switch (alg2) {
-                    case CHUNKED_SHA256:
-                        return 1;
-                    case VERITY_CHUNKED_SHA256:
-                        return 0;
-                    case CHUNKED_SHA512:
-                        return -1;
-                    default:
-                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
-                }
-            default:
-                throw new IllegalArgumentException("Unknown alg1: " + alg1);
-        }
-    }
-
-
-
-    /**
      * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
      * APK and comparing them against the digests listed in APK Signing Block. The expected digests
      * are taken from {@code SignerInfos} of the provided {@code result}.
@@ -279,155 +228,27 @@
             ByteBuffer apkSigningBlock,
             int blockId,
             Result result) throws SignatureNotFoundException {
-        checkByteOrderLittleEndian(apkSigningBlock);
-        // FORMAT:
-        // OFFSET       DATA TYPE  DESCRIPTION
-        // * @+0  bytes uint64:    size in bytes (excluding this field)
-        // * @+8  bytes pairs
-        // * @-24 bytes uint64:    size in bytes (same as the one above)
-        // * @-16 bytes uint128:   magic
-        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
-
-        int entryCount = 0;
-        while (pairs.hasRemaining()) {
-            entryCount++;
-            if (pairs.remaining() < 8) {
-                throw new SignatureNotFoundException(
-                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
-            }
-            long lenLong = pairs.getLong();
-            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
-                throw new SignatureNotFoundException(
-                        "APK Signing Block entry #" + entryCount
-                                + " size out of range: " + lenLong);
-            }
-            int len = (int) lenLong;
-            int nextEntryPos = pairs.position() + len;
-            if (len > pairs.remaining()) {
-                throw new SignatureNotFoundException(
-                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
-                                + ", available: " + pairs.remaining());
-            }
-            int id = pairs.getInt();
-            if (id == blockId) {
-                return getByteBuffer(pairs, len - 4);
-            }
-            pairs.position(nextEntryPos);
+        try {
+            return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
+        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage());
         }
-
-        throw new SignatureNotFoundException(
-                "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
     }
 
     public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
-        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
-            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
-        }
-    }
-
-    /**
-     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
-     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
-     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
-     * buffer's byte order.
-     */
-    private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
-        if (start < 0) {
-            throw new IllegalArgumentException("start: " + start);
-        }
-        if (end < start) {
-            throw new IllegalArgumentException("end < start: " + end + " < " + start);
-        }
-        int capacity = source.capacity();
-        if (end > source.capacity()) {
-            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
-        }
-        int originalLimit = source.limit();
-        int originalPosition = source.position();
-        try {
-            source.position(0);
-            source.limit(end);
-            source.position(start);
-            ByteBuffer result = source.slice();
-            result.order(source.order());
-            return result;
-        } finally {
-            source.position(0);
-            source.limit(originalLimit);
-            source.position(originalPosition);
-        }
-    }
-
-    /**
-     * Relative <em>get</em> method for reading {@code size} number of bytes from the current
-     * position of this buffer.
-     *
-     * <p>This method reads the next {@code size} bytes at this buffer's current position,
-     * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
-     * {@code size}, byte order set to this buffer's byte order; and then increments the position by
-     * {@code size}.
-     */
-    private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
-        if (size < 0) {
-            throw new IllegalArgumentException("size: " + size);
-        }
-        int originalLimit = source.limit();
-        int position = source.position();
-        int limit = position + size;
-        if ((limit < position) || (limit > originalLimit)) {
-            throw new BufferUnderflowException();
-        }
-        source.limit(limit);
-        try {
-            ByteBuffer result = source.slice();
-            result.order(source.order());
-            source.position(limit);
-            return result;
-        } finally {
-            source.limit(originalLimit);
-        }
+        ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
     }
 
     public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
-        if (source.remaining() < 4) {
-            throw new ApkFormatException(
-                    "Remaining buffer too short to contain length of length-prefixed field"
-                            + ". Remaining: " + source.remaining());
-        }
-        int len = source.getInt();
-        if (len < 0) {
-            throw new IllegalArgumentException("Negative length");
-        } else if (len > source.remaining()) {
-            throw new ApkFormatException(
-                    "Length-prefixed field longer than remaining buffer"
-                            + ". Field length: " + len + ", remaining: " + source.remaining());
-        }
-        return getByteBuffer(source, len);
+        return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source);
     }
 
     public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
-        int len = buf.getInt();
-        if (len < 0) {
-            throw new ApkFormatException("Negative length");
-        } else if (len > buf.remaining()) {
-            throw new ApkFormatException(
-                    "Underflow while reading length-prefixed value. Length: " + len
-                            + ", available: " + buf.remaining());
-        }
-        byte[] result = new byte[len];
-        buf.get(result);
-        return result;
+        return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf);
     }
 
     public static String toHex(byte[] value) {
-        StringBuilder sb = new StringBuilder(value.length * 2);
-        int len = value.length;
-        for (int i = 0; i < len; i++) {
-            int hi = (value[i] & 0xff) >>> 4;
-            int lo = value[i] & 0x0f;
-            sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
-        }
-        return sb.toString();
+        return ApkSigningBlockUtilsLite.toHex(value);
     }
 
     public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
@@ -946,20 +767,8 @@
 
     public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
             List<Pair<Integer, byte[]>> sequence) {
-          int resultSize = 0;
-          for (Pair<Integer, byte[]> element : sequence) {
-              resultSize += 12 + element.getSecond().length;
-          }
-          ByteBuffer result = ByteBuffer.allocate(resultSize);
-          result.order(ByteOrder.LITTLE_ENDIAN);
-          for (Pair<Integer, byte[]> element : sequence) {
-              byte[] second = element.getSecond();
-              result.putInt(8 + second.length);
-              result.putInt(element.getFirst());
-              result.putInt(second.length);
-              result.put(second);
-          }
-          return result.array();
+        return ApkSigningBlockUtilsLite
+                .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence);
       }
 
     /**
@@ -976,30 +785,11 @@
     public static SignatureInfo findSignature(
             DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)
                     throws IOException, SignatureNotFoundException {
-        // Find the APK Signing Block.
-        DataSource apkSigningBlock;
-        long apkSigningBlockOffset;
         try {
-            ApkUtils.ApkSigningBlock apkSigningBlockInfo =
-                    ApkUtils.findApkSigningBlock(apk, zipSections);
-            apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
-            apkSigningBlock = apkSigningBlockInfo.getContents();
-        } catch (ApkSigningBlockNotFoundException e) {
-            throw new SignatureNotFoundException(e.getMessage(), e);
+            return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
+        } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage());
         }
-        ByteBuffer apkSigningBlockBuf =
-                apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
-        apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
-
-        // Find the APK Signature Scheme Block inside the APK Signing Block.
-        ByteBuffer apkSignatureSchemeBlock =
-                findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result);
-        return new SignatureInfo(
-                apkSignatureSchemeBlock,
-                apkSigningBlockOffset,
-                zipSections.getZipCentralDirectoryOffset(),
-                zipSections.getZipEndOfCentralDirectoryOffset(),
-                zipSections.getZipEndOfCentralDirectory());
     }
 
     /**
@@ -1173,57 +963,39 @@
      * @throws NoSupportedSignaturesException if no supported signatures were
      *         found for an Android platform version in the range.
      */
-    public static List<SupportedSignature> getSignaturesToVerify(
-            List<SupportedSignature> signatures, int minSdkVersion, int maxSdkVersion)
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion)
             throws NoSupportedSignaturesException {
-        // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
-        // Android's behavior on those versions.
-        //
-        // Here we assume that, once introduced, a signature algorithm continues to be supported in
-        // all future Android versions. We also assume that the better-than relationship between
-        // algorithms is exactly the same on all Android platform versions (except that older
-        // platforms might support fewer algorithms). If these assumption are no longer true, the
-        // logic here will need to change accordingly.
-        Map<Integer, SupportedSignature> bestSigAlgorithmOnSdkVersion = new HashMap<>();
-        int minProvidedSignaturesVersion = Integer.MAX_VALUE;
-        for (SupportedSignature sig : signatures) {
-            SignatureAlgorithm sigAlgorithm = sig.algorithm;
-            int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion();
-            if (sigMinSdkVersion > maxSdkVersion) {
-                continue;
-            }
-            if (sigMinSdkVersion < minProvidedSignaturesVersion) {
-                minProvidedSignaturesVersion = sigMinSdkVersion;
-            }
-
-            SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
-            if ((candidate == null)
-                    || (compareSignatureAlgorithm(
-                            sigAlgorithm, candidate.algorithm) > 0)) {
-                bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
-            }
-        }
-
-        // Must have some supported signature algorithms for minSdkVersion.
-        if (minSdkVersion < minProvidedSignaturesVersion) {
-            throw new NoSupportedSignaturesException(
-                    "Minimum provided signature version " + minProvidedSignaturesVersion +
-                    " > minSdkVersion " + minSdkVersion);
-        }
-        if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
-            throw new NoSupportedSignaturesException("No supported signature");
-        }
-        List<SupportedSignature> signaturesToVerify =
-                new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
-        Collections.sort(
-                signaturesToVerify,
-                (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
-        return signaturesToVerify;
+        return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
     }
 
-    public static class NoSupportedSignaturesException extends Exception {
-        private static final long serialVersionUID = 1L;
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+     * signature within the signing block using the standard JCA.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion,
+            boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException {
+        try {
+            return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion,
+                    maxSdkVersion, onlyRequireJcaSupport);
+        } catch (NoApkSupportedSignaturesException e) {
+            throw new NoSupportedSignaturesException(e.getMessage());
+        }
+    }
 
+    public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException {
         public NoSupportedSignaturesException(String message) {
             super(message);
         }
@@ -1386,19 +1158,14 @@
         public SigningCertificateLineage mSigningCertificateLineage;
     }
 
-    public static class Result {
-        public final int signatureSchemeVersion;
-
-        /** Whether the APK's APK Signature Scheme signature verifies. */
-        public boolean verified;
-
-        public final List<Result.SignerInfo> signers = new ArrayList<>();
+    public static class Result extends ApkSigResult {
         public SigningCertificateLineage signingCertificateLineage = null;
+        public final List<Result.SignerInfo> signers = new ArrayList<>();
         private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
         private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
 
         public Result(int signatureSchemeVersion) {
-            this.signatureSchemeVersion = signatureSchemeVersion;
+            super(signatureSchemeVersion);
         }
 
         public boolean containsErrors() {
@@ -1437,17 +1204,17 @@
             mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
         }
 
+        @Override
         public List<ApkVerifier.IssueWithParams> getErrors() {
             return mErrors;
         }
 
+        @Override
         public List<ApkVerifier.IssueWithParams> getWarnings() {
             return mWarnings;
         }
 
-        public static class SignerInfo {
-            public int index;
-            public List<X509Certificate> certs = new ArrayList<>();
+        public static class SignerInfo extends ApkSignerInfo {
             public List<ContentDigest> contentDigests = new ArrayList<>();
             public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
             public List<Signature> signatures = new ArrayList<>();
@@ -1541,13 +1308,9 @@
         }
     }
 
-    public static class SupportedSignature {
-        public final SignatureAlgorithm algorithm;
-        public final byte[] signature;
-
+    public static class SupportedSignature extends ApkSupportedSignature {
         public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
-            this.algorithm = algorithm;
-            this.signature = signature;
+            super(algorithm, signature);
         }
     }
 
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
new file mode 100644
index 0000000..40ae947
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
@@ -0,0 +1,393 @@
+/*
+ * 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;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
+ * utility functionality.
+ */
+public class ApkSigningBlockUtilsLite {
+    private ApkSigningBlockUtilsLite() {}
+
+    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+    /**
+     * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+     * and the additional information relevant for verifying the block against the file.
+     *
+     * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+     *                identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+     *                block ID.
+     *
+     * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+     * @throws IOException if an I/O error occurs while reading the APK
+     */
+    public static SignatureInfo findSignature(
+            DataSource apk, ZipSections zipSections, int blockId)
+            throws IOException, SignatureNotFoundException {
+        // Find the APK Signing Block.
+        DataSource apkSigningBlock;
+        long apkSigningBlockOffset;
+        try {
+            ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
+                    ApkUtilsLite.findApkSigningBlock(apk, zipSections);
+            apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+            apkSigningBlock = apkSigningBlockInfo.getContents();
+        } catch (ApkSigningBlockNotFoundException e) {
+            throw new SignatureNotFoundException(e.getMessage(), e);
+        }
+        ByteBuffer apkSigningBlockBuf =
+                apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+        apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Find the APK Signature Scheme Block inside the APK Signing Block.
+        ByteBuffer apkSignatureSchemeBlock =
+                findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
+        return new SignatureInfo(
+                apkSignatureSchemeBlock,
+                apkSigningBlockOffset,
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectory());
+    }
+
+    public static ByteBuffer findApkSignatureSchemeBlock(
+            ByteBuffer apkSigningBlock,
+            int blockId) throws SignatureNotFoundException {
+        checkByteOrderLittleEndian(apkSigningBlock);
+        // FORMAT:
+        // OFFSET       DATA TYPE  DESCRIPTION
+        // * @+0  bytes uint64:    size in bytes (excluding this field)
+        // * @+8  bytes pairs
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+        int entryCount = 0;
+        while (pairs.hasRemaining()) {
+            entryCount++;
+            if (pairs.remaining() < 8) {
+                throw new SignatureNotFoundException(
+                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+            }
+            long lenLong = pairs.getLong();
+            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount
+                                + " size out of range: " + lenLong);
+            }
+            int len = (int) lenLong;
+            int nextEntryPos = pairs.position() + len;
+            if (len > pairs.remaining()) {
+                throw new SignatureNotFoundException(
+                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
+                                + ", available: " + pairs.remaining());
+            }
+            int id = pairs.getInt();
+            if (id == blockId) {
+                return getByteBuffer(pairs, len - 4);
+            }
+            pairs.position(nextEntryPos);
+        }
+
+        throw new SignatureNotFoundException(
+                "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
+    }
+
+    public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+        }
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoApkSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion)
+            throws NoApkSupportedSignaturesException {
+        return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+    }
+
+    /**
+     * Returns the subset of signatures which are expected to be verified by at least one Android
+     * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+     * guaranteed to contain at least one signature.
+     *
+     * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+     * signature within the signing block using the standard JCA.
+     *
+     * <p>Each Android platform version typically verifies exactly one signature from the provided
+     * {@code signatures} set. This method returns the set of these signatures collected over all
+     * requested platform versions. As a result, the result may contain more than one signature.
+     *
+     * @throws NoApkSupportedSignaturesException if no supported signatures were
+     *         found for an Android platform version in the range.
+     */
+    public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+            List<T> signatures, int minSdkVersion, int maxSdkVersion,
+            boolean onlyRequireJcaSupport) throws
+            NoApkSupportedSignaturesException {
+        // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
+        // Android's behavior on those versions.
+        //
+        // Here we assume that, once introduced, a signature algorithm continues to be supported in
+        // all future Android versions. We also assume that the better-than relationship between
+        // algorithms is exactly the same on all Android platform versions (except that older
+        // platforms might support fewer algorithms). If these assumption are no longer true, the
+        // logic here will need to change accordingly.
+        Map<Integer, T>
+                bestSigAlgorithmOnSdkVersion = new HashMap<>();
+        int minProvidedSignaturesVersion = Integer.MAX_VALUE;
+        for (T sig : signatures) {
+            SignatureAlgorithm sigAlgorithm = sig.algorithm;
+            int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
+                    : sigAlgorithm.getMinSdkVersion();
+            if (sigMinSdkVersion > maxSdkVersion) {
+                continue;
+            }
+            if (sigMinSdkVersion < minProvidedSignaturesVersion) {
+                minProvidedSignaturesVersion = sigMinSdkVersion;
+            }
+
+            T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
+            if ((candidate == null)
+                    || (compareSignatureAlgorithm(
+                    sigAlgorithm, candidate.algorithm) > 0)) {
+                bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
+            }
+        }
+
+        // Must have some supported signature algorithms for minSdkVersion.
+        if (minSdkVersion < minProvidedSignaturesVersion) {
+            throw new NoApkSupportedSignaturesException(
+                    "Minimum provided signature version " + minProvidedSignaturesVersion +
+                            " > minSdkVersion " + minSdkVersion);
+        }
+        if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
+            throw new NoApkSupportedSignaturesException("No supported signature");
+        }
+        List<T> signaturesToVerify =
+                new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+        Collections.sort(
+                signaturesToVerify,
+                (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+        return signaturesToVerify;
+    }
+
+    /**
+     * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+     * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+     */
+    public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+        ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+        ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+        return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+    }
+
+    /**
+     * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
+     * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
+     */
+    private static int compareContentDigestAlgorithm(
+            ContentDigestAlgorithm alg1,
+            ContentDigestAlgorithm alg2) {
+        switch (alg1) {
+            case CHUNKED_SHA256:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 0;
+                    case CHUNKED_SHA512:
+                    case VERITY_CHUNKED_SHA256:
+                        return -1;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            case CHUNKED_SHA512:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                    case VERITY_CHUNKED_SHA256:
+                        return 1;
+                    case CHUNKED_SHA512:
+                        return 0;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            case VERITY_CHUNKED_SHA256:
+                switch (alg2) {
+                    case CHUNKED_SHA256:
+                        return 1;
+                    case VERITY_CHUNKED_SHA256:
+                        return 0;
+                    case CHUNKED_SHA512:
+                        return -1;
+                    default:
+                        throw new IllegalArgumentException("Unknown alg2: " + alg2);
+                }
+            default:
+                throw new IllegalArgumentException("Unknown alg1: " + alg1);
+        }
+    }
+
+    /**
+     * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+     * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+     * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+     * buffer's byte order.
+     */
+    private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+        if (start < 0) {
+            throw new IllegalArgumentException("start: " + start);
+        }
+        if (end < start) {
+            throw new IllegalArgumentException("end < start: " + end + " < " + start);
+        }
+        int capacity = source.capacity();
+        if (end > source.capacity()) {
+            throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+        }
+        int originalLimit = source.limit();
+        int originalPosition = source.position();
+        try {
+            source.position(0);
+            source.limit(end);
+            source.position(start);
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            return result;
+        } finally {
+            source.position(0);
+            source.limit(originalLimit);
+            source.position(originalPosition);
+        }
+    }
+
+    /**
+     * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+     * position of this buffer.
+     *
+     * <p>This method reads the next {@code size} bytes at this buffer's current position,
+     * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+     * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+     * {@code size}.
+     */
+    private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
+        if (size < 0) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        int originalLimit = source.limit();
+        int position = source.position();
+        int limit = position + size;
+        if ((limit < position) || (limit > originalLimit)) {
+            throw new BufferUnderflowException();
+        }
+        source.limit(limit);
+        try {
+            ByteBuffer result = source.slice();
+            result.order(source.order());
+            source.position(limit);
+            return result;
+        } finally {
+            source.limit(originalLimit);
+        }
+    }
+
+    public static String toHex(byte[] value) {
+        StringBuilder sb = new StringBuilder(value.length * 2);
+        int len = value.length;
+        for (int i = 0; i < len; i++) {
+            int hi = (value[i] & 0xff) >>> 4;
+            int lo = value[i] & 0x0f;
+            sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+        }
+        return sb.toString();
+    }
+
+    public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+        if (source.remaining() < 4) {
+            throw new ApkFormatException(
+                    "Remaining buffer too short to contain length of length-prefixed field"
+                            + ". Remaining: " + source.remaining());
+        }
+        int len = source.getInt();
+        if (len < 0) {
+            throw new IllegalArgumentException("Negative length");
+        } else if (len > source.remaining()) {
+            throw new ApkFormatException(
+                    "Length-prefixed field longer than remaining buffer"
+                            + ". Field length: " + len + ", remaining: " + source.remaining());
+        }
+        return getByteBuffer(source, len);
+    }
+
+    public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+        int len = buf.getInt();
+        if (len < 0) {
+            throw new ApkFormatException("Negative length");
+        } else if (len > buf.remaining()) {
+            throw new ApkFormatException(
+                    "Underflow while reading length-prefixed value. Length: " + len
+                            + ", available: " + buf.remaining());
+        }
+        byte[] result = new byte[len];
+        buf.get(result);
+        return result;
+    }
+
+    public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+            List<Pair<Integer, byte[]>> sequence) {
+        int resultSize = 0;
+        for (Pair<Integer, byte[]> element : sequence) {
+            resultSize += 12 + element.getSecond().length;
+        }
+        ByteBuffer result = ByteBuffer.allocate(resultSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        for (Pair<Integer, byte[]> element : sequence) {
+            byte[] second = element.getSecond();
+            result.putInt(8 + second.length);
+            result.putInt(element.getFirst());
+            result.putInt(second.length);
+            result.put(second);
+        }
+        return result.array();
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
new file mode 100644
index 0000000..61652a4
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Base implementation of a supported signature for an APK.
+ */
+public class ApkSupportedSignature {
+    public final SignatureAlgorithm algorithm;
+    public final byte[] signature;
+
+    /**
+     * Constructs a new supported signature using the provided {@code algorithm} and {@code
+     * signature} bytes.
+     */
+    public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+        this.algorithm = algorithm;
+        this.signature = signature;
+    }
+
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
new file mode 100644
index 0000000..52c6085
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * Base exception that is thrown when there are no signatures that support the full range of
+ * requested platform versions.
+ */
+public class NoApkSupportedSignaturesException extends Exception {
+    public NoApkSupportedSignaturesException(String message) {
+        super(message);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
index 0db8cb8..d54f1e0 100644
--- a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
@@ -38,7 +38,8 @@
             Pair.of("SHA256withRSA/PSS",
                     new PSSParameterSpec(
                             "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.M),
 
     /**
      * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
@@ -52,7 +53,8 @@
                     "SHA512withRSA/PSS",
                     new PSSParameterSpec(
                             "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.M),
 
     /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
     RSA_PKCS1_V1_5_WITH_SHA256(
@@ -60,7 +62,8 @@
             ContentDigestAlgorithm.CHUNKED_SHA256,
             "RSA",
             Pair.of("SHA256withRSA", null),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
 
     /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
     RSA_PKCS1_V1_5_WITH_SHA512(
@@ -68,7 +71,8 @@
             ContentDigestAlgorithm.CHUNKED_SHA512,
             "RSA",
             Pair.of("SHA512withRSA", null),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
 
     /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
     ECDSA_WITH_SHA256(
@@ -76,7 +80,8 @@
             ContentDigestAlgorithm.CHUNKED_SHA256,
             "EC",
             Pair.of("SHA256withECDSA", null),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.HONEYCOMB),
 
     /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
     ECDSA_WITH_SHA512(
@@ -84,7 +89,8 @@
             ContentDigestAlgorithm.CHUNKED_SHA512,
             "EC",
             Pair.of("SHA512withECDSA", null),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.HONEYCOMB),
 
     /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
     DSA_WITH_SHA256(
@@ -92,7 +98,8 @@
             ContentDigestAlgorithm.CHUNKED_SHA256,
             "DSA",
             Pair.of("SHA256withDSA", null),
-            AndroidSdkVersion.N),
+            AndroidSdkVersion.N,
+            AndroidSdkVersion.INITIAL_RELEASE),
 
     /**
      * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
@@ -104,7 +111,8 @@
             ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
             "RSA",
             Pair.of("SHA256withRSA", null),
-            AndroidSdkVersion.P),
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.INITIAL_RELEASE),
 
     /**
      * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
@@ -116,7 +124,8 @@
             ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
             "EC",
             Pair.of("SHA256withECDSA", null),
-            AndroidSdkVersion.P),
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.HONEYCOMB),
 
     /**
      * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
@@ -128,24 +137,28 @@
             ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
             "DSA",
             Pair.of("SHA256withDSA", null),
-            AndroidSdkVersion.P);
+            AndroidSdkVersion.P,
+            AndroidSdkVersion.INITIAL_RELEASE);
 
     private final int mId;
     private final String mJcaKeyAlgorithm;
     private final ContentDigestAlgorithm mContentDigestAlgorithm;
     private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
     private final int mMinSdkVersion;
+    private final int mJcaSigAlgMinSdkVersion;
 
     SignatureAlgorithm(int id,
             ContentDigestAlgorithm contentDigestAlgorithm,
             String jcaKeyAlgorithm,
             Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
-            int minSdkVersion) {
+            int minSdkVersion,
+            int jcaSigAlgMinSdkVersion) {
         mId = id;
         mContentDigestAlgorithm = contentDigestAlgorithm;
         mJcaKeyAlgorithm = jcaKeyAlgorithm;
         mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
         mMinSdkVersion = minSdkVersion;
+        mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
     }
 
     /**
@@ -181,6 +194,13 @@
         return mMinSdkVersion;
     }
 
+    /**
+     * Returns the minimum SDK version that supports the JCA signature algorithm.
+     */
+    public int getJcaSigAlgMinSdkVersion() {
+        return mJcaSigAlgMinSdkVersion;
+    }
+
     public static SignatureAlgorithm findById(int id) {
         for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
             if (alg.getId() == id) {
diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
new file mode 100644
index 0000000..95f06ef
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Base exception that is thrown when the APK is not signed with the requested signature scheme.
+ */
+public class SignatureNotFoundException extends Exception {
+    public SignatureNotFoundException(String message) {
+        super(message);
+    }
+
+    public SignatureNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
new file mode 100644
index 0000000..93627ff
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
@@ -0,0 +1,235 @@
+/*
+ * 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.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
+public class SourceStampCertificateLineage {
+
+    private final static int FIRST_VERSION = 1;
+    private final static int CURRENT_VERSION = FIRST_VERSION;
+
+    /**
+     * Deserializes the binary representation of a SourceStampCertificateLineage. Also
+     * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+     * parent.
+     */
+    public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+            throws IOException {
+        List<SigningCertificateNode> result = new ArrayList<>();
+        int nodeCount = 0;
+        if (inputBytes == null || !inputBytes.hasRemaining()) {
+            return null;
+        }
+
+        ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
+
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance("X.509");
+        } catch (CertificateException e) {
+            throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+        }
+
+        // FORMAT (little endian):
+        // * uint32: version code
+        // * sequence of length-prefixed (uint32): nodes
+        //   * length-prefixed bytes: signed data
+        //     * length-prefixed bytes: certificate
+        //     * uint32: signature algorithm id
+        //   * uint32: flags
+        //   * uint32: signature algorithm id (used by to sign next cert in lineage)
+        //   * length-prefixed bytes: signature over above signed data
+
+        X509Certificate lastCert = null;
+        int lastSigAlgorithmId = 0;
+
+        try {
+            int version = inputBytes.getInt();
+            if (version != CURRENT_VERSION) {
+                // we only have one version to worry about right now, so just check it
+                throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+                        + " different than any of which we are aware");
+            }
+            HashSet<X509Certificate> certHistorySet = new HashSet<>();
+            while (inputBytes.hasRemaining()) {
+                nodeCount++;
+                ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+                ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+                int flags = nodeBytes.getInt();
+                int sigAlgorithmId = nodeBytes.getInt();
+                SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+                byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+                if (lastCert != null) {
+                    // Use previous level cert to verify current level
+                    String jcaSignatureAlgorithm =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+                    AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                            sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+                    PublicKey publicKey = lastCert.getPublicKey();
+                    Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                    sig.initVerify(publicKey);
+                    if (jcaSignatureAlgorithmParams != null) {
+                        sig.setParameter(jcaSignatureAlgorithmParams);
+                    }
+                    sig.update(signedData);
+                    if (!sig.verify(signature)) {
+                        throw new SecurityException("Unable to verify signature of certificate #"
+                                + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+                                + " SourceStampCertificateLineage object");
+                    }
+                }
+
+                signedData.rewind();
+                byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+                int signedSigAlgorithm = signedData.getInt();
+                if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+                    throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+                            + nodeBytes + " when verifying SourceStampCertificateLineage object");
+                }
+                lastCert = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(encodedCert));
+                lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+                if (certHistorySet.contains(lastCert)) {
+                    throw new SecurityException("Encountered duplicate entries in "
+                            + "SigningCertificateLineage at certificate #" + nodeCount + ".  All "
+                            + "signing certificates should be unique");
+                }
+                certHistorySet.add(lastCert);
+                lastSigAlgorithmId = sigAlgorithmId;
+                result.add(new SigningCertificateNode(
+                        lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+                        SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+            }
+        } catch(ApkFormatException | BufferUnderflowException e){
+            throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
+        } catch(NoSuchAlgorithmException | InvalidKeyException
+                | InvalidAlgorithmParameterException | SignatureException e){
+            throw new SecurityException(
+                    "Failed to verify signature over signed data for certificate #" + nodeCount
+                            + " when parsing SourceStampCertificateLineage object", e);
+        } catch(CertificateException e){
+            throw new SecurityException("Failed to decode certificate #" + nodeCount
+                    + " when parsing SourceStampCertificateLineage object", e);
+        }
+        return result;
+    }
+
+    /**
+     * Represents one signing certificate in the SourceStampCertificateLineage, which
+     * generally means it is/was used at some point to sign source stamps.
+     */
+    public static class SigningCertificateNode {
+
+        public SigningCertificateNode(
+                X509Certificate signingCert,
+                SignatureAlgorithm parentSigAlgorithm,
+                SignatureAlgorithm sigAlgorithm,
+                byte[] signature,
+                int flags) {
+            this.signingCert = signingCert;
+            this.parentSigAlgorithm = parentSigAlgorithm;
+            this.sigAlgorithm = sigAlgorithm;
+            this.signature = signature;
+            this.flags = flags;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof SigningCertificateNode)) return false;
+
+            SigningCertificateNode that = (SigningCertificateNode) o;
+            if (!signingCert.equals(that.signingCert)) return false;
+            if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+            if (sigAlgorithm != that.sigAlgorithm) return false;
+            if (!Arrays.equals(signature, that.signature)) return false;
+            if (flags != that.flags) return false;
+
+            // we made it
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
+            result = prime * result +
+                ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
+            result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
+            result = prime * result + Arrays.hashCode(signature);
+            result = prime * result + flags;
+            return result;
+        }
+
+        /**
+         * the signing cert for this node.  This is part of the data signed by the parent node.
+         */
+        public final X509Certificate signingCert;
+
+        /**
+         * the algorithm used by this node's parent to bless this data.  Its ID value is part of
+         * the data signed by the parent node. {@code null} for first node.
+         */
+        public final SignatureAlgorithm parentSigAlgorithm;
+
+        /**
+         * the algorithm used by this node to bless the next node's data.  Its ID value is part
+         * of the signed data of the next node. {@code null} for the last node.
+         */
+        public SignatureAlgorithm sigAlgorithm;
+
+        /**
+         * signature over the signed data (above).  The signature is from this node's parent
+         * signing certificate, which should correspond to the signing certificate used to sign an
+         * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+         */
+        public final byte[] signature;
+
+        /**
+         * the flags detailing how the platform should treat this signing cert
+         */
+        public int flags;
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
new file mode 100644
index 0000000..465fbb0
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/** Constants used for source stamp signing and verification. */
+public class SourceStampConstants {
+    private SourceStampConstants() {}
+
+    public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+    public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+    public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
+}
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
index 2f4c3ba..b4ae71a 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -15,15 +15,25 @@
  */
 package com.android.apksig.internal.apk.stamp;
 
-import com.android.apksig.ApkVerifier;
-import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.internal.apk.ApkSigningBlockUtils;
-import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
-import com.android.apksig.internal.util.X509CertificateUtils;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
 
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSupportedSignature;
+import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+
+import java.io.ByteArrayInputStream;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
@@ -53,7 +63,8 @@
  */
 class SourceStampVerifier {
     /** Hidden constructor to prevent instantiation. */
-    private SourceStampVerifier() {}
+    private SourceStampVerifier() {
+    }
 
     /**
      * Parses the SourceStamp block and populates the {@code result}.
@@ -67,7 +78,7 @@
     public static void verifyV1SourceStamp(
             ByteBuffer sourceStampBlockData,
             CertificateFactory certFactory,
-            ApkSigningBlockUtils.Result.SignerInfo result,
+            ApkSignerInfo result,
             byte[] apkDigest,
             byte[] sourceStampCertificateDigest,
             int minSdkVersion,
@@ -80,12 +91,13 @@
             return;
         }
 
+        ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
         verifySourceStampSignature(
                 apkDigest,
                 minSdkVersion,
                 maxSdkVersion,
                 sourceStampCertificate,
-                sourceStampBlockData,
+                apkDigestSignatures,
                 result);
     }
 
@@ -101,7 +113,7 @@
     public static void verifyV2SourceStamp(
             ByteBuffer sourceStampBlockData,
             CertificateFactory certFactory,
-            ApkSigningBlockUtils.Result.SignerInfo result,
+            ApkSignerInfo result,
             Map<Integer, byte[]> signatureSchemeApkDigests,
             byte[] sourceStampCertificateDigest,
             int minSdkVersion,
@@ -115,20 +127,19 @@
         }
 
         // Parse signed signature schemes block.
-        ByteBuffer signedSignatureSchemes =
-                ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+        ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
         Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
         while (signedSignatureSchemes.hasRemaining()) {
-            ByteBuffer signedSignatureScheme =
-                    ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
+            ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
             int signatureSchemeId = signedSignatureScheme.getInt();
-            signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
+            ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
+            signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
         }
 
         for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
                 signatureSchemeApkDigests.entrySet()) {
             if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
-                result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
                 return;
             }
             verifySourceStampSignature(
@@ -138,28 +149,43 @@
                     sourceStampCertificate,
                     signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
                     result);
-            if (result.containsWarnings() || result.containsWarnings()) {
+            if (result.containsWarnings() || result.containsErrors()) {
                 return;
             }
         }
+
+        if (sourceStampBlockData.hasRemaining()) {
+            // The stamp block contains some additional attributes.
+            ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
+            ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+
+            byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
+            stampAttributeData.get(stampAttributeBytes);
+            stampAttributeData.flip();
+
+            verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
+                    sourceStampCertificate, stampAttributeDataSignatures, result);
+            if (result.containsErrors() || result.containsWarnings()) {
+                return;
+            }
+            parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
+        }
     }
 
     private static X509Certificate verifySourceStampCertificate(
             ByteBuffer sourceStampBlockData,
             CertificateFactory certFactory,
             byte[] sourceStampCertificateDigest,
-            ApkSigningBlockUtils.Result.SignerInfo result)
+            ApkSignerInfo result)
             throws NoSuchAlgorithmException, ApkFormatException {
         // Parse the SourceStamp certificate.
-        byte[] sourceStampEncodedCertificate =
-                ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+        byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
         X509Certificate sourceStampCertificate;
         try {
-            sourceStampCertificate =
-                    X509CertificateUtils.generateCertificate(
-                            sourceStampEncodedCertificate, certFactory);
+            sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
+                    new ByteArrayInputStream(sourceStampEncodedCertificate));
         } catch (CertificateException e) {
-            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
             return null;
         }
         // Wrap the cert so that the result's getEncoded returns exactly the original encoded
@@ -177,62 +203,71 @@
         byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
         if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
             result.addWarning(
-                    ApkVerifier.Issue
+                    ApkVerificationIssue
                             .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
-                    ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
-                    ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
+                    toHex(sourceStampBlockCertificateDigest),
+                    toHex(sourceStampCertificateDigest));
             return null;
         }
         return sourceStampCertificate;
     }
 
     private static void verifySourceStampSignature(
-            byte[] apkDigest,
+            byte[] data,
             int minSdkVersion,
             int maxSdkVersion,
             X509Certificate sourceStampCertificate,
-            ByteBuffer signedData,
-            ApkSigningBlockUtils.Result.SignerInfo result)
-            throws ApkFormatException {
+            ByteBuffer signatures,
+            ApkSignerInfo result) {
         // Parse the signatures block and identify supported signatures
-        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
         int signatureCount = 0;
-        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
         while (signatures.hasRemaining()) {
             signatureCount++;
             try {
-                ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+                ByteBuffer signature = getLengthPrefixedSlice(signatures);
                 int sigAlgorithmId = signature.getInt();
-                byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+                byte[] sigBytes = readLengthPrefixedByteArray(signature);
                 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
                 if (signatureAlgorithm == null) {
                     result.addWarning(
-                            ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                            ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+                            sigAlgorithmId);
                     continue;
                 }
                 supportedSignatures.add(
-                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+                        new ApkSupportedSignature(signatureAlgorithm, sigBytes));
             } catch (ApkFormatException | BufferUnderflowException e) {
                 result.addWarning(
-                        ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+                        ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
                 return;
             }
         }
         if (supportedSignatures.isEmpty()) {
-            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
             return;
         }
         // Verify signatures over digests using the SourceStamp's certificate.
-        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+        List<ApkSupportedSignature> signaturesToVerify;
         try {
             signaturesToVerify =
-                    ApkSigningBlockUtils.getSignaturesToVerify(
-                            supportedSignatures, minSdkVersion, maxSdkVersion);
-        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
-            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+                    getSignaturesToVerify(
+                            supportedSignatures, minSdkVersion, maxSdkVersion, true);
+        } catch (NoApkSupportedSignaturesException e) {
+            // To facilitate debugging capture the signature algorithms and resulting exception in
+            // the warning.
+            StringBuilder signatureAlgorithms = new StringBuilder();
+            for (ApkSupportedSignature supportedSignature : supportedSignatures) {
+                if (signatureAlgorithms.length() > 0) {
+                    signatureAlgorithms.append(", ");
+                }
+                signatureAlgorithms.append(supportedSignature.algorithm);
+            }
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+                    signatureAlgorithms.toString(), e);
             return;
         }
-        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+        for (ApkSupportedSignature signature : signaturesToVerify) {
             SignatureAlgorithm signatureAlgorithm = signature.algorithm;
             String jcaSignatureAlgorithm =
                     signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
@@ -245,11 +280,11 @@
                 if (jcaSignatureAlgorithmParams != null) {
                     sig.setParameter(jcaSignatureAlgorithmParams);
                 }
-                sig.update(apkDigest);
+                sig.update(data);
                 byte[] sigBytes = signature.signature;
                 if (!sig.verify(sigBytes)) {
                     result.addWarning(
-                            ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+                            ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
                     return;
                 }
             } catch (InvalidKeyException
@@ -257,9 +292,57 @@
                     | SignatureException
                     | NoSuchAlgorithmException e) {
                 result.addWarning(
-                        ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                        ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
                 return;
             }
         }
     }
+
+    private static void parseStampAttributes(ByteBuffer stampAttributeData,
+            X509Certificate sourceStampCertificate, ApkSignerInfo result)
+            throws ApkFormatException {
+        ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
+        int stampAttributeCount = 0;
+        while (stampAttributes.hasRemaining()) {
+            stampAttributeCount++;
+            try {
+                ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
+                int id = attribute.getInt();
+                byte[] value = ByteBufferUtils.toByteArray(attribute);
+                if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
+                    readStampCertificateLineage(value, sourceStampCertificate, result);
+                } else {
+                    result.addWarning(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
+                }
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+                        stampAttributeCount);
+                return;
+            }
+        }
+    }
+
+    private static void readStampCertificateLineage(byte[] lineageBytes,
+            X509Certificate sourceStampCertificate, ApkSignerInfo result) {
+        try {
+            // SourceStampCertificateLineage is verified when built
+            List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
+                    SourceStampCertificateLineage.readSigningCertificateLineage(
+                            ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
+            for (int i = 0; i < nodes.size(); i++) {
+                result.certificateLineage.add(nodes.get(i).signingCert);
+            }
+            // Make sure that the last cert in the chain matches this signer cert
+            if (!sourceStampCertificate.equals(
+                    result.certificateLineage.get(result.certificateLineage.size() - 1))) {
+                result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+            }
+        } catch (SecurityException e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+        } catch (IllegalArgumentException e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+        } catch (Exception e) {
+            result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
+        }
+    }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
index dacd0be..dee24bd 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -48,8 +48,8 @@
  * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
  */
 public abstract class V1SourceStampSigner {
-
-    public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+    public static final int V1_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
 
     /** Hidden constructor to prevent instantiation. */
     private V1SourceStampSigner() {}
@@ -98,8 +98,8 @@
 
         // FORMAT:
         // * length-prefixed stamp block.
-        return Pair.of(
-                encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_SOURCE_STAMP_BLOCK_ID);
+        return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+                SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
     }
 
     private static final class SourceStampBlock {
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
index 8a3e776..c3fdeec 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -16,7 +16,7 @@
 package com.android.apksig.internal.apk.stamp;
 
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
-import static com.android.apksig.internal.apk.stamp.V1SourceStampSigner.V1_SOURCE_STAMP_BLOCK_ID;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
 
 import com.android.apksig.ApkVerifier;
 import com.android.apksig.apk.ApkFormatException;
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
index 16062bf..1c1570a 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -23,17 +23,22 @@
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
 
+import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.util.Pair;
 
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -50,11 +55,12 @@
  * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
  */
 public abstract class V2SourceStampSigner {
-
-    public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+    public static final int V2_SOURCE_STAMP_BLOCK_ID =
+            SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
 
     /** Hidden constructor to prevent instantiation. */
-    private V2SourceStampSigner() {}
+    private V2SourceStampSigner() {
+    }
 
     public static Pair<byte[], Integer> generateSourceStampBlock(
             SignerConfig sourceStampSignerConfig,
@@ -81,7 +87,7 @@
                 signatureSchemeDigestInfos,
                 sourceStampSignerConfig,
                 signatureSchemeDigests);
-        signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst));
+        Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
 
         SourceStampBlock sourceStampBlock = new SourceStampBlock();
 
@@ -95,23 +101,36 @@
 
         sourceStampBlock.signedDigests = signatureSchemeDigests;
 
+        sourceStampBlock.stampAttributes = encodeStampAttributes(
+                generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage));
+        sourceStampBlock.signedStampAttributes =
+                ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig,
+                        sourceStampBlock.stampAttributes);
+
         // FORMAT:
         // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
         // * length-prefixed sequence of length-prefixed signed signature scheme digests:
         //   * uint32: signature scheme id
         //   * length-prefixed bytes: signed digests for the respective signature scheme
+        // * length-prefixed bytes: encoded stamp attributes
+        // * length-prefixed sequence of length-prefixed signed stamp attributes:
+        //   * uint32: signature algorithm id
+        //   * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
         byte[] sourceStampSignerBlock =
                 encodeAsSequenceOfLengthPrefixedElements(
-                        new byte[][] {
-                            sourceStampBlock.stampCertificate,
-                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
-                                    sourceStampBlock.signedDigests),
+                        new byte[][]{
+                                sourceStampBlock.stampCertificate,
+                                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                        sourceStampBlock.signedDigests),
+                                sourceStampBlock.stampAttributes,
+                                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                        sourceStampBlock.signedStampAttributes),
                         });
 
         // FORMAT:
         // * length-prefixed stamp block.
-        return Pair.of(
-                encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID);
+        return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+                SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
     }
 
     private static void getSignedDigestsFor(
@@ -130,7 +149,7 @@
         for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
             digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
         }
-        digests.sort(Comparator.comparing(Pair::getFirst));
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
 
         // FORMAT:
         // * length-prefixed sequence of length-prefixed digests:
@@ -158,8 +177,43 @@
                                 signedDigest)));
     }
 
+    private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
+        int payloadSize = 0;
+        for (byte[] attributeValue : stampAttributes.values()) {
+            // Pair size + Attribute ID + Attribute value
+            payloadSize += 4 + 4 + attributeValue.length;
+        }
+
+        // FORMAT (little endian):
+        // * length-prefixed bytes: pair
+        //   * uint32: ID
+        //   * bytes: value
+        ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(payloadSize);
+        for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
+            // Pair size
+            result.putInt(4 + stampAttribute.getValue().length);
+            result.putInt(stampAttribute.getKey());
+            result.put(stampAttribute.getValue());
+        }
+        return result.array();
+    }
+
+    private static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+        HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
+        if (lineage != null) {
+            stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
+                    lineage.encodeSigningCertificateLineage());
+        }
+        return stampAttributes;
+    }
+
     private static final class SourceStampBlock {
         public byte[] stampCertificate;
         public List<Pair<Integer, byte[]>> signedDigests;
+        // Optional stamp attributes that are not required for verification.
+        public byte[] stampAttributes;
+        public List<Pair<Integer, byte[]>> signedStampAttributes;
     }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
index 8a776fc..5ba3618 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -16,17 +16,21 @@
 
 package com.android.apksig.internal.apk.stamp;
 
-import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
-import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
 
-import com.android.apksig.ApkVerifier;
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
 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.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
 
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
@@ -53,30 +57,29 @@
 
     /**
      * 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
+     * The APK must be considered verified only if {@link ApkSigResult#verified} is
      * {@code true}. If verification fails, the result will contain errors -- see {@link
-     * ApkSigningBlockUtils.Result#getErrors()}.
+     * ApkSigResult#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
+     * @throws SignatureNotFoundException if no SourceStamp signatures are
      *     found
      * @throws IOException if an I/O error occurs when reading the APK
      */
-    public static ApkSigningBlockUtils.Result verify(
+    public static ApkSigResult verify(
             DataSource apk,
-            ApkUtils.ZipSections zipSections,
+            ZipSections zipSections,
             byte[] sourceStampCertificateDigest,
             Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
             int minSdkVersion,
             int maxSdkVersion)
-            throws IOException, NoSuchAlgorithmException,
-                    ApkSigningBlockUtils.SignatureNotFoundException {
-        ApkSigningBlockUtils.Result result =
-                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        ApkSigResult result =
+                new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
         SignatureInfo signatureInfo =
-                ApkSigningBlockUtils.findSignature(
-                        apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result);
+                ApkSigningBlockUtilsLite.findSignature(
+                        apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
 
         verify(
                 signatureInfo.signatureBlock,
@@ -91,7 +94,7 @@
     /**
      * 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
+     * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
      * more information about the contract of this method.
      */
     private static void verify(
@@ -100,15 +103,14 @@
             Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
             int minSdkVersion,
             int maxSdkVersion,
-            ApkSigningBlockUtils.Result result)
+            ApkSigResult result)
             throws NoSuchAlgorithmException {
-        ApkSigningBlockUtils.Result.SignerInfo signerInfo =
-                new ApkSigningBlockUtils.Result.SignerInfo();
-        result.signers.add(signerInfo);
+        ApkSignerInfo signerInfo = new ApkSignerInfo();
+        result.mSigners.add(signerInfo);
         try {
             CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
             ByteBuffer sourceStampBlockData =
-                    ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+                    ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
             SourceStampVerifier.verifyV2SourceStamp(
                     sourceStampBlockData,
                     certFactory,
@@ -121,7 +123,7 @@
         } catch (CertificateException e) {
             throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
         } catch (ApkFormatException | BufferUnderflowException e) {
-            signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+            signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
         }
     }
 
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
new file mode 100644
index 0000000..db1d15f
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
@@ -0,0 +1,26 @@
+/*
+ * 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.v1;
+
+/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
+public class V1SchemeConstants {
+    private V1SchemeConstants() {}
+
+    public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+    public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
+            "X-Android-APK-Signed";
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
index a83608a..30aaee5 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
@@ -60,17 +60,15 @@
  * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
  */
 public abstract class V1SchemeSigner {
-
-    public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+    public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
 
     private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
             new Attributes.Name("Created-By");
     private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
     private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
 
-    static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
     private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
-            new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+            new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
 
     /**
      * Signer configuration.
@@ -309,7 +307,7 @@
             signatureJarEntries.add(
                     Pair.of(signatureBlockFileName, signatureBlock));
         }
-        signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
+        signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
         return signatureJarEntries;
     }
 
@@ -327,7 +325,7 @@
                             + publicKey.getAlgorithm().toUpperCase(Locale.US);
             result.add(signatureBlockFileName);
         }
-        result.add(MANIFEST_ENTRY_NAME);
+        result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
         return result;
     }
 
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index a2a5be6..d3caeff 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -47,13 +47,13 @@
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
 import com.android.apksig.util.DataSinks;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.zip.ZipFormatException;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -83,9 +83,6 @@
  * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
  */
 public abstract class V1SchemeVerifier {
-
-    private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME;
-
     private V1SchemeVerifier() {}
 
     /**
@@ -232,7 +229,8 @@
                 if (!entryName.startsWith("META-INF/")) {
                     continue;
                 }
-                if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) {
+                if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(
+                        entryName))) {
                     manifestEntry = cdRecord;
                     continue;
                 }
@@ -945,7 +943,7 @@
                 if (!Arrays.equals(expected, actual)) {
                     mResult.addWarning(
                             Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
-                            V1SchemeSigner.MANIFEST_ENTRY_NAME,
+                            V1SchemeConstants.MANIFEST_ENTRY_NAME,
                             jcaDigestAlgorithm,
                             mSignatureFileEntry.getName(),
                             Base64.getEncoder().encodeToString(actual),
@@ -1055,7 +1053,7 @@
                 Set<Integer> foundApkSigSchemeIds) {
             String signedWithApkSchemes =
                     sfMainSection.getAttributeValue(
-                            V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+                            V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
             // This field contains a comma-separated list of APK signature scheme IDs which were
             // used to sign this APK. Android rejects APKs where an ID is known to the platform but
             // the APK didn't verify using that scheme.
@@ -1245,40 +1243,7 @@
             DataSource apk,
             ApkUtils.ZipSections apkSections)
                     throws IOException, ApkFormatException {
-        // Read the ZIP Central Directory
-        long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
-        if (cdSizeBytes > Integer.MAX_VALUE) {
-            throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
-        }
-        long cdOffset = apkSections.getZipCentralDirectoryOffset();
-        ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
-        cd.order(ByteOrder.LITTLE_ENDIAN);
-
-        // Parse the ZIP Central Directory
-        int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
-        List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
-        for (int i = 0; i < expectedCdRecordCount; i++) {
-            CentralDirectoryRecord cdRecord;
-            int offsetInsideCd = cd.position();
-            try {
-                cdRecord = CentralDirectoryRecord.getRecord(cd);
-            } catch (ZipFormatException e) {
-                throw new ApkFormatException(
-                        "Malformed ZIP Central Directory record #" + (i + 1)
-                                + " at file offset " + (cdOffset + offsetInsideCd),
-                        e);
-            }
-            String entryName = cdRecord.getName();
-            if (entryName.endsWith("/")) {
-                // Ignore directory entries
-                continue;
-            }
-            cdRecords.add(cdRecord);
-        }
-        // There may be more data in Central Directory, but we don't warn or throw because Android
-        // ignores unused CD data.
-
-        return cdRecords;
+        return ZipUtils.parseZipCentralDirectory(apk, apkSections);
     }
 
     /**
@@ -1382,7 +1347,7 @@
                             Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
                             entryName,
                             expectedDigest.jcaDigestAlgorithm,
-                            V1SchemeSigner.MANIFEST_ENTRY_NAME,
+                            V1SchemeConstants.MANIFEST_ENTRY_NAME,
                             Base64.getEncoder().encodeToString(actualDigest),
                             Base64.getEncoder().encodeToString(expectedDigest.digest));
                 }
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
new file mode 100644
index 0000000..0e244c8
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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.v2;
+
+/** Constants used by the V2 Signature Scheme signing and verification. */
+public class V2SchemeConstants {
+    private V2SchemeConstants() {}
+
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+    public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
index 93f2b0a..ccbe9c4 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
@@ -72,7 +72,8 @@
      * protected by signatures inside the block.
      */
 
-    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+    public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+            V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
 
     /** Hidden constructor to prevent instantiation. */
     private V2SchemeSigner() {}
@@ -191,7 +192,7 @@
                         new byte[][] {
                             encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
                         }),
-                APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+                V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
     }
 
     private static byte[] generateSignerBlock(
@@ -271,9 +272,6 @@
                 });
     }
 
-    // Attribute to check whether a newer APK Signature Scheme signature was stripped
-    protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
-
     private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
         if (v3SigningEnabled) {
             // FORMAT (little endian):
@@ -284,7 +282,7 @@
             ByteBuffer result = ByteBuffer.allocate(payloadSize);
             result.order(ByteOrder.LITTLE_ENDIAN);
             result.putInt(payloadSize - 4);
-            result.putInt(STRIPPING_PROTECTION_ATTR_ID);
+            result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
             result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
             return result.array();
         } else {
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index acc5e3e..4d6e3e1 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -63,9 +63,6 @@
  * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
  */
 public abstract class V2SchemeVerifier {
-
-    private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
     /** Hidden constructor to prevent instantiation. */
     private V2SchemeVerifier() {}
 
@@ -103,7 +100,7 @@
                 ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
         SignatureInfo signatureInfo =
                 ApkSigningBlockUtils.findSignature(apk, zipSections,
-                        APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
+                        V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
 
         DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
         DataSource centralDir =
@@ -252,8 +249,7 @@
             Map<Integer, String> supportedApkSigSchemeNames,
             Set<Integer> foundApkSigSchemeIds,
             int minSdkVersion,
-            int maxSdkVersion)
-                    throws ApkFormatException, NoSuchAlgorithmException {
+            int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
         ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
         byte[] signedDataBytes = new byte[signedData.remaining()];
         signedData.get(signedDataBytes);
@@ -440,7 +436,7 @@
                 result.additionalAttributes.add(
                         new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
                 switch (id) {
-                    case V2SchemeSigner.STRIPPING_PROTECTION_ATTR_ID:
+                    case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
                         // stripping protection added when signing with a newer scheme
                         int foundId = ByteBuffer.wrap(value).order(
                                 ByteOrder.LITTLE_ENDIAN).getInt();
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
new file mode 100644
index 0000000..3b70aa0
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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.v3;
+
+/** Constants used by the V3 Signature Scheme signing and verification. */
+public class V3SchemeConstants {
+    private V3SchemeConstants() {}
+
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
index 56ab60e..cab2a47 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -57,8 +57,9 @@
  *     it can prove the new siging certificate was signed by the old.
  */
 public abstract class V3SchemeSigner {
-
-    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+    public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+    public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
 
     /** Hidden constructor to prevent instantiation. */
     private V3SchemeSigner() {}
@@ -141,6 +142,22 @@
                 digestInfo.getSecond());
     }
 
+    public static byte[] generateV3SignerAttribute(
+            SigningCertificateLineage signingCertificateLineage) {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: attribute pair
+        //   * uint32: ID
+        //   * bytes: value - encoded V3 SigningCertificateLineage
+        byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
+        int payloadSize = 4 + 4 + encodedLineage.length;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(4 + encodedLineage.length);
+        result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
+        result.put(encodedLineage);
+        return result.array();
+    }
+
     private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
             List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
@@ -166,7 +183,7 @@
                         new byte[][] {
                             encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
                         }),
-                APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
     }
 
     private static byte[] generateSignerBlock(
@@ -284,13 +301,11 @@
         return result.array();
     }
 
-    public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
-
     private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
         if (signerConfig.mSigningCertificateLineage == null) {
             return new byte[0];
         }
-        return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute();
+        return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
     }
 
     private static final class V3SignatureSchemeBlock {
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index 659d379..ea93194 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -30,10 +30,11 @@
 import com.android.apksig.internal.apk.SignatureInfo;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferUtils;
-import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.RunnablesExecutor;
+
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
@@ -53,7 +54,6 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -68,9 +68,6 @@
  * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
  */
 public abstract class V3SchemeVerifier {
-
-    private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
-
     /** Hidden constructor to prevent instantiation. */
     private V3SchemeVerifier() {}
 
@@ -105,7 +102,7 @@
                 ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
         SignatureInfo signatureInfo =
                 ApkSigningBlockUtils.findSignature(apk, zipSections,
-                        APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+                        V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
 
         DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
         DataSource centralDir =
@@ -494,7 +491,7 @@
                 byte[] value = ByteBufferUtils.toByteArray(attribute);
                 result.additionalAttributes.add(
                         new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
-                if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
+                if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
                     try {
                         // SigningCertificateLineage is verified when built
                         result.signingCertificateLineage =
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 73ba46f..1a1ad93 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -17,10 +17,9 @@
 package com.android.apksig.internal.apk.v4;
 
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
-import static com.android.apksig.internal.apk.v2.V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
-import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
 
-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.ApkSigningBlockUtils.SignerConfig;
@@ -324,8 +323,9 @@
                 return 1;
             case CHUNKED_SHA512:
                 return 2;
+            default:
+                return -1;
         }
-        return -1;
     }
 
     private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
diff --git a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
index 4ef67c7..87eae48 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -24,9 +24,15 @@
     /** Hidden constructor to prevent instantiation. */
     private AndroidSdkVersion() {}
 
+    /** Android 1.0 */
+    public static final int INITIAL_RELEASE = 1;
+
     /** Android 2.3. */
     public static final int GINGERBREAD = 9;
 
+    /** Android 3.0 */
+    public static final int HONEYCOMB = 11;
+
     /** Android 4.3. The revenge of the beans. */
     public static final int JELLY_BEAN_MR2 = 18;
 
diff --git a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
index 272015a..9d9da15 100644
--- a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
+++ b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
@@ -16,12 +16,18 @@
 
 package com.android.apksig.internal.zip;
 
+import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.zip.CRC32;
 import java.util.zip.Deflater;
 
@@ -247,6 +253,46 @@
         return buffer.getShort() & 0xffff;
     }
 
+    public static List<CentralDirectoryRecord> parseZipCentralDirectory(
+            DataSource apk,
+            ZipSections apkSections)
+            throws IOException, ApkFormatException {
+        // Read the ZIP Central Directory
+        long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+        if (cdSizeBytes > Integer.MAX_VALUE) {
+            throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+        }
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+        cd.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Parse the ZIP Central Directory
+        int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+        List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+        for (int i = 0; i < expectedCdRecordCount; i++) {
+            CentralDirectoryRecord cdRecord;
+            int offsetInsideCd = cd.position();
+            try {
+                cdRecord = CentralDirectoryRecord.getRecord(cd);
+            } catch (ZipFormatException e) {
+                throw new ApkFormatException(
+                        "Malformed ZIP Central Directory record #" + (i + 1)
+                                + " at file offset " + (cdOffset + offsetInsideCd),
+                        e);
+            }
+            String entryName = cdRecord.getName();
+            if (entryName.endsWith("/")) {
+                // Ignore directory entries
+                continue;
+            }
+            cdRecords.add(cdRecord);
+        }
+        // There may be more data in Central Directory, but we don't warn or throw because Android
+        // ignores unused CD data.
+
+        return cdRecords;
+    }
+
     static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
         if ((value < 0) || (value > 0xffff)) {
             throw new IllegalArgumentException("uint16 value of out range: " + value);
diff --git a/src/main/java/com/android/apksig/zip/ZipSections.java b/src/main/java/com/android/apksig/zip/ZipSections.java
new file mode 100644
index 0000000..17bce05
--- /dev/null
+++ b/src/main/java/com/android/apksig/zip/ZipSections.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 com.android.apksig.zip;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base representation of an APK's zip sections containing the central directory's offset, the size
+ * of the central directory in bytes, the number of records in the central directory, the offset
+ * of the end of central directory, and a ByteBuffer containing the end of central directory
+ * contents.
+ */
+public class ZipSections {
+    private final long mCentralDirectoryOffset;
+    private final long mCentralDirectorySizeBytes;
+    private final int mCentralDirectoryRecordCount;
+    private final long mEocdOffset;
+    private final ByteBuffer mEocd;
+
+    public ZipSections(
+            long centralDirectoryOffset,
+            long centralDirectorySizeBytes,
+            int centralDirectoryRecordCount,
+            long eocdOffset,
+            ByteBuffer eocd) {
+        mCentralDirectoryOffset = centralDirectoryOffset;
+        mCentralDirectorySizeBytes = centralDirectorySizeBytes;
+        mCentralDirectoryRecordCount = centralDirectoryRecordCount;
+        mEocdOffset = eocdOffset;
+        mEocd = eocd;
+    }
+
+    /**
+     * Returns the start offset of the ZIP Central Directory. This value is taken from the
+     * ZIP End of Central Directory record.
+     */
+    public long getZipCentralDirectoryOffset() {
+        return mCentralDirectoryOffset;
+    }
+
+    /**
+     * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
+     * ZIP End of Central Directory record.
+     */
+    public long getZipCentralDirectorySizeBytes() {
+        return mCentralDirectorySizeBytes;
+    }
+
+    /**
+     * Returns the number of records in the ZIP Central Directory. This value is taken from the
+     * ZIP End of Central Directory record.
+     */
+    public int getZipCentralDirectoryRecordCount() {
+        return mCentralDirectoryRecordCount;
+    }
+
+    /**
+     * Returns the start offset of the ZIP End of Central Directory record. The record extends
+     * until the very end of the APK.
+     */
+    public long getZipEndOfCentralDirectoryOffset() {
+        return mEocdOffset;
+    }
+
+    /**
+     * Returns the contents of the ZIP End of Central Directory.
+     */
+    public ByteBuffer getZipEndOfCentralDirectory() {
+        return mEocd;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/apksig/AllTests.java b/src/test/java/com/android/apksig/AllTests.java
index 4a9243d..3cb1052 100644
--- a/src/test/java/com/android/apksig/AllTests.java
+++ b/src/test/java/com/android/apksig/AllTests.java
@@ -24,6 +24,7 @@
     ApkSignerTest.class,
     ApkVerifierTest.class,
     SigningCertificateLineageTest.class,
+    SourceStampVerifierTest.class,
     com.android.apksig.apk.AllTests.class,
     com.android.apksig.internal.AllTests.class,
     com.android.apksig.util.AllTests.class,
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 84c6dc7..fab1361 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -31,10 +31,10 @@
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.SignatureInfo;
-import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
 import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
-import com.android.apksig.internal.apk.v2.V2SchemeSigner;
-import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.asn1.Asn1BerParser;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.Resources;
@@ -42,23 +42,23 @@
 import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.LocalFileRecord;
-import com.android.apksig.util.DataSinks;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
-import com.android.apksig.util.ReadableDataSink;
 import com.android.apksig.zip.ZipFormatException;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
-import java.nio.channels.ByteChannel;
 import java.nio.file.Files;
-import java.nio.file.StandardOpenOption;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
@@ -90,6 +90,9 @@
     private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
             "rsa-2048-lineage-2-signers";
 
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
     public static void main(String[] params) throws Exception {
         File outDir = (params.length > 0) ? new File(params[0]) : new File(".");
         generateGoldenFiles(outDir);
@@ -136,21 +139,24 @@
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
         signGolden(
                 "golden-legacy-aligned-in.apk",
                 new File(outDir, "golden-legacy-aligned-v1-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
         signGolden(
                 "golden-aligned-in.apk",
                 new File(outDir, "golden-aligned-v1-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
 
         signGolden(
                 "golden-unaligned-in.apk",
@@ -367,7 +373,13 @@
         DataSource in =
                 DataSources.asDataSource(
                         ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName)));
-        apkSignerBuilder.setInputApk(in).setOutputApk(outFile).build().sign();
+        apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
+
+        File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig");
+        apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig);
+        apkSignerBuilder.setV4ErrorReportingEnabled(true);
+
+        apkSignerBuilder.build().sign();
     }
 
     @Test
@@ -399,7 +411,8 @@
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
         assertGolden(
                 "golden-unaligned-in.apk",
                 "golden-unaligned-v2-out.apk",
@@ -474,7 +487,8 @@
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
         assertGolden(
                 "golden-legacy-aligned-in.apk",
                 "golden-legacy-aligned-v2-out.apk",
@@ -548,7 +562,8 @@
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
-                        .setV3SigningEnabled(false));
+                        .setV3SigningEnabled(false)
+                        .setV4SigningEnabled(false));
         assertGolden(
                 "golden-aligned-in.apk",
                 "golden-aligned-v2-out.apk",
@@ -661,7 +676,7 @@
         String in = "original.apk";
 
         // Sign so that the APK is guaranteed to verify on API Level 1+
-        DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+        File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
         assertVerified(verifyForMinSdkVersion(out, 1));
 
         // Sign so that the APK is guaranteed to verify on API Level 18+
@@ -679,7 +694,7 @@
         String in = "original.apk";
 
         // Sign so that the APK is guaranteed to verify on API Level 1+
-        DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+        File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
         assertVerified(verifyForMinSdkVersion(out, 1));
 
         // Sign so that the APK is guaranteed to verify on API Level 21+
@@ -698,7 +713,7 @@
 
         // NOTE: EC APK signatures are not supported prior to API Level 18
         // Sign so that the APK is guaranteed to verify on API Level 18+
-        DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18));
+        File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18));
         assertVerified(verifyForMinSdkVersion(out, 18));
         // Does not verify on API Level 17 because EC not supported
         assertVerificationFailure(
@@ -1030,14 +1045,13 @@
                 Arrays.asList(
                         getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
                         getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
-        DataSource out =
+        File out =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signerConfigs)
                                 .setV3SigningEnabled(true)
                                 .setSigningCertificateLineage(lineage));
-        SigningCertificateLineage lineageFromApk =
-                SigningCertificateLineage.readFromApkDataSource(out);
+        SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkFile(out);
         assertTrue(
                 "The first signer was not in the lineage from the signed APK",
                 lineageFromApk.isSignerInLineage((firstSigner)));
@@ -1060,7 +1074,7 @@
                         getDefaultSignerConfigFromResources(
                                 FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
                                 FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signersList)
@@ -1109,28 +1123,32 @@
         messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded());
         byte[] expectedStampCertificateDigest = messageDigest.digest();
 
-        DataSource signedApk =
+        File signedApkFile =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signers)
                                 .setV1SigningEnabled(true)
                                 .setSourceStampSignerConfig(sourceStampSigner));
 
-        ApkUtils.ZipSections zipSections = findZipSections(signedApk);
-        List<CentralDirectoryRecord> cdRecords =
-                V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections);
-        CentralDirectoryRecord stampCdRecord = null;
-        for (CentralDirectoryRecord cdRecord : cdRecords) {
-            if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
-                stampCdRecord = cdRecord;
-                break;
+        try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) {
+            DataSource signedApk = DataSources.asDataSource(f, 0, f.length());
+
+            ApkUtils.ZipSections zipSections = findZipSections(signedApk);
+            List<CentralDirectoryRecord> cdRecords =
+                    V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections);
+            CentralDirectoryRecord stampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+                    stampCdRecord = cdRecord;
+                    break;
+                }
             }
+            assertNotNull(stampCdRecord);
+            byte[] actualStampCertificateDigest =
+                    LocalFileRecord.getUncompressedData(
+                            signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
+            assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
         }
-        assertNotNull(stampCdRecord);
-        byte[] actualStampCertificateDigest =
-                LocalFileRecord.getUncompressedData(
-                        signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
-        assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
     }
 
     @Test
@@ -1141,7 +1159,7 @@
         ApkSigner.SignerConfig sourceStampSigner =
                 getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
 
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original-with-stamp-file.apk",
                         new ApkSigner.Builder(signers)
@@ -1192,7 +1210,7 @@
         ApkSigner.SignerConfig sourceStampSigner =
                 getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
 
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original-with-stamp-file.apk",
                         new ApkSigner.Builder(signers)
@@ -1213,7 +1231,7 @@
                 Collections.singletonList(
                         getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
 
-        DataSource signedApk =
+        File signedApkFile =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signersList)
@@ -1221,17 +1239,21 @@
                                 .setV2SigningEnabled(true)
                                 .setV3SigningEnabled(true));
 
-        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
-        ApkSigningBlockUtils.Result result =
-                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
-        assertThrows(
-                ApkSigningBlockUtils.SignatureNotFoundException.class,
-                () ->
-                        ApkSigningBlockUtils.findSignature(
-                                signedApk,
-                                zipSections,
-                                ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
-                                result));
+        try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) {
+            DataSource signedApk = DataSources.asDataSource(f, 0, f.length());
+
+            ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
+            ApkSigningBlockUtils.Result result =
+                    new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+            assertThrows(
+                    ApkSigningBlockUtils.SignatureNotFoundException.class,
+                    () ->
+                            ApkSigningBlockUtils.findSignature(
+                                    signedApk,
+                                    zipSections,
+                                    ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+                                    result));
+        }
     }
 
     @Test
@@ -1242,13 +1264,14 @@
         ApkSigner.SignerConfig sourceStampSigner =
                 getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
 
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signersList)
                                 .setV1SigningEnabled(true)
                                 .setV2SigningEnabled(false)
                                 .setV3SigningEnabled(false)
+                                .setV4SigningEnabled(false)
                                 .setSourceStampSignerConfig(sourceStampSigner));
 
         ApkVerifier.Result sourceStampVerificationResult =
@@ -1264,7 +1287,7 @@
         ApkSigner.SignerConfig sourceStampSigner =
                 getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
 
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signersList)
@@ -1286,7 +1309,7 @@
         ApkSigner.SignerConfig sourceStampSigner =
                 getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
 
-        DataSource signedApk =
+        File signedApk =
                 sign(
                         "original.apk",
                         new ApkSigner.Builder(signersList)
@@ -1300,15 +1323,41 @@
         assertSourceStampVerified(signedApk, sourceStampVerificationResult);
     }
 
-    private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId)
+    @Test
+    public void testSignApk_stampBlock_withStampLineage() throws Exception {
+        List<ApkSigner.SignerConfig> signersList =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        ApkSigner.SignerConfig sourceStampSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage sourceStampLineage =
+                Resources.toSigningCertificateLineage(
+                        getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(true)
+                                .setV3SigningEnabled(true)
+                                .setSourceStampSignerConfig(sourceStampSigner)
+                                .setSourceStampSigningCertificateLineage(sourceStampLineage));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verify(signedApk, /* minSdkVersion= */ null);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
+    private RSAPublicKey getRSAPublicKeyFromSigningBlock(File apk, int signatureVersionId)
             throws Exception {
         int signatureVersionBlockId;
         switch (signatureVersionId) {
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
-                signatureVersionBlockId = V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+                signatureVersionBlockId = V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
                 break;
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
-                signatureVersionBlockId = V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+                signatureVersionBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
                 break;
             default:
                 throw new Exception(
@@ -1347,13 +1396,17 @@
     }
 
     private static SignatureInfo getSignatureInfoFromApk(
-            DataSource apk, int signatureVersionId, int signatureVersionBlockId)
+            File apkFile, int signatureVersionId, int signatureVersionBlockId)
             throws IOException, ZipFormatException,
-                    ApkSigningBlockUtils.SignatureNotFoundException {
-        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
-        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(signatureVersionId);
-        return ApkSigningBlockUtils.findSignature(
-                apk, zipSections, signatureVersionBlockId, result);
+            ApkSigningBlockUtils.SignatureNotFoundException {
+        try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
+            DataSource apk = DataSources.asDataSource(f, 0, f.length());
+            ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                    signatureVersionId);
+            return ApkSigningBlockUtils.findSignature(apk, zipSections, signatureVersionBlockId,
+                    result);
+        }
     }
 
     /**
@@ -1366,18 +1419,22 @@
             ApkSigner.Builder apkSignerBuilder)
             throws Exception {
         // Sign the provided golden input
-        DataSource out = sign(inResourceName, apkSignerBuilder);
+        File out = sign(inResourceName, apkSignerBuilder);
+        assertVerified(verify(out, AndroidSdkVersion.P));
 
         // Assert that the output is identical to the provided golden output
-        if (out.size() > Integer.MAX_VALUE) {
-            throw new RuntimeException("Output too large: " + out.size() + " bytes");
+        if (out.length() > Integer.MAX_VALUE) {
+            throw new RuntimeException("Output too large: " + out.length() + " bytes");
         }
-        ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size());
+        byte[] outData = new byte[(int) out.length()];
+        try (FileInputStream fis = new FileInputStream(out)) {
+            fis.read(outData);
+        }
+        ByteBuffer actualOutBuf = ByteBuffer.wrap(outData);
 
         ByteBuffer expectedOutBuf =
                 ByteBuffer.wrap(Resources.toByteArray(getClass(), expectedOutResourceName));
 
-        int actualStartPos = actualOutBuf.position();
         boolean identical = false;
         if (actualOutBuf.remaining() == expectedOutBuf.remaining()) {
             while (actualOutBuf.hasRemaining()) {
@@ -1391,47 +1448,47 @@
         if (identical) {
             return;
         }
-        actualOutBuf.position(actualStartPos);
 
         if (KEEP_FAILING_OUTPUT_AS_FILES) {
             File tmp = File.createTempFile(getClass().getSimpleName(), ".apk");
-            try (ByteChannel outChannel =
-                    Files.newByteChannel(
-                            tmp.toPath(),
-                            StandardOpenOption.WRITE,
-                            StandardOpenOption.CREATE,
-                            StandardOpenOption.TRUNCATE_EXISTING)) {
-                while (actualOutBuf.hasRemaining()) {
-                    outChannel.write(actualOutBuf);
-                }
-            }
+            Files.copy(out.toPath(), tmp.toPath());
             fail(tmp + " differs from " + expectedOutResourceName);
         } else {
             fail("Output differs from " + expectedOutResourceName);
         }
     }
 
-    private DataSource sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
+    private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
             throws Exception {
         DataSource in =
                 DataSources.asDataSource(
                         ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName)));
-        ReadableDataSink out = DataSinks.newInMemoryDataSink();
-        apkSignerBuilder.setInputApk(in).setOutputApk(out).build().sign();
-        return out;
+        File outFile = mTemporaryFolder.newFile();
+        apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
+
+        File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig");
+        apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig);
+        apkSignerBuilder.setV4ErrorReportingEnabled(true);
+
+        apkSignerBuilder.build().sign();
+        return outFile;
     }
 
-    private static ApkVerifier.Result verifyForMinSdkVersion(DataSource apk, int minSdkVersion)
+    private static ApkVerifier.Result verifyForMinSdkVersion(File apk, int minSdkVersion)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apk, minSdkVersion);
     }
 
-    private static ApkVerifier.Result verify(DataSource apk, Integer minSdkVersionOverride)
+    private static ApkVerifier.Result verify(File apk, Integer minSdkVersionOverride)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         ApkVerifier.Builder builder = new ApkVerifier.Builder(apk);
         if (minSdkVersionOverride != null) {
             builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
         }
+        File idSig = new File(apk.getCanonicalPath() + ".idsig");
+        if (idSig.exists()) {
+            builder.setV4SignatureFile(idSig);
+        }
         return builder.build().verify();
     }
 
@@ -1439,14 +1496,14 @@
         ApkVerifierTest.assertVerified(result);
     }
 
-    private static void assertSourceStampVerified(DataSource signedApk, ApkVerifier.Result result)
+    private static void assertSourceStampVerified(File signedApk, ApkVerifier.Result result)
             throws ApkSigningBlockUtils.SignatureNotFoundException, IOException,
-                    ZipFormatException {
+            ZipFormatException {
         SignatureInfo signatureInfo =
                 getSignatureInfoFromApk(
                         signedApk,
                         ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
-                        V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID);
+                        SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
         assertNotNull(signatureInfo.signatureBlock);
         assertTrue(result.isSourceStampVerified());
     }
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 89bac3c..79950dc 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -23,6 +23,7 @@
 
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.ApkVerifier.IssueWithParams;
+import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.HexEncoding;
@@ -35,6 +36,8 @@
 import org.junit.runners.JUnit4;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
@@ -60,9 +63,14 @@
     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"
+            "2048", "3072", "4096", "8192", "16384"
     };
 
+    private static final String RSA_2048_CERT_SHA256_DIGEST =
+            "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+    private static final String EC_P256_CERT_SHA256_DIGEST =
+            "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+
     @Test
     public void testOriginalAccepted() throws Exception {
         // APK signed with v1 and v2 schemes. Obtained by building
@@ -1095,6 +1103,33 @@
     }
 
     @Test
+    public void verifySourceStamp_correctSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("valid-stamp.apk");
+        // Since the API is only verifying the source stamp the result itself should be marked as
+        // verified.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        // The source stamp can also be verified by platform version; confirm the verification works
+        // using just the max signature scheme version supported by that platform version.
+        verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @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.
@@ -1103,6 +1138,14 @@
     }
 
     @Test
+    public void verifySourceStamp_signatureMissing() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-without-block.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_NOT_VERIFIED);
+        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.
@@ -1113,6 +1156,80 @@
     }
 
     @Test
+    public void verifySourceStamp_certificateMismatch() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-certificate-mismatch.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(
+                verificationResult,
+                Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+    }
+
+    @Test
+    public void testSourceStampBlock_v1OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verify("v1-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertTrue(verificationResult.isSourceStampVerified());
+    }
+
+    @Test
+    public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        // Confirm that the source stamp verification succeeds when specifying platform versions
+        // that supported later signature scheme versions.
+        verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @Test
+    public void testSourceStampBlock_v2OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verify("v2-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertTrue(verificationResult.isSourceStampVerified());
+    }
+
+    @Test
+    public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+
+        // Confirm that the source stamp verification succeeds when specifying a platform version
+        // that supports a later signature scheme version.
+        verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @Test
+    public void testSourceStampBlock_v3OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verify("v3-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertTrue(verificationResult.isSourceStampVerified());
+    }
+
+    @Test
+    public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @Test
     public void testSourceStampBlock_apkHashMismatch_v1SignatureScheme() throws Exception {
         ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v1.apk");
         // A broken stamp should not block a signing scheme verified APK.
@@ -1121,6 +1238,14 @@
     }
 
     @Test
+    public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v1.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
     public void testSourceStampBlock_apkHashMismatch_v2SignatureScheme() throws Exception {
         ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v2.apk");
         // A broken stamp should not block a signing scheme verified APK.
@@ -1129,6 +1254,14 @@
     }
 
     @Test
+    public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v2.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
     public void testSourceStampBlock_apkHashMismatch_v3SignatureScheme() throws Exception {
         ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v3.apk");
         // A broken stamp should not block a signing scheme verified APK.
@@ -1137,6 +1270,14 @@
     }
 
     @Test
+    public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v3.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        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.
@@ -1145,6 +1286,81 @@
                 verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
     }
 
+    @Test
+    public void verifySourceStamp_malformedSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-malformed-signature.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(
+                verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+    }
+
+    @Test
+    public void verifySourceStamp_expectedDigestMatchesActual() throws Exception {
+        // The ApkVerifier provides an API to specify the expected certificate digest; this test
+        // verifies that the test runs through to completion when the actual digest matches the
+        // provided value.
+        ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+                RSA_2048_CERT_SHA256_DIGEST);
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @Test
+    public void verifySourceStamp_expectedDigestMismatch() throws Exception {
+        // If the caller requests source stamp verification with an expected cert digest that does
+        // not match the actual digest in the APK the verifier should report the mismatch.
+        ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+                EC_P256_CERT_SHA256_DIGEST);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.CERT_DIGEST_MISMATCH);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+    }
+
+    @Test
+    public void verifySourceStamp_validStampLineage() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-valid.apk");
+        assertVerified(verificationResult);
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFIED);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidStampLineage() throws Exception {
+        ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-invalid.apk");
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+    }
+
+    @Test
+    public void apkVerificationIssueAdapter_verifyAllBaseIssuesMapped() throws Exception {
+        Field[] fields = ApkVerificationIssue.class.getFields();
+        StringBuilder msg = new StringBuilder();
+        for (Field field : fields) {
+            // All public static int fields in the ApkVerificationIssue class should be issue IDs;
+            // if any are added that are not intended as IDs a filter set should be applied to this
+            // test.
+            if (Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) {
+                if (!ApkVerifier.ApkVerificationIssueAdapter
+                        .sVerificationIssueIdToIssue.containsKey(field.get(null))) {
+                    if (msg.length() > 0) {
+                        msg.append('\n');
+                    }
+                    msg.append(
+                            "A mapping is required from ApkVerificationIssue." + field.getName()
+                                    + " to an ApkVerifier.Issue in ApkVerificationIssueAdapter");
+                }
+            }
+        }
+        if (msg.length() > 0) {
+            fail(msg.toString());
+        }
+    }
+
     private ApkVerifier.Result verify(String apkFilenameInResources)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, null);
@@ -1180,6 +1396,36 @@
         return builder.build().verify();
     }
 
+    private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources) throws Exception {
+        return verifySourceStamp(apkFilenameInResources, null, null, null);
+    }
+
+    private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+            String expectedCertDigest) throws Exception {
+        return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
+    }
+
+    private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+            Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
+        return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
+                maxSdkVersionOverride);
+    }
+
+    private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+            String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride)
+            throws Exception {
+        byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
+        ApkVerifier.Builder builder = new ApkVerifier.Builder(
+                DataSources.asDataSource(ByteBuffer.wrap(apkBytes)));
+        if (minSdkVersionOverride != null) {
+            builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
+        }
+        if (maxSdkVersionOverride != null) {
+            builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride);
+        }
+        return builder.build().verifySourceStamp(expectedCertDigest);
+    }
+
     static void assertVerified(ApkVerifier.Result result) {
         assertVerified(result, "APK");
     }
@@ -1375,6 +1621,12 @@
                         + msg);
     }
 
+    private static void assertSourceStampVerificationStatus(ApkVerifier.Result result,
+            SourceStampVerificationStatus verificationStatus) throws Exception {
+        assertEquals(verificationStatus,
+                result.getSourceStampInfo().getSourceStampVerificationStatus());
+    }
+
     private void assertVerificationFailure(
             String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
         assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index 2038421..14cab83 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -21,27 +21,23 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
+import com.android.apksig.SigningCertificateLineage.SignerConfig;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
-import com.android.apksig.internal.util.ByteBufferDataSource;
 import com.android.apksig.internal.util.ByteBufferUtils;
 import com.android.apksig.internal.util.Resources;
-
-import com.android.apksig.SigningCertificateLineage.SignerConfig;
-import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
-
 import com.android.apksig.util.DataSource;
 
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 import java.io.File;
+import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
@@ -249,7 +245,8 @@
         // * length-prefixed bytes: attribute pair
         //   * uint32: ID
         //   * bytes: value - encoded V3 SigningCertificateLineage
-        ByteBuffer v3SignerAttribute = ByteBuffer.wrap(lineage.generateV3SignerAttribute());
+        ByteBuffer v3SignerAttribute = ByteBuffer.wrap(
+                V3SchemeSigner.generateV3SignerAttribute(lineage));
         v3SignerAttribute.order(ByteOrder.LITTLE_ENDIAN);
         ByteBuffer attribute = ApkSigningBlockUtils.getLengthPrefixedSlice(v3SignerAttribute);
         // The generateV3SignerAttribute method should only use the PROOF_OF_ROTATION_ATTR_ID
@@ -258,7 +255,7 @@
         assertEquals(
                 "The ID of the v3SignerAttribute ByteBuffer is not the expected "
                         + "PROOF_OF_ROTATION_ATTR_ID",
-                V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID, id);
+                V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID, id);
         lineage = SigningCertificateLineage.readFromV3AttributeValue(
                 ByteBufferUtils.toByteArray(attribute));
         assertLineageContainsExpectedSigners(lineage, mSigners);
diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
new file mode 100644
index 0000000..f5020cc
--- /dev/null
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -0,0 +1,420 @@
+/*
+ * 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;
+
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.android.apksig.SourceStampVerifier.Result;
+import com.android.apksig.SourceStampVerifier.Result.SignerInfo;
+import com.android.apksig.internal.util.Resources;
+import com.android.apksig.util.DataSources;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.nio.ByteBuffer;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class SourceStampVerifierTest {
+    private static final String RSA_2048_CERT_SHA256_DIGEST =
+            "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+    private static final String RSA_2048_2_CERT_SHA256_DIGEST =
+            "681b0e56a796350c08647352a4db800cc44b2adc8f4c72fa350bd05d4d50264d";
+    private static final String RSA_2048_3_CERT_SHA256_DIGEST =
+            "bb77a72efc60e66501ab75953af735874f82cfe52a70d035186a01b3482180f3";
+    private static final String EC_P256_CERT_SHA256_DIGEST =
+            "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+    private static final String EC_P256_2_CERT_SHA256_DIGEST =
+            "d78405f761ff6236cc9b570347a570aba0c62a129a3ac30c831c64d09ad95469";
+
+    @Test
+    public void verifySourceStamp_correctSignature() throws Exception {
+        Result verificationResult = verifySourceStamp("valid-stamp.apk");
+        // Since the API is only verifying the source stamp the result itself should be marked as
+        // verified.
+        assertVerified(verificationResult);
+
+        // The source stamp can also be verified by platform version; confirm the verification works
+        // using just the max signature scheme version supported by that platform version.
+        verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18);
+        assertVerified(verificationResult);
+
+        verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24);
+        assertVerified(verificationResult);
+
+        verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+    }
+
+    @Test
+    public void verifySourceStamp_rotatedV3Key_signingCertDigestsMatch() throws Exception {
+        // The SourceStampVerifier should return a result that includes all of the latest signing
+        // certificates for each of the signature schemes that are applicable to the specified
+        // min / max SDK versions.
+
+        // Verify when platform versions that support the V1 - V3 signature schemes are specified
+        // that an APK signed with all signature schemes has its expected signers returned in the
+        // result.
+        Result verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 23,
+                28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST,
+                EC_P256_CERT_SHA256_DIGEST, EC_P256_2_CERT_SHA256_DIGEST);
+
+        // Verify when the specified platform versions only support a single signature scheme that
+        // scheme's signer is the only one in the result.
+        verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 18, 18);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+        verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 24, 24);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+
+        verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, null, EC_P256_2_CERT_SHA256_DIGEST);
+    }
+
+    @Test
+    public void verifySourceStamp_signatureMissing() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-without-block.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+    }
+
+    @Test
+    public void verifySourceStamp_certificateMismatch() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-certificate-mismatch.apk");
+        assertSourceStampVerificationFailure(
+                verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+    }
+
+    @Test
+    public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception {
+        Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+        // Confirm that the source stamp verification succeeds when specifying platform versions
+        // that supported later signature scheme versions.
+        verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+        verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+    }
+
+    @Test
+    public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception {
+        // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
+        // set the min / max versions to prevent failure due to a missing V1 signature.
+        Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk",
+                24, 24);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+
+        // Confirm that the source stamp verification succeeds when specifying a platform version
+        // that supports a later signature scheme version.
+        verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+    }
+
+    @Test
+    public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception {
+        // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
+        // set the min / max versions to prevent failure due to a missing V1 signature.
+        Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+                28, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, null, EC_P256_CERT_SHA256_DIGEST);
+    }
+
+    @Test
+    public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-apk-hash-mismatch-v1.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-apk-hash-mismatch-v2.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-apk-hash-mismatch-v3.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void verifySourceStamp_malformedSignature() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-malformed-signature.apk");
+        assertSourceStampVerificationFailure(
+                verificationResult, ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+    }
+
+    @Test
+    public void verifySourceStamp_expectedDigestMatchesActual() throws Exception {
+        // The ApkVerifier provides an API to specify the expected certificate digest; this test
+        // verifies that the test runs through to completion when the actual digest matches the
+        // provided value.
+        Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+                RSA_2048_CERT_SHA256_DIGEST, 28, 28);
+        assertVerified(verificationResult);
+    }
+
+    @Test
+    public void verifySourceStamp_expectedDigestMismatch() throws Exception {
+        // If the caller requests source stamp verification with an expected cert digest that does
+        // not match the actual digest in the APK the verifier should report the mismatch.
+        Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+                EC_P256_CERT_SHA256_DIGEST);
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+    }
+
+    @Test
+    public void verifySourceStamp_noStampCertDigestNorSignatureBlock() throws Exception {
+        // The caller of this API expects that the provided APK should be signed with a source
+        // stamp; if no artifacts of the stamp are present ensure that the API fails indicating the
+        // missing stamp.
+        Result verificationResult = verifySourceStamp("original.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+    }
+
+    @Test
+    public void verifySourceStamp_validStampLineage() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-lineage-valid.apk");
+        assertVerified(verificationResult);
+        assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST,
+                RSA_2048_2_CERT_SHA256_DIGEST);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidStampLineage() throws Exception {
+        Result verificationResult = verifySourceStamp(
+                "stamp-lineage-invalid.apk");
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+    }
+
+    @Test
+    public void verifySourceStamp_multipleSignersInLineage() throws Exception {
+        Result verificationResult = verifySourceStamp("stamp-lineage-with-3-signers.apk", 18, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST,
+                RSA_2048_2_CERT_SHA256_DIGEST, RSA_2048_3_CERT_SHA256_DIGEST);
+    }
+
+    @Test
+    public void verifySourceStamp_noSignersInLineage_returnsEmptyLineage() throws Exception {
+        // If the source stamp's signer has not yet been rotated then an empty lineage should be
+        // returned.
+        Result verificationResult = verifySourceStamp("valid-stamp.apk");
+        assertSigningCertificatesInLineage(verificationResult);
+    }
+
+    @Test
+    public void verifySourceStamp_noApkSignature_succeeds()
+            throws Exception {
+        // The SourceStampVerifier is designed to verify an APK's source stamp with minimal
+        // verification of the APK signature schemes. This test verifies if just the MANIFEST.MF
+        // is present without any other APK signatures the stamp signature can still be successfully
+        // verified.
+        Result verificationResult = verifySourceStamp("stamp-without-apk-signature.apk", 18, 28);
+        assertVerified(verificationResult);
+        assertSigningCertificates(verificationResult, null, null, null);
+        // While the source stamp verification should succeed a warning should still be logged to
+        // notify the caller that there were no signers.
+        assertSourceStampVerificationWarning(verificationResult,
+                ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+    }
+
+    private Result verifySourceStamp(String apkFilenameInResources)
+            throws Exception {
+        return verifySourceStamp(apkFilenameInResources, null, null, null);
+    }
+
+    private Result verifySourceStamp(String apkFilenameInResources,
+            String expectedCertDigest) throws Exception {
+        return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
+    }
+
+    private Result verifySourceStamp(String apkFilenameInResources,
+            Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
+        return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
+                maxSdkVersionOverride);
+    }
+
+    private Result verifySourceStamp(String apkFilenameInResources,
+            String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride)
+            throws Exception {
+        byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
+        SourceStampVerifier.Builder builder = new SourceStampVerifier.Builder(
+                DataSources.asDataSource(ByteBuffer.wrap(apkBytes)));
+        if (minSdkVersionOverride != null) {
+            builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
+        }
+        if (maxSdkVersionOverride != null) {
+            builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride);
+        }
+        return builder.build().verifySourceStamp(expectedCertDigest);
+    }
+
+    private static void assertVerified(Result result) {
+        if (result.isVerified()) {
+            return;
+        }
+        StringBuilder msg = new StringBuilder();
+        for (ApkVerificationIssue error : result.getAllErrors()) {
+            if (msg.length() > 0) {
+                msg.append('\n');
+            }
+            msg.append(error.toString());
+        }
+        fail("APK failed source stamp verification: " + msg.toString());
+    }
+
+    private static void assertSourceStampVerificationFailure(Result result, int expectedIssueId) {
+        if (result.isVerified()) {
+            fail(
+                    "APK source stamp verification succeeded instead of failing with "
+                            + expectedIssueId);
+            return;
+        }
+        assertSourceStampVerificationIssue(result.getAllErrors(), expectedIssueId);
+    }
+
+    private static void assertSourceStampVerificationWarning(Result result, int expectedIssueId) {
+        assertSourceStampVerificationIssue(result.getAllWarnings(), expectedIssueId);
+    }
+
+    private static void assertSourceStampVerificationIssue(List<ApkVerificationIssue> issues,
+            int expectedIssueId) {
+        StringBuilder msg = new StringBuilder();
+        for (ApkVerificationIssue issue : issues) {
+            if (issue.getIssueId() == expectedIssueId) {
+                return;
+            }
+            if (msg.length() > 0) {
+                msg.append('\n');
+            }
+            msg.append(issue.toString());
+        }
+
+        fail(
+                "APK source stamp verification did not report the expected issue. "
+                        + "Expected error ID: "
+                        + expectedIssueId
+                        + ", actual: "
+                        + (msg.length() > 0 ? msg.toString() : "No reported issues"));
+    }
+
+    /**
+     * Asserts that the provided {@code expectedCertDigests} match their respective signing
+     * certificate digest in the specified {@code result}.
+     *
+     * <p>{@code expectedCertDigests} should be provided in order of the signature schemes with V1
+     * being the first element, V2 the second, etc. If a signer is not expected to be present for
+     * a signature scheme version a {@code null} value should be provided; for instance if only a V3
+     * signing certificate is expected the following should be provided: {@code null, null,
+     * v3ExpectedCertDigest}.
+     *
+     * <p>Note, this method only supports a single signer per signature scheme; if an expected
+     * certificate digest is provided for a signature scheme and multiple signers are found an
+     * assertion exception will be thrown.
+     */
+    private static void assertSigningCertificates(Result result, String... expectedCertDigests)
+            throws Exception {
+        for (int i = 0; i < expectedCertDigests.length; i++) {
+            List<SignerInfo> signers = null;
+            switch (i) {
+                case 0:
+                    signers = result.getV1SchemeSigners();
+                    break;
+                case 1:
+                    signers = result.getV2SchemeSigners();
+                    break;
+                case 2:
+                    signers = result.getV3SchemeSigners();
+                    break;
+                default:
+                    fail("This method only supports verification of the signing certificates up "
+                            + "through the V3 Signature Scheme");
+            }
+            if (expectedCertDigests[i] == null) {
+                assertEquals(
+                        "Did not expect any V" + (i + 1) + " signers, found " + signers.size(), 0,
+                        signers.size());
+                continue;
+            }
+            if (signers.size() != 1) {
+                fail("Expected one V" + (i + 1) + " signer with certificate digest "
+                        + expectedCertDigests[i] + ", found " + signers.size() + " V" + (i + 1)
+                        + " signers");
+            }
+            X509Certificate signingCertificate = signers.get(0).getSigningCertificate();
+            assertNotNull(signingCertificate);
+            assertEquals(expectedCertDigests[i],
+                    toHex(computeSha256DigestBytes(signingCertificate.getEncoded())));
+        }
+    }
+
+    /**
+     * Asserts that the provided {@code expectedCertDigests} match their respective certificate in
+     * the source stamp's lineage with the oldest signer at element 0.
+     *
+     * <p>If no values are provided for the expectedCertDigests, the source stamp's lineage will
+     * be checked for an empty {@code List} indicating the source stamp has not been rotated.
+     */
+    private static void assertSigningCertificatesInLineage(Result result,
+            String... expectedCertDigests) throws Exception {
+        List<X509Certificate> lineageCertificates =
+                result.getSourceStampInfo().getCertificatesInLineage();
+        assertEquals("Unexpected number of lineage certificates", expectedCertDigests.length,
+                lineageCertificates.size());
+        for (int i = 0; i < expectedCertDigests.length; i++) {
+            assertEquals("Stamp lineage mismatch at signer " + i, expectedCertDigests[i],
+                    toHex(computeSha256DigestBytes(lineageCertificates.get(i).getEncoded())));
+        }
+    }
+}
diff --git a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
index 480dc1a..e8234c9 100644
--- a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
+++ b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
@@ -90,6 +90,49 @@
     }
 
     @Test
+    public void testGetTargetSdkVersionFromBinaryAndroidManifest() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("v3-ec-p256-targetSdk-30.apk");
+        assertEquals(30, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetTargetSdkVersion_noUsesSdkElement_returnsDefault() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("v1-only-no-uses-sdk.apk");
+        assertEquals(1, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetTargetSandboxVersionFromBinaryAndroidManifest() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("targetSandboxVersion-2.apk");
+        assertEquals(2, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetTargetSandboxVersion_noTargetSandboxAttribute_returnsDefault()
+            throws Exception {
+        ByteBuffer manifest = getAndroidManifest("original.apk");
+        assertEquals(1, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetVersionCodeFromBinaryAndroidManifest() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("original.apk");
+        assertEquals(10, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetVersionCode_withVersionCodeMajor_returnsOnlyVersionCode() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk");
+        assertEquals(25, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
+    public void testGetLongVersionCodeFromBinaryAndroidManifest() throws Exception {
+        ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk");
+        assertEquals(4294967321L, ApkUtils.getLongVersionCodeFromBinaryAndroidManifest(manifest));
+    }
+
+    @Test
     public void testGetAndroidManifest() throws Exception {
         ByteBuffer manifest = getAndroidManifest("original.apk");
         MessageDigest md = MessageDigest.getInstance("SHA-256");
diff --git a/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk
new file mode 100644
index 0000000..315254d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk
new file mode 100644
index 0000000..f9777c3
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-valid.apk b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk
new file mode 100644
index 0000000..955652e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk
new file mode 100644
index 0000000..c24fa98
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
new file mode 100644
index 0000000..c2e6826
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-stamp.apk b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk
new file mode 100644
index 0000000..745a7aa
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
new file mode 100644
index 0000000..5f1103a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-only-with-stamp.apk b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk
new file mode 100644
index 0000000..ebd4021
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-only-with-stamp.apk b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk
new file mode 100644
index 0000000..5f65214
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk
Binary files differ