Snap for 6877830 from afc4da0bd225c4902e98bcc3389fe2dc0684b896 to sdk-release

Change-Id: Ie7042f2065502eef8b76179311d766a8fec83fa8
diff --git a/Android.bp b/Android.bp
index 274bfd5..ec57fb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -18,7 +18,10 @@
 // ============================================================
 java_library_host {
     name: "apksig",
-    srcs: ["src/main/java/**/*.java"],
+    srcs: [
+        "src/main/java/**/*.java",
+    ],
+    java_version: "1.8",
 }
 
 // apksigner command-line tool for signing APKs and verifying their signatures
@@ -29,5 +32,10 @@
     java_resource_dirs: ["src/apksigner/java"],
     wrapper: "etc/apksigner",
     manifest: "src/apksigner/apksigner.mf",
-    static_libs: ["apksig"],
+    static_libs: [
+        "apksig",
+        "conscrypt-unbundled",
+    ],
+    required: ["libconscrypt_openjdk_jni"],
+    java_version: "1.8",
 }
diff --git a/apksig.iml b/apksig.iml
deleted file mode 100644
index e0c31f4..0000000
--- a/apksig.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
-    </content>
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="inheritedJdk" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 2590fbb..12c0d32 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
 // Generic Gradle project
 
 apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
 
 sourceCompatibility = '1.8'
 
@@ -8,6 +9,31 @@
     jcenter()
 }
 
-dependencies {
-    testCompile 'junit:junit:4.12'
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.11'
+    }
 }
+
+dependencies {
+    implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
+    testImplementation 'junit:junit:4.13'
+}
+
+protobuf {
+    protoc {
+        artifact = 'com.google.protobuf:protoc:3.8.0'
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/etc/apksigner b/etc/apksigner
index 11a7529..d13afc4 100755
--- a/etc/apksigner
+++ b/etc/apksigner
@@ -46,6 +46,8 @@
 if [ ! -r "$libdir/$jarfile" ]; then
     # set apksigner.jar location for the Android tree case
     libdir=`dirname "$progdir"`/framework
+    # also include the library directory for any provider native libraries
+    providerLibdir=`dirname "$progdir"`/lib64
 fi
 
 if [ ! -r "$libdir/$jarfile" ]; then
@@ -71,6 +73,8 @@
     javaOpts="${javaOpts} -${opt}"
     if expr "x${opt}" : "xXmx[0-9]" >/dev/null; then
         defaultMx="no"
+    elif expr "x${opt}" : "xDjava.library.path=" >/dev/null; then
+        defaultLibdir="no"
     fi
     shift
 done
@@ -79,6 +83,10 @@
     javaOpts="${javaOpts} ${defaultMx}"
 fi
 
+if [ "${defaultLibdir}" != "no" ] && [ -n $providerLibdir ]; then
+    javaOpts="${javaOpts} -Djava.library.path=$providerLibdir"
+fi
+
 if [ "$OSTYPE" = "cygwin" ]; then
     # For Cygwin, convert the jarfile path into native Windows style.
     jarpath=`cygpath -w "$libdir/$jarfile"`
diff --git a/etc/apksigner.bat b/etc/apksigner.bat
index 29106c1..934725f 100755
--- a/etc/apksigner.bat
+++ b/etc/apksigner.bat
@@ -22,12 +22,34 @@
 REM and set up progdir to be the fully-qualified pathname of its directory.

 set prog=%~f0

 

-rem Check we have a valid Java.exe in the path.

-set java_exe=

-if exist    "%~dp0..\tools\lib\find_java.bat" call    "%~dp0..\tools\lib\find_java.bat"

-if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"

-if not defined java_exe goto :EOF

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

 

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+exit /b 1

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+exit /b 1

+

+:init

 set jarfile=apksigner.jar

 set "frameworkdir=%~dp0"

 rem frameworkdir must not end with a dir sep.

diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index affa7f5..2f4e680 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -24,6 +24,9 @@
 import com.android.apksig.apk.MinSdkVersionException;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
+
+import org.conscrypt.OpenSSLProvider;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
@@ -77,6 +80,8 @@
             return;
         }
 
+        addProviders();
+
         String cmd = params[0];
         try {
             if ("sign".equals(cmd)) {
@@ -108,6 +113,19 @@
         }
     }
 
+    /**
+     * Adds additional security providers to add support for signature algorithms not covered by
+     * the default providers.
+     */
+    private static void addProviders() {
+        try {
+            Security.addProvider(new OpenSSLProvider());
+        } catch (UnsatisfiedLinkError e) {
+            // This is expected if the library path does not include the native conscrypt library;
+            // the default providers support all but PSS algorithms.
+        }
+    }
+
     private static void sign(String[] params) throws Exception {
         if (params.length == 0) {
             printUsage(HELP_PAGE_SIGN);
@@ -120,18 +138,24 @@
         boolean v1SigningEnabled = true;
         boolean v2SigningEnabled = true;
         boolean v3SigningEnabled = true;
+        boolean v4SigningEnabled = true;
+        boolean forceSourceStampOverwrite = false;
+        boolean verityEnabled = false;
         boolean debuggableApkPermitted = true;
         int minSdkVersion = 1;
         boolean minSdkVersionSpecified = false;
         int maxSdkVersion = Integer.MAX_VALUE;
         List<SignerParams> signers = new ArrayList<>(1);
         SignerParams signerParams = new SignerParams();
+        SignerParams sourceStampSignerParams = new SignerParams();
         SigningCertificateLineage lineage = null;
         List<ProviderInstallSpec> providers = new ArrayList<>();
         ProviderInstallSpec providerParams = new ProviderInstallSpec();
         OptionsParser optionsParser = new OptionsParser(params);
         String optionName;
         String optionOriginalForm = null;
+        boolean v4SigningFlagFound = false;
+        boolean sourceStampFlagFound = false;
         while ((optionName = optionsParser.nextOption()) != null) {
             optionOriginalForm = optionsParser.getOptionOriginalForm();
             if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -152,6 +176,13 @@
                 v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
             } else if ("v3-signing-enabled".equals(optionName)) {
                 v3SigningEnabled = optionsParser.getOptionalBooleanValue(true);
+            } else if ("v4-signing-enabled".equals(optionName)) {
+                v4SigningEnabled = optionsParser.getOptionalBooleanValue(true);
+                v4SigningFlagFound = true;
+            } else if ("force-stamp-overwrite".equals(optionName)) {
+                forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true);
+            } else if ("verity-enabled".equals(optionName)) {
+                verityEnabled = optionsParser.getOptionalBooleanValue(true);
             } else if ("debuggable-apk-permitted".equals(optionName)) {
                 debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true);
             } else if ("next-signer".equals(optionName)) {
@@ -218,6 +249,9 @@
             } else if ("provider-pos".equals(optionName)) {
                 providerParams.position =
                         optionsParser.getRequiredIntValue("JCA Provider position");
+            } else if ("stamp-signer".equals(optionName)) {
+                sourceStampFlagFound = true;
+                sourceStampSignerParams = processSignerParams(optionsParser);
             } else {
                 throw new ParameterException(
                         "Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -267,49 +301,27 @@
             providerInstallSpec.installProvider();
         }
 
+        ApkSigner.SignerConfig sourceStampSignerConfig = null;
         List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size());
         int signerNumber = 0;
         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
             for (SignerParams signer : signers) {
                 signerNumber++;
                 signer.setName("signer #" + signerNumber);
-                try {
-                    signer.loadPrivateKeyAndCerts(passwordRetriever);
-                } catch (ParameterException e) {
-                    System.err.println(
-                            "Failed to load signer \"" + signer.getName() + "\": "
-                                    + e.getMessage());
-                    System.exit(2);
-                    return;
-                } catch (Exception e) {
-                    System.err.println("Failed to load signer \"" + signer.getName() + "\"");
-                    e.printStackTrace();
-                    System.exit(2);
+                ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever);
+                if (signerConfig == null) {
                     return;
                 }
-                String v1SigBasename;
-                if (signer.getV1SigFileBasename() != null) {
-                    v1SigBasename = signer.getV1SigFileBasename();
-                } else if (signer.getKeystoreKeyAlias() != null) {
-                    v1SigBasename = signer.getKeystoreKeyAlias();
-                } else if (signer.getKeyFile() != null) {
-                    String keyFileName = new File(signer.getKeyFile()).getName();
-                    int delimiterIndex = keyFileName.indexOf('.');
-                    if (delimiterIndex == -1) {
-                        v1SigBasename = keyFileName;
-                    } else {
-                        v1SigBasename = keyFileName.substring(0, delimiterIndex);
-                    }
-                } else {
-                    throw new RuntimeException(
-                            "Neither KeyStore key alias nor private key file available");
-                }
-                ApkSigner.SignerConfig signerConfig =
-                        new ApkSigner.SignerConfig.Builder(
-                                v1SigBasename, signer.getPrivateKey(), signer.getCerts())
-                                .build();
                 signerConfigs.add(signerConfig);
             }
+            if (sourceStampFlagFound) {
+                sourceStampSignerParams.setName("stamp signer");
+                sourceStampSignerConfig =
+                        getSignerConfig(sourceStampSignerParams, passwordRetriever);
+                if (sourceStampSignerConfig == null) {
+                    return;
+                }
+            }
         }
 
         if (outputApk == null) {
@@ -330,11 +342,24 @@
                         .setV1SigningEnabled(v1SigningEnabled)
                         .setV2SigningEnabled(v2SigningEnabled)
                         .setV3SigningEnabled(v3SigningEnabled)
+                        .setV4SigningEnabled(v4SigningEnabled)
+                        .setForceSourceStampOverwrite(forceSourceStampOverwrite)
+                        .setVerityEnabled(verityEnabled)
+                        .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
                         .setDebuggableApkPermitted(debuggableApkPermitted)
                         .setSigningCertificateLineage(lineage);
         if (minSdkVersionSpecified) {
             apkSignerBuilder.setMinSdkVersion(minSdkVersion);
         }
+        if (v4SigningEnabled) {
+            final File outputV4SignatureFile =
+                    new File(outputApk.getCanonicalPath() + ".idsig");
+            Files.deleteIfExists(outputV4SignatureFile.toPath());
+            apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
+        }
+        if (sourceStampSignerConfig != null) {
+            apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
+        }
         ApkSigner apkSigner = apkSignerBuilder.build();
         try {
             apkSigner.sign();
@@ -358,6 +383,44 @@
         }
     }
 
+    private static ApkSigner.SignerConfig getSignerConfig(
+            SignerParams signer, PasswordRetriever passwordRetriever) {
+        try {
+            signer.loadPrivateKeyAndCerts(passwordRetriever);
+        } catch (ParameterException e) {
+            System.err.println(
+                    "Failed to load signer \"" + signer.getName() + "\": " + e.getMessage());
+            System.exit(2);
+            return null;
+        } catch (Exception e) {
+            System.err.println("Failed to load signer \"" + signer.getName() + "\"");
+            e.printStackTrace();
+            System.exit(2);
+            return null;
+        }
+        String v1SigBasename;
+        if (signer.getV1SigFileBasename() != null) {
+            v1SigBasename = signer.getV1SigFileBasename();
+        } else if (signer.getKeystoreKeyAlias() != null) {
+            v1SigBasename = signer.getKeystoreKeyAlias();
+        } else if (signer.getKeyFile() != null) {
+            String keyFileName = new File(signer.getKeyFile()).getName();
+            int delimiterIndex = keyFileName.indexOf('.');
+            if (delimiterIndex == -1) {
+                v1SigBasename = keyFileName;
+            } else {
+                v1SigBasename = keyFileName.substring(0, delimiterIndex);
+            }
+        } else {
+            throw new RuntimeException("Neither KeyStore key alias nor private key file available");
+        }
+        ApkSigner.SignerConfig signerConfig =
+                new ApkSigner.SignerConfig.Builder(
+                        v1SigBasename, signer.getPrivateKey(), signer.getCerts())
+                        .build();
+        return signerConfig;
+    }
+
     private static void verify(String[] params) throws Exception {
         if (params.length == 0) {
             printUsage(HELP_PAGE_VERIFY);
@@ -372,6 +435,7 @@
         boolean printCerts = false;
         boolean verbose = false;
         boolean warningsTreatedAsErrors = false;
+        File v4SignatureFile = null;
         OptionsParser optionsParser = new OptionsParser(params);
         String optionName;
         String optionOriginalForm = null;
@@ -392,6 +456,9 @@
             } else if (("help".equals(optionName)) || ("h".equals(optionName))) {
                 printUsage(HELP_PAGE_VERIFY);
                 return;
+            } else if ("v4-signature-file".equals(optionName)) {
+                v4SignatureFile = new File(optionsParser.getRequiredValue(
+                        "Input V4 Signature File"));
             } else if ("in".equals(optionName)) {
                 inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
             } else {
@@ -435,6 +502,14 @@
         if (maxSdkVersionSpecified) {
             apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion);
         }
+        if (v4SignatureFile != null) {
+            if (!v4SignatureFile.exists()) {
+                throw new ParameterException("V4 signature file does not exist: "
+                        + v4SignatureFile.getCanonicalPath());
+            }
+            apkVerifierBuilder.setV4SignatureFile(v4SignatureFile);
+        }
+
         ApkVerifier apkVerifier = apkVerifierBuilder.build();
         ApkVerifier.Result result;
         try {
@@ -465,6 +540,10 @@
                 System.out.println(
                         "Verified using v3 scheme (APK Signature Scheme v3): "
                                 + result.isVerifiedUsingV3Scheme());
+                System.out.println(
+                        "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 (printCerts) {
@@ -523,6 +602,16 @@
             }
         }
 
+        ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
+        if (sourceStampInfo != null) {
+            for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
+                System.err.println("ERROR: SourceStamp: " + error);
+            }
+            for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) {
+                warningsOut.println("WARNING: SourceStamp: " + warning);
+            }
+        }
+
         if (!verified) {
             System.exit(1);
             return;
@@ -678,7 +767,6 @@
         File outputKeyLineage = null;
         String optionName;
         OptionsParser optionsParser = new OptionsParser(params);
-        SigningCertificateLineage lineage = null;
         List<SignerParams> signers = new ArrayList<>(1);
         while ((optionName = optionsParser.nextOption()) != null) {
             if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -704,7 +792,7 @@
         if (inputKeyLineage == null) {
             throw new ParameterException("Input lineage file parameter not present");
         }
-        lineage = getLineageFromInputFile(inputKeyLineage);
+        SigningCertificateLineage lineage = getLineageFromInputFile(inputKeyLineage);
 
         try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
             for (int i = 0; i < signers.size(); i++) {
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index b9d0146..1285810 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -40,6 +40,19 @@
                       "lineage" option to make sure that the app is signed by
                       an appropriate signer on all supported platform versions.
 
+--v4-signing-enabled  Whether to enable signing using APK Signature Scheme v4
+                      (aka v4 signing scheme) introduced in Android 11,
+                      API Level 30. By default, signing using this scheme is
+                      enabled based on min and max SDK version (see
+                      --min-sdk-version and --max-sdk-version).
+
+--force-stamp-overwrite  Whether to overwrite existing source stamp in the
+                      APK, if found. By default, it is set to false. It has no
+                      effect if no source stamp signer config is provided.
+
+--verity-enabled      Whether to enable the verity signature algorithm for the
+                      v2 and v3 signature schemes.
+
 --min-sdk-version     Lowest API Level on which this APK's signatures will be
                       verified. By default, the value from AndroidManifest.xml
                       is used. The higher the value, the stronger security
@@ -91,6 +104,9 @@
                       (aka v1 scheme) signature of this signer. By default,
                       KeyStore key alias or basename of key file is used.
 
+--stamp-signer        The signing information for the signer of the source stamp
+                      to be included in the APK.
+
         PER-SIGNER SIGNING KEY & CERTIFICATE OPTIONS
 There are two ways to provide the signer's private key and certificate: (1) Java
 KeyStore (see --ks), or (2) private key file in PKCS #8 format and certificate
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index e3870e7..154e917 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -16,6 +16,8 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkSigningBlockNotFoundException;
 import com.android.apksig.apk.ApkUtils;
@@ -31,9 +33,8 @@
 import com.android.apksig.util.DataSources;
 import com.android.apksig.util.ReadableDataSink;
 import com.android.apksig.zip.ZipFormatException;
-import java.io.ByteArrayOutputStream;
+
 import java.io.Closeable;
-import java.io.DataOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
@@ -45,20 +46,19 @@
 import java.security.SignatureException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Pattern;
 
 /**
  * APK signer.
  *
- * <p>The signer preserves as much of the input APK as possible. For example, it preserves the
- * order of APK entries and preserves their contents, including compressed form and alignment of
- * data.
+ * <p>The signer preserves as much of the input APK as possible. For example, it preserves the order
+ * of APK entries and preserves their contents, including compressed form and alignment of data.
  *
  * <p>Use {@link Builder} to obtain instances of this signer.
  *
@@ -81,16 +81,19 @@
 
     private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
 
-    /**
-     * Name of the Android manifest ZIP entry in APKs.
-     */
+    /** Name of the Android manifest ZIP entry in APKs. */
     private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
 
     private final List<SignerConfig> mSignerConfigs;
+    private final SignerConfig mSourceStampSignerConfig;
+    private final boolean mForceSourceStampOverwrite;
     private final Integer mMinSdkVersion;
     private final boolean mV1SigningEnabled;
     private final boolean mV2SigningEnabled;
     private final boolean mV3SigningEnabled;
+    private final boolean mV4SigningEnabled;
+    private final boolean mVerityEnabled;
+    private final boolean mV4ErrorReportingEnabled;
     private final boolean mDebuggableApkPermitted;
     private final boolean mOtherSignersSignaturesPreserved;
     private final String mCreatedBy;
@@ -104,14 +107,21 @@
     private final DataSink mOutputApkDataSink;
     private final DataSource mOutputApkDataSource;
 
+    private final File mOutputV4File;
+
     private final SigningCertificateLineage mSigningCertificateLineage;
 
     private ApkSigner(
             List<SignerConfig> signerConfigs,
+            SignerConfig sourceStampSignerConfig,
+            boolean forceSourceStampOverwrite,
             Integer minSdkVersion,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
+            boolean v4SigningEnabled,
+            boolean verityEnabled,
+            boolean v4ErrorReportingEnabled,
             boolean debuggableApkPermitted,
             boolean otherSignersSignaturesPreserved,
             String createdBy,
@@ -121,13 +131,19 @@
             File outputApkFile,
             DataSink outputApkDataSink,
             DataSource outputApkDataSource,
+            File outputV4File,
             SigningCertificateLineage signingCertificateLineage) {
 
         mSignerConfigs = signerConfigs;
+        mSourceStampSignerConfig = sourceStampSignerConfig;
+        mForceSourceStampOverwrite = forceSourceStampOverwrite;
         mMinSdkVersion = minSdkVersion;
         mV1SigningEnabled = v1SigningEnabled;
         mV2SigningEnabled = v2SigningEnabled;
         mV3SigningEnabled = v3SigningEnabled;
+        mV4SigningEnabled = v4SigningEnabled;
+        mVerityEnabled = verityEnabled;
+        mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
         mDebuggableApkPermitted = debuggableApkPermitted;
         mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
         mCreatedBy = createdBy;
@@ -141,6 +157,8 @@
         mOutputApkDataSink = outputApkDataSink;
         mOutputApkDataSource = outputApkDataSource;
 
+        mOutputV4File = outputV4File;
+
         mSigningCertificateLineage = signingCertificateLineage;
     }
 
@@ -150,12 +168,12 @@
      * @throws IOException if an I/O error is encountered while reading or writing the APKs
      * @throws ApkFormatException if the input APK is malformed
      * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
-     *         a required cryptographic algorithm implementation is missing
+     *     a required cryptographic algorithm implementation is missing
      * @throws InvalidKeyException if a signature could not be generated because a signing key is
-     *         not suitable for generating the signature
+     *     not suitable for generating the signature
      * @throws SignatureException if an error occurred while generating or verifying a signature
      * @throws IllegalStateException if this signer's configuration is missing required information
-     *         or if the signing engine is in an invalid state.
+     *     or if the signing engine is in an invalid state.
      */
     public void sign()
             throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
@@ -203,12 +221,9 @@
         }
     }
 
-    private void sign(
-            DataSource inputApk,
-            DataSink outputApkOut,
-            DataSource outputApkIn)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException,
-                            InvalidKeyException, SignatureException {
+    private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+                    SignatureException {
         // Step 1. Find input APK's main ZIP sections
         ApkUtils.ZipSections inputZipSections;
         try {
@@ -240,8 +255,8 @@
         List<CentralDirectoryRecord> inputCdRecords =
                 parseZipCentralDirectory(inputCd, inputZipSections);
 
-        List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(
-                inputCdRecords, inputApkLfhSection);
+        List<Hints.PatternWithRange> pinPatterns =
+                extractPinPatterns(inputCdRecords, inputApkLfhSection);
         List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
 
         // Step 3. Obtain a signer engine instance
@@ -264,9 +279,9 @@
             for (SignerConfig signerConfig : mSignerConfigs) {
                 engineSignerConfigs.add(
                         new DefaultApkSignerEngine.SignerConfig.Builder(
-                                signerConfig.getName(),
-                                signerConfig.getPrivateKey(),
-                                signerConfig.getCertificates())
+                                        signerConfig.getName(),
+                                        signerConfig.getPrivateKey(),
+                                        signerConfig.getCertificates())
                                 .build());
             }
             DefaultApkSignerEngine.Builder signerEngineBuilder =
@@ -274,12 +289,21 @@
                             .setV1SigningEnabled(mV1SigningEnabled)
                             .setV2SigningEnabled(mV2SigningEnabled)
                             .setV3SigningEnabled(mV3SigningEnabled)
+                            .setVerityEnabled(mVerityEnabled)
                             .setDebuggableApkPermitted(mDebuggableApkPermitted)
                             .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
                             .setSigningCertificateLineage(mSigningCertificateLineage);
             if (mCreatedBy != null) {
                 signerEngineBuilder.setCreatedBy(mCreatedBy);
             }
+            if (mSourceStampSignerConfig != null) {
+                signerEngineBuilder.setStampSignerConfig(
+                        new DefaultApkSignerEngine.SignerConfig.Builder(
+                                        mSourceStampSignerConfig.getName(),
+                                        mSourceStampSignerConfig.getPrivateKey(),
+                                        mSourceStampSignerConfig.getCertificates())
+                                .build());
+            }
             signerEngine = signerEngineBuilder.build();
         }
 
@@ -301,12 +325,23 @@
         int lastModifiedTimeForNewEntries = -1;
         long inputOffset = 0;
         long outputOffset = 0;
+        byte[] sourceStampCertificateDigest = null;
         Map<String, CentralDirectoryRecord> outputCdRecordsByName =
                 new HashMap<>(inputCdRecords.size());
         for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
             String entryName = inputCdRecord.getName();
             if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
-                continue;  // We'll re-add below if needed.
+                continue; // We'll re-add below if needed.
+            }
+            if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
+                try {
+                    sourceStampCertificateDigest =
+                            LocalFileRecord.getUncompressedData(
+                                    inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+                } catch (ZipFormatException ex) {
+                    throw new ApkFormatException("Bad source stamp entry");
+                }
+                continue; // Existing source stamp is handled below as needed.
             }
             ApkSignerEngine.InputJarEntryInstructions entryInstructions =
                     signerEngine.inputJarEntry(entryName);
@@ -397,8 +432,8 @@
                         }
                     }
                     if (pinFileHeader) {
-                        pinByteRanges.add(new Hints.ByteRange(outputLocalFileHeaderOffset,
-                                                              outputDataOffset));
+                        pinByteRanges.add(
+                                new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset));
                     }
                 }
 
@@ -436,94 +471,94 @@
             }
         }
 
-        // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
+        if (lastModifiedDateForNewEntries == -1) {
+            lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+            lastModifiedTimeForNewEntries = 0;
+        }
+
+        // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output
+        // more Local File Header + data entries and add to the list of output Central Directory
+        // records.
+        if (signerEngine.isEligibleForSourceStamp()) {
+            byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
+            if (mForceSourceStampOverwrite
+                    || sourceStampCertificateDigest == null
+                    || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
+                outputOffset +=
+                        outputDataToOutputApk(
+                                SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
+                                uncompressedData,
+                                outputOffset,
+                                outputCdRecords,
+                                lastModifiedTimeForNewEntries,
+                                lastModifiedDateForNewEntries,
+                                outputApkOut);
+            } else {
+                throw new ApkFormatException(
+                        String.format(
+                                "Cannot generate SourceStamp. APK contains an existing entry with"
+                                    + " the name: %s, and it is different than the provided source"
+                                    + " stamp certificate",
+                                SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
+            }
+        }
+
+        // Step 8. Generate and output JAR signatures, if necessary. This may output more Local File
         // Header + data entries and add to the list of output Central Directory records.
         ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
                 signerEngine.outputJarEntries();
         if (outputJarSignatureRequest != null) {
-            if (lastModifiedDateForNewEntries == -1) {
-                lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
-                lastModifiedTimeForNewEntries = 0;
-            }
             for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
                     outputJarSignatureRequest.getAdditionalJarEntries()) {
                 String entryName = entry.getName();
                 byte[] uncompressedData = entry.getData();
-                ZipUtils.DeflateResult deflateResult =
-                        ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
-                byte[] compressedData = deflateResult.output;
-                long uncompressedDataCrc32 = deflateResult.inputCrc32;
 
                 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                         signerEngine.outputJarEntry(entryName);
                 if (inspectEntryRequest != null) {
-                    inspectEntryRequest.getDataSink().consume(
-                            uncompressedData, 0, uncompressedData.length);
+                    inspectEntryRequest
+                            .getDataSink()
+                            .consume(uncompressedData, 0, uncompressedData.length);
                     inspectEntryRequest.done();
                 }
 
-                long localFileHeaderOffset = outputOffset;
                 outputOffset +=
-                        LocalFileRecord.outputRecordWithDeflateCompressedData(
+                        outputDataToOutputApk(
                                 entryName,
+                                uncompressedData,
+                                outputOffset,
+                                outputCdRecords,
                                 lastModifiedTimeForNewEntries,
                                 lastModifiedDateForNewEntries,
-                                compressedData,
-                                uncompressedDataCrc32,
-                                uncompressedData.length,
                                 outputApkOut);
-
-
-                outputCdRecords.add(
-                        CentralDirectoryRecord.createWithDeflateCompressedData(
-                                entryName,
-                                lastModifiedTimeForNewEntries,
-                                lastModifiedDateForNewEntries,
-                                uncompressedDataCrc32,
-                                compressedData.length,
-                                uncompressedData.length,
-                                localFileHeaderOffset));
             }
             outputJarSignatureRequest.done();
         }
 
         if (pinByteRanges != null) {
-            pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));  // central dir
+            pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir
             String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
             byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
-            ZipUtils.DeflateResult deflateResult =
-                    ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
-            byte[] compressedData = deflateResult.output;
-            long uncompressedDataCrc32 = deflateResult.inputCrc32;
-            long localFileHeaderOffset = outputOffset;
             outputOffset +=
-                    LocalFileRecord.outputRecordWithDeflateCompressedData(
-                        entryName,
-                        lastModifiedTimeForNewEntries,
-                        lastModifiedDateForNewEntries,
-                        compressedData,
-                        uncompressedDataCrc32,
-                        uncompressedData.length,
-                        outputApkOut);
-            outputCdRecords.add(
-                CentralDirectoryRecord.createWithDeflateCompressedData(
-                    entryName,
-                    lastModifiedTimeForNewEntries,
-                    lastModifiedDateForNewEntries,
-                    uncompressedDataCrc32,
-                    compressedData.length,
-                    uncompressedData.length,
-                    localFileHeaderOffset));
+                    outputDataToOutputApk(
+                            entryName,
+                            uncompressedData,
+                            outputOffset,
+                            outputCdRecords,
+                            lastModifiedTimeForNewEntries,
+                            lastModifiedDateForNewEntries,
+                            outputApkOut);
         }
 
-        // Step 8. Construct output ZIP Central Directory in an in-memory buffer
+        // Step 9. Construct output ZIP Central Directory in an in-memory buffer
         long outputCentralDirSizeBytes = 0;
         for (CentralDirectoryRecord record : outputCdRecords) {
             outputCentralDirSizeBytes += record.getSize();
         }
         if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
             throw new IOException(
-                    "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+                    "Output ZIP Central Directory too large: "
+                            + outputCentralDirSizeBytes
                             + " bytes");
         }
         ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
@@ -535,7 +570,7 @@
         long outputCentralDirStartOffset = outputOffset;
         int outputCentralDirRecordCount = outputCdRecords.size();
 
-        // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
+        // Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer
         ByteBuffer outputEocd =
                 EocdRecord.createWithModifiedCentralDirectoryInfo(
                         inputZipSections.getZipEndOfCentralDirectory(),
@@ -543,7 +578,8 @@
                         outputCentralDirDataSource.size(),
                         outputCentralDirStartOffset);
 
-        // Step 10. Generate and output APK Signature Scheme v2 and/or v3 signatures, if necessary.
+        // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or
+        // SourceStamp signatures, if necessary.
         // This may insert an APK Signing Block just before the output's ZIP Central Directory
         ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
                 signerEngine.outputZipSections2(
@@ -556,22 +592,61 @@
             outputApkOut.consume(ByteBuffer.allocate(padding));
             byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
             outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
-            ZipUtils.setZipEocdCentralDirectoryOffset(outputEocd,
+            ZipUtils.setZipEocdCentralDirectoryOffset(
+                    outputEocd,
                     outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
             outputApkSigningBlockRequest.done();
         }
 
-        // Step 11. Output ZIP Central Directory and ZIP End of Central Directory
+        // Step 12. Output ZIP Central Directory and ZIP End of Central Directory
         outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
         outputApkOut.consume(outputEocd);
         signerEngine.outputDone();
+
+        // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
+        if (mV4SigningEnabled) {
+            signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled);
+        }
+    }
+
+    private static long outputDataToOutputApk(
+            String entryName,
+            byte[] uncompressedData,
+            long localFileHeaderOffset,
+            List<CentralDirectoryRecord> outputCdRecords,
+            int lastModifiedTimeForNewEntries,
+            int lastModifiedDateForNewEntries,
+            DataSink outputApkOut)
+            throws IOException {
+        ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+        byte[] compressedData = deflateResult.output;
+        long uncompressedDataCrc32 = deflateResult.inputCrc32;
+        long numOfDataBytes =
+                LocalFileRecord.outputRecordWithDeflateCompressedData(
+                        entryName,
+                        lastModifiedTimeForNewEntries,
+                        lastModifiedDateForNewEntries,
+                        compressedData,
+                        uncompressedDataCrc32,
+                        uncompressedData.length,
+                        outputApkOut);
+        outputCdRecords.add(
+                CentralDirectoryRecord.createWithDeflateCompressedData(
+                        entryName,
+                        lastModifiedTimeForNewEntries,
+                        lastModifiedDateForNewEntries,
+                        uncompressedDataCrc32,
+                        compressedData.length,
+                        uncompressedData.length,
+                        localFileHeaderOffset));
+        return numOfDataBytes;
     }
 
     private static void fulfillInspectInputJarEntryRequest(
             DataSource lfhSection,
             LocalFileRecord localFileRecord,
             ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
-                    throws IOException, ApkFormatException {
+            throws IOException, ApkFormatException {
         try {
             localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
         } catch (ZipFormatException e) {
@@ -594,7 +669,8 @@
             DataSource inputLfhSection,
             LocalFileRecord inputRecord,
             DataSink outputLfhSection,
-            long outputOffset) throws IOException {
+            long outputOffset)
+            throws IOException {
         long inputOffset = inputRecord.getStartOffsetInArchive();
         if (inputOffset == outputOffset) {
             // This record's data will be aligned same as in the input APK.
@@ -628,11 +704,14 @@
                         inputRecord.getExtra(),
                         outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
                         dataAlignmentMultiple);
-        long dataOffset = inputRecord.getDataStartOffsetInRecord() +
-                          aligningExtra.remaining() -
-                          inputRecord.getExtra().remaining();
-        return new OutputSizeAndDataOffset(inputRecord.outputRecordWithModifiedExtra(
-                inputLfhSection, aligningExtra, outputLfhSection), dataOffset);
+        long dataOffset =
+                (long) inputRecord.getDataStartOffsetInRecord()
+                        + aligningExtra.remaining()
+                        - inputRecord.getExtra().remaining();
+        return new OutputSizeAndDataOffset(
+                inputRecord.outputRecordWithModifiedExtra(
+                        inputLfhSection, aligningExtra, outputLfhSection),
+                dataOffset);
     }
 
     private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
@@ -650,7 +729,7 @@
             //   * uint16 size
             //   * 'size' bytes: payload
             while (extra.remaining() >= 4) {
-                short headerId  = extra.getShort();
+                short headerId = extra.getShort();
                 int dataSize = ZipUtils.getUnsignedInt16(extra);
                 if (dataSize > extra.remaining()) {
                     // Malformed field -- insufficient input remaining
@@ -679,9 +758,7 @@
     }
 
     private static ByteBuffer createExtraFieldToAlignData(
-            ByteBuffer original,
-            long extraStartOffset,
-            int dataAlignmentMultiple) {
+            ByteBuffer original, long extraStartOffset, int dataAlignmentMultiple) {
         if (dataAlignmentMultiple <= 1) {
             return original;
         }
@@ -696,7 +773,7 @@
         //   * uint16 size
         //   * 'size' bytes: payload
         while (original.remaining() >= 4) {
-            short headerId  = original.getShort();
+            short headerId = original.getShort();
             int dataSize = ZipUtils.getUnsignedInt16(original);
             if (dataSize > original.remaining()) {
                 // Malformed field -- insufficient input remaining
@@ -726,7 +803,8 @@
         //      * remaining bytes -- padding to achieve alignment of data which starts after the
         //        extra field
         long dataMinStartOffset =
-                extraStartOffset + result.position()
+                extraStartOffset
+                        + result.position()
                         + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
         int paddingSizeBytes =
                 (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
@@ -741,8 +819,8 @@
     }
 
     private static ByteBuffer getZipCentralDirectory(
-            DataSource apk,
-            ApkUtils.ZipSections apkSections) throws IOException, ApkFormatException {
+            DataSource apk, ApkUtils.ZipSections apkSections)
+            throws IOException, ApkFormatException {
         long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
         if (cdSizeBytes > Integer.MAX_VALUE) {
             throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
@@ -754,8 +832,7 @@
     }
 
     private static List<CentralDirectoryRecord> parseZipCentralDirectory(
-            ByteBuffer cd,
-            ApkUtils.ZipSections apkSections) throws ApkFormatException {
+            ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
         long cdOffset = apkSections.getZipCentralDirectoryOffset();
         int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
         List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
@@ -767,8 +844,10 @@
                 cdRecord = CentralDirectoryRecord.getRecord(cd);
             } catch (ZipFormatException e) {
                 throw new ApkFormatException(
-                        "Malformed ZIP Central Directory record #" + (i + 1)
-                                + " at file offset " + (cdOffset + offsetInsideCd),
+                        "Malformed ZIP Central Directory record #"
+                                + (i + 1)
+                                + " at file offset "
+                                + (cdOffset + offsetInsideCd),
                         e);
             }
             String entryName = cdRecord.getName();
@@ -780,15 +859,17 @@
         }
         if (cd.hasRemaining()) {
             throw new ApkFormatException(
-                    "Unused space at the end of ZIP Central Directory: " + cd.remaining()
-                            + " bytes starting at file offset " + (cdOffset + cd.position()));
+                    "Unused space at the end of ZIP Central Directory: "
+                            + cd.remaining()
+                            + " bytes starting at file offset "
+                            + (cdOffset + cd.position()));
         }
 
         return cdRecords;
     }
 
     private static CentralDirectoryRecord findCdRecord(
-        List<CentralDirectoryRecord> cdRecords, String name) {
+            List<CentralDirectoryRecord> cdRecords, String name) {
         for (CentralDirectoryRecord cdRecord : cdRecords) {
             if (name.equals(cdRecord.getName())) {
                 return cdRecord;
@@ -803,7 +884,7 @@
      */
     static ByteBuffer getAndroidManifestFromApk(
             List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
-                    throws IOException, ApkFormatException, ZipFormatException {
+            throws IOException, ApkFormatException, ZipFormatException {
         CentralDirectoryRecord androidManifestCdRecord =
                 findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
         if (androidManifestCdRecord == null) {
@@ -816,12 +897,12 @@
     }
 
     /**
-     * Return list of pin patterns embedded in the pin pattern asset
-     * file.  If no such file, return {@code null}.
+     * Return list of pin patterns embedded in the pin pattern asset file. If no such file, return
+     * {@code null}.
      */
     private static List<Hints.PatternWithRange> extractPinPatterns(
             List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
-                    throws IOException, ApkFormatException {
+            throws IOException, ApkFormatException {
         CentralDirectoryRecord pinListCdRecord =
                 findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
         List<Hints.PatternWithRange> pinPatterns = null;
@@ -829,8 +910,9 @@
             pinPatterns = new ArrayList<>();
             byte[] patternBlob;
             try {
-                patternBlob = LocalFileRecord.getUncompressedData(
-                    lhfSection, pinListCdRecord, lhfSection.size());
+                patternBlob =
+                        LocalFileRecord.getUncompressedData(
+                                lhfSection, pinListCdRecord, lhfSection.size());
             } catch (ZipFormatException ex) {
                 throw new ApkFormatException("Bad " + pinListCdRecord);
             }
@@ -845,14 +927,13 @@
      */
     private static int getMinSdkVersionFromApk(
             List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
-                    throws IOException, MinSdkVersionException {
+            throws IOException, MinSdkVersionException {
         ByteBuffer androidManifest;
         try {
             androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection);
         } catch (ZipFormatException | ApkFormatException e) {
             throw new MinSdkVersionException(
-                    "Failed to determine APK's minimum supported Android platform version",
-                    e);
+                    "Failed to determine APK's minimum supported Android platform version", e);
         }
         return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
     }
@@ -868,24 +949,18 @@
         private final List<X509Certificate> mCertificates;
 
         private SignerConfig(
-                String name,
-                PrivateKey privateKey,
-                List<X509Certificate> certificates) {
+                String name, PrivateKey privateKey, List<X509Certificate> certificates) {
             mName = name;
             mPrivateKey = privateKey;
             mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
         }
 
-        /**
-         * Returns the name of this signer.
-         */
+        /** Returns the name of this signer. */
         public String getName() {
             return mName;
         }
 
-        /**
-         * Returns the signing key of this signer.
-         */
+        /** Returns the signing key of this signer. */
         public PrivateKey getPrivateKey() {
             return mPrivateKey;
         }
@@ -898,9 +973,7 @@
             return mCertificates;
         }
 
-        /**
-         * Builder of {@link SignerConfig} instances.
-         */
+        /** Builder of {@link SignerConfig} instances. */
         public static class Builder {
             private final String mName;
             private final PrivateKey mPrivateKey;
@@ -910,15 +983,12 @@
              * Constructs a new {@code Builder}.
              *
              * @param name signer's name. The name is reflected in the name of files comprising the
-             *        JAR signature of the APK.
+             *     JAR signature of the APK.
              * @param privateKey signing key
              * @param certificates list of one or more X.509 certificates. The subject public key of
-             *        the first certificate must correspond to the {@code privateKey}.
+             *     the first certificate must correspond to the {@code privateKey}.
              */
-            public Builder(
-                    String name,
-                    PrivateKey privateKey,
-                    List<X509Certificate> certificates) {
+            public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
                 if (name.isEmpty()) {
                     throw new IllegalArgumentException("Empty name");
                 }
@@ -932,10 +1002,7 @@
              * this builder.
              */
             public SignerConfig build() {
-                return new SignerConfig(
-                        mName,
-                        mPrivateKey,
-                        mCertificates);
+                return new SignerConfig(mName, mPrivateKey, mCertificates);
             }
         }
     }
@@ -944,19 +1011,24 @@
      * Builder of {@link ApkSigner} instances.
      *
      * <p>The builder requires the following information to construct a working {@code ApkSigner}:
+     *
      * <ul>
-     * <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,</li>
-     * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,</li>
-     * <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
-     *     variants.
-     * </li>
+     *   <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+     *   <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+     *   <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+     *       variants.
      * </ul>
      */
     public static class Builder {
         private final List<SignerConfig> mSignerConfigs;
+        private SignerConfig mSourceStampSignerConfig;
+        private boolean mForceSourceStampOverwrite = false;
         private boolean mV1SigningEnabled = true;
         private boolean mV2SigningEnabled = true;
         private boolean mV3SigningEnabled = true;
+        private boolean mV4SigningEnabled = true;
+        private boolean mVerityEnabled = false;
+        private boolean mV4ErrorReportingEnabled = false;
         private boolean mDebuggableApkPermitted = true;
         private boolean mOtherSignersSignaturesPreserved;
         private String mCreatedBy;
@@ -971,6 +1043,8 @@
         private DataSink mOutputApkDataSink;
         private DataSource mOutputApkDataSource;
 
+        private File mOutputV4File;
+
         private SigningCertificateLineage mSigningCertificateLineage;
 
         // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
@@ -985,12 +1059,12 @@
         /**
          * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
          * signer configurations. The resulting signer may be further customized through this
-         * builder's setters, such as {@link #setMinSdkVersion(int)},
-         * {@link #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)},
-         * {@link #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
+         * builder's setters, such as {@link #setMinSdkVersion(int)}, {@link
+         * #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, {@link
+         * #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
          *
-         * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where
-         * more control over low-level details of signing is desired.
+         * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
+         * control over low-level details of signing is desired.
          */
         public Builder(List<SignerConfig> signerConfigs) {
             if (signerConfigs.isEmpty()) {
@@ -1007,10 +1081,10 @@
         }
 
         /**
-         * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the
-         * provided signing engine. This is meant for advanced use cases where more control is
-         * needed over the lower-level details of signing. For typical use cases,
-         * {@link #Builder(List)} is more appropriate.
+         * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
+         * signing engine. This is meant for advanced use cases where more control is needed over
+         * the lower-level details of signing. For typical use cases, {@link #Builder(List)} is more
+         * appropriate.
          */
         public Builder(ApkSignerEngine signerEngine) {
             if (signerEngine == null) {
@@ -1020,6 +1094,22 @@
             mSignerConfigs = null;
         }
 
+        /** Sets the signing configuration of the source stamp to be embedded in the APK. */
+        public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) {
+            mSourceStampSignerConfig = sourceStampSignerConfig;
+            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
+         */
+        public Builder setForceSourceStampOverwrite(boolean force) {
+            mForceSourceStampOverwrite = force;
+            return this;
+        }
+
         /**
          * Sets the APK to be signed.
          *
@@ -1071,8 +1161,8 @@
          * the sink.
          *
          * <p>This variant of {@code setOutputApk} is useful for avoiding writing the output APK to
-         * a file. For example, an in-memory data sink, such as
-         * {@link DataSinks#newInMemoryDataSink()}, could be used instead of a file.
+         * a file. For example, an in-memory data sink, such as {@link
+         * DataSinks#newInMemoryDataSink()}, could be used instead of a file.
          *
          * @see #setOutputApk(File)
          * @see #setOutputApk(DataSink, DataSource)
@@ -1085,8 +1175,8 @@
         }
 
         /**
-         * Sets the sink which will receive the output (signed) APK. Data received by the
-         * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+         * Sets the sink which will receive the output (signed) APK. Data received by the {@code
+         * outputApkOut} sink must be visible through the {@code outputApkIn} data source.
          *
          * <p>This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the
          * sink and the source to be different objects.
@@ -1108,8 +1198,20 @@
         }
 
         /**
-         * Sets the minimum Android platform version (API Level) on which APK signatures produced
-         * by the signer being built must verify. This method is useful for overriding the default
+         * Sets the location of the V4 output file. {@code ApkSigner} will create this file if it
+         * doesn't exist.
+         */
+        public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) {
+            if (v4SignatureOutputFile == null) {
+                throw new NullPointerException("v4HashRootOutputFile == null");
+            }
+            mOutputV4File = v4SignatureOutputFile;
+            return this;
+        }
+
+        /**
+         * Sets the minimum Android platform version (API Level) on which APK signatures produced by
+         * the signer being built must verify. This method is useful for overriding the default
          * behavior where the minimum API Level is obtained from the {@code android:minSdkVersion}
          * attribute of the APK's {@code AndroidManifest.xml}.
          *
@@ -1119,8 +1221,8 @@
          * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
          * with an {@link ApkSignerEngine}.
          *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
          */
         public Builder setMinSdkVersion(int minSdkVersion) {
             checkInitializedWithoutEngine();
@@ -1131,21 +1233,21 @@
         /**
          * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
          *
-         * <p>By default, whether APK is signed using JAR signing is determined by
-         * {@code ApkSigner}, based on the platform versions supported by the APK or specified using
-         * {@link #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which
-         * don't verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
+         * <p>By default, whether APK is signed using JAR signing is determined by {@code
+         * ApkSigner}, based on the platform versions supported by the APK or specified using {@link
+         * #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which don't
+         * verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
          *
          * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
          * with an {@link ApkSignerEngine}.
          *
-         * @param enabled {@code true} to require the APK to be signed using JAR signing,
-         *        {@code false} to require the APK to not be signed using JAR signing.
-         *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
-         *
-         * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR signing</a>
+         * @param enabled {@code true} to require the APK to be signed using JAR signing, {@code
+         *     false} to require the APK to not be signed using JAR signing.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         * @see <a
+         *     href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR
+         *     signing</a>
          */
         public Builder setV1SigningEnabled(boolean enabled) {
             checkInitializedWithoutEngine();
@@ -1165,13 +1267,11 @@
          * with an {@link ApkSignerEngine}.
          *
          * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
-         *        v2, {@code false} to require the APK to not be signed using APK Signature Scheme
-         *        v2.
-         *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
-         *
-         * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+         *     v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
+         * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature
+         *     Scheme v2</a>
          */
         public Builder setV2SigningEnabled(boolean enabled) {
             checkInitializedWithoutEngine();
@@ -1189,15 +1289,14 @@
          *
          * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
          * with an {@link ApkSignerEngine}.
+         *
          * <p><em>Note:</em> APK Signature Scheme v3 only supports a single signing certificate, but
          * may take multiple signers mapping to different targeted platform versions.
          *
          * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
-         *        v3, {@code false} to require the APK to not be signed using APK Signature Scheme
-         *        v3.
-         *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
+         *     v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3.
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
          */
         public Builder setV3SigningEnabled(boolean enabled) {
             checkInitializedWithoutEngine();
@@ -1211,8 +1310,53 @@
         }
 
         /**
-         * Sets whether the APK should be signed even if it is marked as debuggable
-         * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+         * Sets whether the APK should be signed using APK Signature Scheme v4.
+         *
+         * <p>V4 signing requires that the APK be v2 or v3 signed.
+         *
+         * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
+         *     or v3 and generate an v4 signature file
+         */
+        public Builder setV4SigningEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV4SigningEnabled = enabled;
+            mV4ErrorReportingEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether errors during v4 signing should be reported and halt the signing process.
+         *
+         * <p>Error reporting for v4 signing is disabled by default, but will be enabled if the
+         * caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful
+         * for tools that enable v4 signing by default but don't want to fail the signing process if
+         * the user did not explicitly request the v4 signing.
+         *
+         * @param enabled {@code false} to prevent errors encountered during the V4 signing from
+         *     halting the signing process
+         */
+        public Builder setV4ErrorReportingEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mV4ErrorReportingEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether to enable the verity signature algorithm for the v2 and v3 signature
+         * schemes.
+         *
+         * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
+         *     v2 and v3 signature blocks.
+         */
+        public Builder setVerityEnabled(boolean enabled) {
+            checkInitializedWithoutEngine();
+            mVerityEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed even if it is marked as debuggable ({@code
+         * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
          * compatibility reasons, the default value of this setting is {@code true}.
          *
          * <p>It is dangerous to sign debuggable APKs with production/release keys because Android
@@ -1237,8 +1381,8 @@
          * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
          * with an {@link ApkSignerEngine}.
          *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
          */
         public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
             checkInitializedWithoutEngine();
@@ -1252,8 +1396,8 @@
          * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
          * with an {@link ApkSignerEngine}.
          *
-         * @throws IllegalStateException if this builder was initialized with an
-         *         {@link ApkSignerEngine}
+         * @throws IllegalStateException if this builder was initialized with an {@link
+         *     ApkSignerEngine}
          */
         public Builder setCreatedBy(String createdBy) {
             checkInitializedWithoutEngine();
@@ -1272,7 +1416,7 @@
         }
 
         /**
-         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme.  This
+         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
          * structure provides proof of signing certificate rotation linking {@link SignerConfig}
          * objects to previous ones.
          */
@@ -1290,10 +1434,10 @@
          * this builder.
          */
         public ApkSigner build() {
-
             if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
-                throw new IllegalStateException("Builder configured to both enable and disable APK "
-                        + "Signature Scheme v3 signing");
+                throw new IllegalStateException(
+                        "Builder configured to both enable and disable APK "
+                                + "Signature Scheme v3 signing");
             }
 
             if (mV3SigningExplicitlyDisabled) {
@@ -1304,14 +1448,31 @@
                 mV3SigningEnabled = true;
             }
 
+            // If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing
+            // must be disabled as well as it is dependent on V2/V3.
+            if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) {
+                if (!mV4ErrorReportingEnabled) {
+                    mV4SigningEnabled = false;
+                } else {
+                    throw new IllegalStateException(
+                            "APK Signature Scheme v4 signing requires at least "
+                                    + "v2 or v3 signing to be enabled");
+                }
+            }
+
             // TODO - if v3 signing is enabled, check provided signers and history to see if valid
 
             return new ApkSigner(
                     mSignerConfigs,
+                    mSourceStampSignerConfig,
+                    mForceSourceStampOverwrite,
                     mMinSdkVersion,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
                     mV3SigningEnabled,
+                    mV4SigningEnabled,
+                    mVerityEnabled,
+                    mV4ErrorReportingEnabled,
                     mDebuggableApkPermitted,
                     mOtherSignersSignaturesPreserved,
                     mCreatedBy,
@@ -1321,6 +1482,7 @@
                     mOutputApkFile,
                     mOutputApkDataSink,
                     mOutputApkDataSource,
+                    mOutputV4File,
                     mSigningCertificateLineage);
         }
     }
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 138bc38..c79f232 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -20,9 +20,10 @@
 import com.android.apksig.util.DataSink;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.RunnablesExecutor;
+
 import java.io.Closeable;
+import java.io.File;
 import java.io.IOException;
-import java.lang.UnsupportedOperationException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
@@ -220,7 +221,7 @@
      */
     OutputJarSignatureRequest outputJarEntries()
             throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
-                    SignatureException, IllegalStateException;
+            SignatureException, IllegalStateException;
 
     /**
      * Indicates to this engine that the ZIP sections comprising the output APK have been output.
@@ -258,8 +259,8 @@
             DataSource zipEntries,
             DataSource zipCentralDirectory,
             DataSource zipEocd)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException,
-                            InvalidKeyException, SignatureException, IllegalStateException;
+            throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            InvalidKeyException, SignatureException, IllegalStateException;
 
     /**
      * Indicates to this engine that the ZIP sections comprising the output APK have been output.
@@ -293,8 +294,8 @@
             DataSource zipEntries,
             DataSource zipCentralDirectory,
             DataSource zipEocd)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException,
-                            InvalidKeyException, SignatureException, IllegalStateException;
+            throws IOException, ApkFormatException, NoSuchAlgorithmException,
+            InvalidKeyException, SignatureException, IllegalStateException;
 
     /**
      * Indicates to this engine that the signed APK was output.
@@ -308,6 +309,35 @@
     void outputDone() throws IllegalStateException;
 
     /**
+     * Generates a V4 signature proto and write to output file.
+     *
+     * @param data Input data to calculate a verity hash tree and hash root
+     * @param outputFile To store the serialized V4 Signature.
+     * @param ignoreFailures Whether any failures will be silently ignored.
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *         not suitable for generating the signature
+     * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+     *         cryptographic algorithm implementation is missing
+     * @throws SignatureException if an error occurred while generating a signature
+     * @throws IOException if protobuf fails to be serialized and written to file
+     */
+    void signV4(DataSource data, File outputFile, boolean ignoreFailures)
+            throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
+
+    /**
+     * Checks if the signing configuration provided to the engine is capable of creating a
+     * SourceStamp.
+     */
+    default boolean isEligibleForSourceStamp() {
+        return false;
+    }
+
+    /** Generates the digest of the certificate used to sign the source stamp. */
+    default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        return new byte[0];
+    }
+
+    /**
      * Indicates to this engine that it will no longer be used. Invoking this on an already closed
      * engine is OK.
      *
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 3e1e7da..f2d0fbc 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -16,21 +16,32 @@
 
 package com.android.apksig;
 
+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.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 com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.AndroidBinXmlParser;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
-import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
 import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
 import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 import com.android.apksig.util.RunnablesExecutor;
 import com.android.apksig.zip.ZipFormatException;
+
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
@@ -75,6 +86,7 @@
 
     private final File mApkFile;
     private final DataSource mApkDataSource;
+    private final File mV4SignatureFile;
 
     private final Integer mMinSdkVersion;
     private final int mMaxSdkVersion;
@@ -82,10 +94,12 @@
     private ApkVerifier(
             File apkFile,
             DataSource apkDataSource,
+            File v4SignatureFile,
             Integer minSdkVersion,
             int maxSdkVersion) {
         mApkFile = apkFile;
         mApkDataSource = apkDataSource;
+        mV4SignatureFile = v4SignatureFile;
         mMinSdkVersion = minSdkVersion;
         mMaxSdkVersion = maxSdkVersion;
     }
@@ -186,6 +200,8 @@
         }
 
         Result result = new Result();
+        Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+                new HashMap<>();
 
         // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
         // name, but the verifiers use this parameter as the schemes supported by the target SDK
@@ -205,7 +221,7 @@
                     SUPPORTED_APK_SIG_SCHEME_NAMES.get(
                             ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
         } else {
-            supportedSchemeNames = Collections.EMPTY_MAP;
+            supportedSchemeNames = Collections.emptyMap();
         }
         // 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
@@ -225,6 +241,9 @@
                                     maxSdkVersion);
                     foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
                     result.mergeFrom(v3Result);
+                    signatureSchemeApkContentDigests.put(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+                            getApkContentDigestsFromSigningSchemeResult(v3Result));
                 } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
                     // v3 signature not required
                 }
@@ -250,6 +269,9 @@
                                     maxSdkVersion);
                     foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
                     result.mergeFrom(v2Result);
+                    signatureSchemeApkContentDigests.put(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+                            getApkContentDigestsFromSigningSchemeResult(v2Result));
                 } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
                     // v2 signature not required
                 }
@@ -257,6 +279,18 @@
                     return result;
                 }
             }
+
+            // If v4 file is specified, use additional verification on it
+            if (mV4SignatureFile != null) {
+                final ApkSigningBlockUtils.Result v4Result =
+                        V4SchemeVerifier.verify(apk, mV4SignatureFile);
+                foundApkSigSchemeIds.add(
+                        ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+                result.mergeFrom(v4Result);
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
         }
 
         // Android O and newer requires that APKs targeting security sandbox version 2 and higher
@@ -276,6 +310,9 @@
             }
         }
 
+        List<CentralDirectoryRecord> cdRecords =
+                V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
         // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
         // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
         // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
@@ -290,6 +327,46 @@
                             minSdkVersion,
                             maxSdkVersion);
             result.mergeFrom(v1Result);
+            signatureSchemeApkContentDigests.put(
+                    ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+                    getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+        }
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Verify the SourceStamp, if found in the APK.
+        try {
+            CentralDirectoryRecord sourceStampCdRecord = null;
+            for (CentralDirectoryRecord cdRecord : cdRecords) {
+                if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+                        cdRecord.getName())) {
+                    sourceStampCdRecord = cdRecord;
+                    break;
+                }
+            }
+            // If SourceStamp file is found inside the APK, there must be a SourceStamp
+            // block in the APK signing block as well.
+            if (sourceStampCdRecord != null) {
+                byte[] sourceStampCertificateDigest =
+                        LocalFileRecord.getUncompressedData(
+                                apk,
+                                sourceStampCdRecord,
+                                zipSections.getZipCentralDirectoryOffset());
+                ApkSigningBlockUtils.Result sourceStampResult =
+                        V2SourceStampVerifier.verify(
+                                apk,
+                                zipSections,
+                                sourceStampCertificateDigest,
+                                signatureSchemeApkContentDigests,
+                                Math.max(minSdkVersion, AndroidSdkVersion.R),
+                                maxSdkVersion);
+                result.mergeFrom(sourceStampResult);
+            }
+        } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+            result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+        } catch (ZipFormatException e) {
+            throw new ApkFormatException("Failed to read APK", e);
         }
         if (result.containsErrors()) {
             return result;
@@ -308,7 +385,7 @@
                 try {
                     v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
                 } catch (CertificateEncodingException e) {
-                    throw new RuntimeException(
+                    throw new IllegalStateException(
                             "Failed to encode JAR signer " + signer.getName() + " certs", e);
                 }
             }
@@ -316,7 +393,7 @@
                 try {
                     v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
                 } catch (CertificateEncodingException e) {
-                    throw new RuntimeException(
+                    throw new IllegalStateException(
                             "Failed to encode APK Signature Scheme v2 signer (index: "
                                     + signer.getIndex() + ") certs",
                             e);
@@ -400,6 +477,85 @@
             }
         }
 
+
+        // If there is a v4 scheme signer, make sure that their certificates match.
+        // The apkDigest field in the v4 signature should match the selected v2/v3.
+        if (result.isVerifiedUsingV4Scheme()) {
+            List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
+            if (v4Signers.size() != 1) {
+                result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+            }
+
+            List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
+                    v4Signers.get(0).getContentDigests();
+            if (digestsFromV4.size() != 1) {
+                result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+            }
+            final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
+
+            if (result.isVerifiedUsingV3Scheme()) {
+                List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
+                if (v3Signers.size() != 1) {
+                    result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+                }
+
+                // Compare certificates.
+                checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result);
+
+                // Compare digests.
+                final byte[] digestFromV3 = pickBestDigestForV4(
+                        v3Signers.get(0).getContentDigests());
+                if (!Arrays.equals(digestFromV4, digestFromV3)) {
+                    result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+                }
+            } else if (result.isVerifiedUsingV2Scheme()) {
+                List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
+                if (v2Signers.size() != 1) {
+                    result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+                }
+
+                // Compare certificates.
+                checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result);
+
+                // Compare digests.
+                final byte[] digestFromV2 = pickBestDigestForV4(
+                        v2Signers.get(0).getContentDigests());
+                if (!Arrays.equals(digestFromV4, digestFromV2)) {
+                    result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+                }
+            } else {
+                throw new RuntimeException("V4 signature must be also verified with V2/V3");
+            }
+        }
+
+        // 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);
+        }
+        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 (result.containsErrors()) {
             return result;
         }
@@ -419,12 +575,84 @@
             }
         } else {
             throw new RuntimeException(
-                    "APK verified, but has not verified using any of v1, v2 or v3schemes");
+                    "APK verified, but has not verified using any of v1, v2 or v3 schemes");
         }
 
         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();
+            if (!Arrays.equals(cert, v4Cert)) {
+                result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH);
+            }
+        } catch (CertificateEncodingException e) {
+            throw new RuntimeException("Failed to encode APK signer cert", e);
+        }
+    }
+
+    private static byte[] pickBestDigestForV4(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+        collectApkContentDigests(contentDigests, apkContentDigests);
+        return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
+    }
+
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+            ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+        Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+        for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+            collectApkContentDigests(signerInfo.contentDigests, apkContentDigests);
+        }
+        return apkContentDigests;
+    }
+
+    private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+            List<CentralDirectoryRecord> cdRecords,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections)
+            throws IOException, ApkFormatException {
+        CentralDirectoryRecord manifestCdRecord = null;
+        Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new HashMap<>();
+        for (CentralDirectoryRecord cdRecord : cdRecords) {
+            if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+                manifestCdRecord = cdRecord;
+                break;
+            }
+        }
+        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;
+        }
+        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);
+        }
+    }
+
+    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());
+            if (signatureAlgorithm == null) {
+                continue;
+            }
+            ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+        }
+
+    }
+
     private static ByteBuffer getAndroidManifestFromApk(
             DataSource apk, ApkUtils.ZipSections zipSections)
                     throws IOException, ApkFormatException {
@@ -444,6 +672,15 @@
      * 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
@@ -456,21 +693,61 @@
      */
     private static int getTargetSandboxVersionFromBinaryAndroidManifest(
             ByteBuffer androidManifestContents) throws ApkFormatException {
-        // Return the value of the android:targetSandboxVersion attribute of the top-level manifest
-        // element
+        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)
-                        && (parser.getDepth() == 1)
-                        && ("manifest".equals(parser.getName()))
+                        && (elementName.equals(parser.getName()))
                         && (parser.getNamespace().isEmpty())) {
-                    // In each manifest element, targetSandboxVersion defaults to 1
                     int result = 1;
                     for (int i = 0; i < parser.getAttributeCount(); i++) {
-                        if (parser.getAttributeNameResourceId(i)
-                                == TARGET_SANDBOX_VERSION_ATTR_ID) {
+                        if (parser.getAttributeNameResourceId(i) == attributeId) {
                             int valueType = parser.getAttributeValueType(i);
                             switch (valueType) {
                                 case AndroidBinXmlParser.VALUE_TYPE_INT:
@@ -478,10 +755,11 @@
                                     break;
                                 default:
                                     throw new ApkFormatException(
-                                            "Failed to determine APK's target sandbox version"
+                                            "Failed to determine APK's "
+                                                    + elementName + " attribute"
                                                     + ": unsupported value type of"
-                                                    + " AndroidManifest.xml"
-                                                    + " android:targetSandboxVersion"
+                                                    + " AndroidManifest.xml "
+                                                    + String.format("0x%08X", attributeId)
                                                     + ". Only integer values supported.");
                             }
                             break;
@@ -492,16 +770,24 @@
                 eventType = parser.next();
             }
             throw new ApkFormatException(
-                    "Failed to determine APK's target sandbox version"
-                            + " : no manifest element in AndroidManifest.xml");
+                    "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 target sandbox version"
-                            + ": malformed AndroidManifest.xml",
-                    e);
+                    "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;
+        }
+        return VERSION_JAR_SIGNATURE_SCHEME;
+    }
+
     /**
      * Result of verifying an APKs signatures. The APK can be considered verified iff
      * {@link #isVerified()} returns {@code true}.
@@ -514,11 +800,15 @@
         private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
         private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
         private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+        private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
+        private SourceStampInfo mSourceStampInfo;
 
         private boolean mVerified;
         private boolean mVerifiedUsingV1Scheme;
         private boolean mVerifiedUsingV2Scheme;
         private boolean mVerifiedUsingV3Scheme;
+        private boolean mVerifiedUsingV4Scheme;
+        private boolean mSourceStampVerified;
         private SigningCertificateLineage mSigningCertificateLineage;
 
         /**
@@ -554,6 +844,20 @@
         }
 
         /**
+         * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
+         */
+        public boolean isVerifiedUsingV4Scheme() {
+            return mVerifiedUsingV4Scheme;
+        }
+
+        /**
+         * Returns {@code true} if the APK's SourceStamp signature verified.
+         */
+        public boolean isSourceStampVerified() {
+            return mSourceStampVerified;
+        }
+
+        /**
          * Returns the verified signers' certificates, one per signer.
          */
         public List<X509Certificate> getSignerCertificates() {
@@ -605,6 +909,17 @@
             return mV3SchemeSigners;
         }
 
+        private List<V4SchemeSignerInfo> getV4SchemeSigners() {
+            return mV4SchemeSigners;
+        }
+
+        /**
+         * Returns information about SourceStamp associated with the APK's signature.
+         */
+        public SourceStampInfo getSourceStampInfo() {
+            return mSourceStampInfo;
+        }
+
         /**
          * Returns the combined SigningCertificateLineage associated with this APK's APK Signature
          * Scheme v3 signing block.
@@ -617,6 +932,10 @@
             mErrors.add(new IssueWithParams(msg, parameters));
         }
 
+        void addWarning(Issue msg, Object... parameters) {
+            mWarnings.add(new IssueWithParams(msg, parameters));
+        }
+
         /**
          * Returns errors encountered while verifying the APK's signatures.
          */
@@ -658,6 +977,18 @@
                     }
                     mSigningCertificateLineage = source.signingCertificateLineage;
                     break;
+                case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+                    mVerifiedUsingV4Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
+                    }
+                    break;
+                case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                    mSourceStampVerified = source.verified;
+                    if (!source.signers.isEmpty()) {
+                        mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
+                    }
+                    break;
                 default:
                     throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
             }
@@ -694,6 +1025,9 @@
                     }
                 }
             }
+            if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) {
+                return true;
+            }
 
             return false;
         }
@@ -799,12 +1133,15 @@
 
             private final List<IssueWithParams> mErrors;
             private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
 
             private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
                 mIndex = result.index;
                 mCerts = result.certs;
                 mErrors = result.getErrors();
                 mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
             }
 
             /**
@@ -850,6 +1187,10 @@
             public List<IssueWithParams> getWarnings() {
                 return mWarnings;
             }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
         }
 
         /**
@@ -861,12 +1202,15 @@
 
             private final List<IssueWithParams> mErrors;
             private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
 
             private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
                 mIndex = result.index;
                 mCerts = result.certs;
                 mErrors = result.getErrors();
                 mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
             }
 
             /**
@@ -908,13 +1252,122 @@
             public List<IssueWithParams> getWarnings() {
                 return mWarnings;
             }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
+        }
+
+        /**
+         * Information about an APK Signature Scheme V4 signer associated with the APK's
+         * signature.
+         */
+        public static class V4SchemeSignerInfo {
+            private final int mIndex;
+            private final List<X509Certificate> mCerts;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+            private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+                    mContentDigests;
+
+            private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+                mIndex = result.index;
+                mCerts = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = result.getWarnings();
+                mContentDigests = result.contentDigests;
+            }
+
+            /**
+             * Returns this signer's {@code 0}-based index in the list of signers contained in the
+             * APK's APK Signature Scheme v3 signature.
+             */
+            public int getIndex() {
+                return mIndex;
+            }
+
+            /**
+             * Returns this signer'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 signer's public key.
+             */
+            public X509Certificate getCertificate() {
+                return mCerts.isEmpty() ? null : mCerts.get(0);
+            }
+
+            /**
+             * Returns this signer's certificates. The first certificate is for the signer's public
+             * key. An empty list may be returned if an error was encountered during verification
+             * (see {@link #containsErrors()}).
+             */
+            public List<X509Certificate> getCertificates() {
+                return mCerts;
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
+
+            public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+                return mContentDigests;
+            }
+        }
+
+        /**
+         * Information about SourceStamp associated with the APK's signature.
+         */
+        public static class SourceStampInfo {
+            private final List<X509Certificate> mCertificates;
+
+            private final List<IssueWithParams> mErrors;
+            private final List<IssueWithParams> mWarnings;
+
+            private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+                mCertificates = result.certs;
+                mErrors = result.getErrors();
+                mWarnings = 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);
+            }
+
+            public boolean containsErrors() {
+                return !mErrors.isEmpty();
+            }
+
+            public List<IssueWithParams> getErrors() {
+                return mErrors;
+            }
+
+            public List<IssueWithParams> getWarnings() {
+                return mWarnings;
+            }
         }
     }
 
     /**
      * Error or warning encountered while verifying an APK's signatures.
      */
-    public static enum Issue {
+    public enum Issue {
 
         /**
          * APK is not JAR-signed.
@@ -1254,6 +1707,19 @@
                         + " %1$d"),
 
         /**
+         * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+         * APK is not signed with that version or later.
+         *
+         * <ul>
+         *     <li>Parameter 1: target SDK Version (@code Integer})</li>
+         *     <li>Parameter 2: minimum signature scheme version ((@code Integer})</li>
+         * </ul>
+         */
+        MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+                "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+                        + " not signed with this or a later signature scheme"),
+
+        /**
          * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
          * signature from this signer, but does not contain an APK Signature Scheme v2 signature
          * from this signer.
@@ -1401,7 +1867,7 @@
         /**
          * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
          */
-        V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+        V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"),
 
         /**
          * This APK Signature Scheme v2 signer offers no certificates.
@@ -1724,11 +2190,221 @@
          * <li>Parameter 1: entry ID ({@code Integer})</li>
          * </ul>
          */
-        APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
+        APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"),
+
+        /**
+         * Failed to parse this signer's signature record contained in the APK Signature Scheme
+         * V4 signature.
+         *
+         * <ul>
+         * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+         * </ul>
+         */
+        V4_SIG_MALFORMED_SIGNERS(
+                "V4 signature has malformed signer block"),
+
+        /**
+         * This APK Signature Scheme V4 signer contains a signature produced using an
+         * unknown algorithm.
+         *
+         * <ul>
+         * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+         * </ul>
+         */
+        V4_SIG_UNKNOWN_SIG_ALGORITHM(
+                "V4 signature has unknown signing algorithm: %1$#x"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers no signatures.
+         */
+        V4_SIG_NO_SIGNATURES(
+                "V4 signature has no signature found"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers signatures but none of them are
+         * supported.
+         */
+        V4_SIG_NO_SUPPORTED_SIGNATURES(
+                "V4 signature has no supported signature"),
+
+        /**
+         * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * </ul>
+         */
+        V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /**
+         * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+         * signer.
+         *
+         * <ul>
+         * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+         * <li>Parameter 2: exception ({@code Throwable})</li>
+         * </ul>
+         */
+        V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+         * parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+        /**
+         * This APK Signature Scheme V4 signer's certificate could not be parsed.
+         *
+         * <ul>
+         * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+         *     certificates ({@code Integer})</li>
+         * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+         *     list of certificates ({@code Integer})</li>
+         * <li>Parameter 3: error details ({@code Throwable})</li>
+         * </ul>
+         */
+        V4_SIG_MALFORMED_CERTIFICATE(
+                "V4 signature has malformed certificate"),
+
+        /**
+         * This APK Signature Scheme V4 signer offers no certificate.
+         */
+        V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"),
+
+        /**
+         * This APK Signature Scheme V4 signer's public key listed in the signer's
+         * certificate does not match the public key listed in the signature proto.
+         *
+         * <ul>
+         * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+         * <li>Parameter 2: hex-encoded public key from signature proto ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+                "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
+
+        /**
+         * The APK's hash root (aka digest) does not match the hash root contained in the Signature
+         * Scheme V4 signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_APK_ROOT_DID_NOT_VERIFY(
+                "V4 signature's hash tree root (content digest) did not verity"),
+
+        /**
+         * The APK's hash tree does not match the hash tree contained in the Signature
+         * Scheme V4 signature.
+         *
+         * <ul>
+         * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+         * <li>Parameter 2: hex-encoded expected hash tree of the APK ({@code String})</li>
+         * <li>Parameter 3: hex-encoded actual hash tree of the APK ({@code String})</li>
+         * </ul>
+         */
+        V4_SIG_APK_TREE_DID_NOT_VERIFY(
+                "V4 signature's hash tree did not verity"),
+
+        /**
+         * Using more than one Signer to sign APK Signature Scheme V4 signature.
+         */
+        V4_SIG_MULTIPLE_SIGNERS(
+                "V4 signature only supports one signer"),
+
+        /**
+         * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
+         * used to sign APK Signature Scheme V4 signature.
+         */
+        V4_SIG_V2_V3_SIGNERS_MISMATCH(
+                "V4 signature and V2/V3 signature have mismatched certificates"),
+
+        V4_SIG_V2_V3_DIGESTS_MISMATCH(
+                "V4 signature and V2/V3 signature have mismatched digests"),
+
+        /**
+         * The v4 signature format version isn't the same as the tool's current version, something
+         * may go wrong.
+         */
+        V4_SIG_VERSION_NOT_CURRENT(
+                "V4 signature format version %1$d is different from the tool's current "
+                        + "version %2$d"),
+
+        /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
+        SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
+
+        /**
+         * SourceStamp's certificate could not be parsed.
+         *
+         * <ul>
+         *   <li>Parameter 1: error details ({@code Throwable})
+         * </ul>
+         */
+        SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"),
+
+        /** Failed to parse SourceStamp's signature. */
+        SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"),
+
+        /**
+         * SourceStamp contains a signature produced using an unknown algorithm.
+         *
+         * <ul>
+         *   <li>Parameter 1: algorithm ID ({@code Integer})
+         * </ul>
+         */
+        SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+        /**
+         * An exception was encountered while verifying SourceStamp signature.
+         *
+         * <ul>
+         *   <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+         *   <li>Parameter 2: exception ({@code Throwable})
+         * </ul>
+         */
+        SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+        /**
+         * SourceStamp signature block did not verify.
+         *
+         * <ul>
+         *   <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+         * </ul>
+         */
+        SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+        /** SourceStamp offers no signatures. */
+        SOURCE_STAMP_NO_SIGNATURE("No signature"),
+
+        /** SourceStamp offers an unsupported signature. */
+        SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
+
+        /**
+         * SourceStamp's certificate listed in the APK signing block does not match the certificate
+         * listed in the SourceStamp file in the APK.
+         *
+         * <ul>
+         *   <li>Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing
+         *       block ({@code String})
+         *   <li>Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code
+         *       String})
+         * </ul>
+         */
+        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>");
 
         private final String mFormat;
 
-        private Issue(String format) {
+        Issue(String format) {
             mFormat = format;
         }
 
@@ -1804,10 +2480,7 @@
             if (this == obj) {
                 return true;
             }
-            if (obj == null) {
-                return false;
-            }
-            if (getClass() != obj.getClass()) {
+            if (!(obj instanceof ByteArray)) {
                 return false;
             }
             ByteArray other = (ByteArray) obj;
@@ -1832,6 +2505,7 @@
     public static class Builder {
         private final File mApkFile;
         private final DataSource mApkDataSource;
+        private File mV4SignatureFile;
 
         private Integer mMinSdkVersion;
         private int mMaxSdkVersion = Integer.MAX_VALUE;
@@ -1894,6 +2568,11 @@
             return this;
         }
 
+        public Builder setV4SignatureFile(File v4SignatureFile) {
+            mV4SignatureFile = v4SignatureFile;
+            return this;
+        }
+
         /**
          * Returns an {@link ApkVerifier} initialized according to the configuration of this
          * builder.
@@ -1902,6 +2581,7 @@
             return new ApkVerifier(
                     mApkFile,
                     mApkDataSource,
+                    mV4SignatureFile,
                     mMinSdkVersion,
                     mMaxSdkVersion);
         }
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index c88239e..f0796fb 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -16,15 +16,25 @@
 
 package com.android.apksig;
 
+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.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 com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
 import com.android.apksig.internal.apk.v1.DigestAlgorithm;
 import com.android.apksig.internal.apk.v1.V1SchemeSigner;
 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.v4.V4SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4Signature;
 import com.android.apksig.internal.jar.ManifestParser;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.Pair;
@@ -32,10 +42,12 @@
 import com.android.apksig.util.DataSink;
 import com.android.apksig.util.DataSinks;
 import com.android.apksig.util.DataSource;
-
 import com.android.apksig.util.RunnablesExecutor;
+
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
@@ -43,15 +55,16 @@
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -79,10 +92,12 @@
     private final boolean mV1SigningEnabled;
     private final boolean mV2SigningEnabled;
     private final boolean mV3SigningEnabled;
+    private final boolean mVerityEnabled;
     private final boolean mDebuggableApkPermitted;
     private final boolean mOtherSignersSignaturesPreserved;
     private final String mCreatedBy;
     private final List<SignerConfig> mSignerConfigs;
+    private final SignerConfig mSourceStampSignerConfig;
     private final int mMinSdkVersion;
     private final SigningCertificateLineage mSigningCertificateLineage;
 
@@ -93,9 +108,7 @@
 
     private boolean mV1SignaturePending;
 
-    /**
-     * Names of JAR entries which this engine is expected to output as part of v1 signing.
-     */
+    /** Names of JAR entries which this engine is expected to output as part of v1 signing. */
     private Set<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
 
     /** Requests for digests of output JAR entries. */
@@ -123,8 +136,8 @@
     private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest;
 
     /**
-     * Whether the package being signed is marked as {@code android:debuggable} or {@code null}
-     * if this is not yet known.
+     * Whether the package being signed is marked as {@code android:debuggable} or {@code null} if
+     * this is not yet known.
      */
     private Boolean mDebuggable;
 
@@ -142,19 +155,21 @@
      */
     private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
 
-
-    private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+    private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
 
     private DefaultApkSignerEngine(
             List<SignerConfig> signerConfigs,
+            SignerConfig sourceStampSignerConfig,
             int minSdkVersion,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
+            boolean verityEnabled,
             boolean debuggableApkPermitted,
             boolean otherSignersSignaturesPreserved,
             String createdBy,
-            SigningCertificateLineage signingCertificateLineage) throws InvalidKeyException {
+            SigningCertificateLineage signingCertificateLineage)
+            throws InvalidKeyException {
         if (signerConfigs.isEmpty()) {
             throw new IllegalArgumentException("At least one signer config must be provided");
         }
@@ -166,6 +181,7 @@
         mV1SigningEnabled = v1SigningEnabled;
         mV2SigningEnabled = v2SigningEnabled;
         mV3SigningEnabled = v3SigningEnabled;
+        mVerityEnabled = verityEnabled;
         mV1SignaturePending = v1SigningEnabled;
         mV2SignaturePending = v2SigningEnabled;
         mV3SignaturePending = v3SigningEnabled;
@@ -173,6 +189,7 @@
         mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
         mCreatedBy = createdBy;
         mSignerConfigs = signerConfigs;
+        mSourceStampSignerConfig = sourceStampSignerConfig;
         mMinSdkVersion = minSdkVersion;
         mSigningCertificateLineage = signingCertificateLineage;
 
@@ -191,13 +208,12 @@
                                     oldestConfig.mCertificates.get(0));
                     if (subLineage.size() != 1) {
                         throw new IllegalArgumentException(
-                                "v1 signing enabled but the oldest signer in the "
-                                + "SigningCertificateLineage is missing.  Please provide the oldest"
-                                + " signer to enable v1 signing");
+                                "v1 signing enabled but the oldest signer in the"
+                                    + " SigningCertificateLineage is missing.  Please provide the"
+                                    + " oldest signer to enable v1 signing");
                     }
                 }
-                createV1SignerConfigs(
-                        Collections.singletonList(oldestConfig), minSdkVersion);
+                createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
             } else {
                 createV1SignerConfigs(signerConfigs, minSdkVersion);
             }
@@ -216,19 +232,20 @@
 
             String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName());
             // Check whether the signer's name is unique among all v1 signers
-            Integer indexOfOtherSignerWithSameName =
-                    v1SignerNameToSignerIndex.put(v1SignerName, i);
+            Integer indexOfOtherSignerWithSameName = v1SignerNameToSignerIndex.put(v1SignerName, i);
             if (indexOfOtherSignerWithSameName != null) {
                 throw new IllegalArgumentException(
-                        "Signers #" + (indexOfOtherSignerWithSameName + 1)
-                        + " and #" + (i + 1)
-                        + " have the same name: " + v1SignerName
-                        + ". v1 signer names must be unique");
+                        "Signers #"
+                                + (indexOfOtherSignerWithSameName + 1)
+                                + " and #"
+                                + (i + 1)
+                                + " have the same name: "
+                                + v1SignerName
+                                + ". v1 signer names must be unique");
             }
 
             DigestAlgorithm v1SignatureDigestAlgorithm =
-                    V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
-                            publicKey, minSdkVersion);
+                    V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion);
             V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
             v1SignerConfig.name = v1SignerName;
             v1SignerConfig.privateKey = signerConfig.getPrivateKey();
@@ -243,7 +260,8 @@
                 v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
             } else {
                 if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
-                        v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) > 0) {
+                                v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+                        > 0) {
                     v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
                 }
             }
@@ -260,8 +278,7 @@
 
             // v3 signing only supports single signers, of which the oldest (first) will be the one
             // to use for v1 and v2 signing
-            List<ApkSigningBlockUtils.SignerConfig> signerConfig =
-                    new ArrayList<>();
+            List<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
 
             SignerConfig oldestConfig = mSignerConfigs.get(0);
 
@@ -271,18 +288,21 @@
                 SigningCertificateLineage subLineage =
                         mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0));
                 if (subLineage.size() != 1) {
-                    throw new IllegalArgumentException("v2 signing enabled but the oldest signer in"
+                    throw new IllegalArgumentException(
+                            "v2 signing enabled but the oldest signer in"
                                     + " the SigningCertificateLineage is missing.  Please provide"
                                     + " the oldest signer to enable v2 signing.");
                 }
             }
             signerConfig.add(
                     createSigningBlockSignerConfig(
-                            mSignerConfigs.get(0), apkSigningBlockPaddingSupported,
+                            mSignerConfigs.get(0),
+                            apkSigningBlockPaddingSupported,
                             ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
             return signerConfig;
         } else {
-            return createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+            return createSigningBlockSignerConfigs(
+                    apkSigningBlockPaddingSupported,
                     ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
         }
     }
@@ -290,7 +310,8 @@
     private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
             boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
         List<ApkSigningBlockUtils.SignerConfig> rawConfigs =
-                createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+                createSigningBlockSignerConfigs(
+                        apkSigningBlockPaddingSupported,
                         ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
 
         List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
@@ -304,8 +325,11 @@
                 // no valid algorithm was found for this signer, and we haven't yet covered all
                 // platform versions, something's wrong
                 String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm();
-                throw new InvalidKeyException("Unsupported key algorithm " + keyAlgorithm + " is "
-                        + "not supported for APK Signature Scheme v3 signing");
+                throw new InvalidKeyException(
+                        "Unsupported key algorithm "
+                                + keyAlgorithm
+                                + " is "
+                                + "not supported for APK Signature Scheme v3 signing");
             }
             if (i == rawConfigs.size() - 1) {
                 // first go through the loop, config should support all future platform versions.
@@ -333,12 +357,32 @@
         }
         if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) {
             // we can't cover all desired SDK versions, abort
-            throw new InvalidKeyException("Provided key algorithms not supported on all desired "
-                    + "Android SDK versions");
+            throw new InvalidKeyException(
+                    "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);
+        if (configs.size() != 1) {
+            throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
+        }
+        return configs.get(0);
+    }
+
+    private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
+            throws InvalidKeyException {
+        return createSigningBlockSignerConfig(
+                mSourceStampSignerConfig,
+                /* apkSigningBlockPaddingSupported= */ false,
+                ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+    }
+
     private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) {
         int min = Integer.MAX_VALUE;
         for (SignatureAlgorithm algorithm : algorithms) {
@@ -370,26 +414,29 @@
 
     private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
             SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
-                    throws InvalidKeyException {
+            throws InvalidKeyException {
         List<X509Certificate> certificates = signerConfig.getCertificates();
         PublicKey publicKey = certificates.get(0).getPublicKey();
 
-        ApkSigningBlockUtils.SignerConfig newSignerConfig =
-                new ApkSigningBlockUtils.SignerConfig();
+        ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
         newSignerConfig.privateKey = signerConfig.getPrivateKey();
         newSignerConfig.certificates = certificates;
 
         switch (schemeId) {
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
                 newSignerConfig.signatureAlgorithms =
-                        V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, mMinSdkVersion,
-                                apkSigningBlockPaddingSupported);
+                        V2SchemeSigner.getSuggestedSignatureAlgorithms(
+                                publicKey,
+                                mMinSdkVersion,
+                                apkSigningBlockPaddingSupported && mVerityEnabled);
                 break;
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
                 try {
                     newSignerConfig.signatureAlgorithms =
                             V3SchemeSigner.getSuggestedSignatureAlgorithms(
-                                    publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+                                    publicKey,
+                                    mMinSdkVersion,
+                                    apkSigningBlockPaddingSupported && mVerityEnabled);
                 } catch (InvalidKeyException e) {
 
                     // It is possible for a signer used for v1/v2 signing to not be allowed for use
@@ -399,6 +446,21 @@
                     newSignerConfig.signatureAlgorithms = null;
                 }
                 break;
+            case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+                try {
+                    newSignerConfig.signatureAlgorithms =
+                            V4SchemeSigner.getSuggestedSignatureAlgorithms(
+                                    publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+                } catch (InvalidKeyException e) {
+                    // V4 is an optional signing schema, ok to proceed without.
+                    newSignerConfig.signatureAlgorithms = null;
+                }
+                break;
+            case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                newSignerConfig.signatureAlgorithms =
+                        Collections.singletonList(
+                                SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+                break;
             default:
                 throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
         }
@@ -416,13 +478,13 @@
      * without recalculation. This step has a significant performance benefit in case of incremental
      * build.
      *
-     * This method extracts and stored computed digest for every entry that it would compute it for
-     * in the {@link #outputJarEntry(String)} method
+     * <p>This method extracts and stored computed digest for every entry that it would compute it
+     * for in the {@link #outputJarEntry(String)} method
      *
      * @param manifestBytes raw representation of MANIFEST.MF file
      * @param entryNames a set of expected entries names
      * @return set of entry names which were processed by the engine during the initialization, a
-     *         subset of entryNames
+     *     subset of entryNames
      */
     @Override
     @SuppressWarnings("AndroidJdkLibsChecker")
@@ -431,20 +493,24 @@
         Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
                 V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
         String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
-        for (Map.Entry<String, ManifestParser.Section> entry: sections.getSecond().entrySet()) {
+        for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
             String entryName = entry.getKey();
-            if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
-                    isDebuggable(entryName)) {
+            if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
+                    && isDebuggable(entryName)) {
 
-                Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
+                V1SchemeVerifier.NamedDigest extractedDigest = null;
+                Collection<V1SchemeVerifier.NamedDigest> digestsToVerify =
                         V1SchemeVerifier.getDigestsToVerify(
-                                entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE)
-                                .stream()
-                                .filter(d -> d.jcaDigestAlgorithm == alg)
-                                .findFirst();
-
-                extractedDigest.ifPresent(
-                        namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
+                                entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE);
+                for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) {
+                    if (digestToVerify.jcaDigestAlgorithm.equals(alg)) {
+                        extractedDigest = digestToVerify;
+                        break;
+                    }
+                }
+                if (extractedDigest != null) {
+                    mOutputJarEntryDigests.put(entryName, extractedDigest.digest);
+                }
             }
         }
         return mOutputJarEntryDigests.keySet();
@@ -559,7 +625,8 @@
                 // check whether the output entry's data matches what the engine emitted.
                 dataRequest =
                         (mEmittedSignatureJarEntryData.containsKey(entryName))
-                                ? new GetJarEntryDataRequest(entryName) : null;
+                                ? new GetJarEntryDataRequest(entryName)
+                                : null;
             }
 
             if (dataRequest != null) {
@@ -619,8 +686,7 @@
                             + mInputJarManifestEntryDataRequest.getEntryName());
         }
 
-        for (GetJarEntryDataDigestRequest digestRequest
-                : mOutputJarEntryDigestRequests.values()) {
+        for (GetJarEntryDataDigestRequest digestRequest : mOutputJarEntryDigestRequests.values()) {
             String entryName = digestRequest.getEntryName();
             if (!digestRequest.isDone()) {
                 throw new IllegalStateException(
@@ -628,6 +694,14 @@
             }
             mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
         }
+        if (isEligibleForSourceStamp()) {
+            MessageDigest messageDigest =
+                    MessageDigest.getInstance(
+                            V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+            messageDigest.update(generateSourceStampCertificateDigest());
+            mOutputJarEntryDigests.put(
+                    SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+        }
         mOutputJarEntryDigestRequests.clear();
 
         for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
@@ -646,7 +720,16 @@
         }
         byte[] inputJarManifest =
                 (mInputJarManifestEntryDataRequest != null)
-                    ? mInputJarManifestEntryDataRequest.getData() : null;
+                        ? mInputJarManifestEntryDataRequest.getData()
+                        : null;
+        if (isEligibleForSourceStamp()) {
+            inputJarManifest =
+                    V1SchemeSigner.generateManifestFile(
+                                    mV1ContentDigestAlgorithm,
+                                    mOutputJarEntryDigests,
+                                    inputJarManifest)
+                            .contents;
+        }
 
         // Check whether the most recently used signature (if present) is still fine.
         checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
@@ -667,9 +750,7 @@
         } else {
             V1SchemeSigner.OutputManifestFile newManifest =
                     V1SchemeSigner.generateManifestFile(
-                            mV1ContentDigestAlgorithm,
-                            mOutputJarEntryDigests,
-                            inputJarManifest);
+                            mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
             byte[] emittedSignatureManifest =
                     mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
             if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
@@ -689,8 +770,8 @@
                 // Emitted v1 signature is still valid. Check whether the signature is there in the
                 // output.
                 signatureZipEntries = new ArrayList<>();
-                for (Map.Entry<String, byte[]> expectedOutputEntry
-                        : mEmittedSignatureJarEntryData.entrySet()) {
+                for (Map.Entry<String, byte[]> expectedOutputEntry :
+                        mEmittedSignatureJarEntryData.entrySet()) {
                     String entryName = expectedOutputEntry.getKey();
                     byte[] expectedData = expectedOutputEntry.getValue();
                     GetJarEntryDataRequest actualDataRequest =
@@ -734,21 +815,15 @@
     @Deprecated
     @Override
     public OutputApkSigningBlockRequest outputZipSections(
-            DataSource zipEntries,
-            DataSource zipCentralDirectory,
-            DataSource zipEocd)
-                    throws IOException, InvalidKeyException, SignatureException,
-                            NoSuchAlgorithmException {
+            DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
         return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false);
     }
 
     @Override
     public OutputApkSigningBlockRequest2 outputZipSections2(
-            DataSource zipEntries,
-            DataSource zipCentralDirectory,
-            DataSource zipEocd)
-                    throws IOException, InvalidKeyException, SignatureException,
-                            NoSuchAlgorithmException {
+            DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
         return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true);
     }
 
@@ -757,59 +832,103 @@
             DataSource zipCentralDirectory,
             DataSource zipEocd,
             boolean apkSigningBlockPaddingSupported)
-                    throws IOException, InvalidKeyException, SignatureException,
-                            NoSuchAlgorithmException {
+            throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
         checkNotClosed();
         checkV1SigningDoneIfEnabled();
-        if (!mV2SigningEnabled && !mV3SigningEnabled) {
+        if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) {
             return null;
         }
         checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
 
         // adjust to proper padding
         Pair<DataSource, Integer> paddingPair =
-                ApkSigningBlockUtils.generateApkSigningBlockPadding(zipEntries,
-                        apkSigningBlockPaddingSupported);
+                ApkSigningBlockUtils.generateApkSigningBlockPadding(
+                        zipEntries, apkSigningBlockPaddingSupported);
         DataSource beforeCentralDir = paddingPair.getFirst();
-        int padSizeBeforeApkSigningBlock  = paddingPair.getSecond();
-        DataSource eocd =
-                ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+        int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+        DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
 
         List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
+        ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
+        ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
 
         // create APK Signature Scheme V2 Signature if requested
         if (mV2SigningEnabled) {
             invalidateV2Signature();
             List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
                     createV2SignerConfigs(apkSigningBlockPaddingSupported);
-            signingSchemeBlocks.add(
+            v2SigningSchemeBlockAndDigests =
                     V2SchemeSigner.generateApkSignatureSchemeV2Block(
                             mExecutor,
                             beforeCentralDir,
                             zipCentralDirectory,
                             eocd,
                             v2SignerConfigs,
-                            mV3SigningEnabled));
+                            mV3SigningEnabled);
+            signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
         }
         if (mV3SigningEnabled) {
             invalidateV3Signature();
             List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
                     createV3SignerConfigs(apkSigningBlockPaddingSupported);
-            signingSchemeBlocks.add(
+            v3SigningSchemeBlockAndDigests =
                     V3SchemeSigner.generateApkSignatureSchemeV3Block(
                             mExecutor,
                             beforeCentralDir,
                             zipCentralDirectory,
                             eocd,
-                            v3SignerConfigs));
+                            v3SignerConfigs);
+            signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
+        }
+        if (isEligibleForSourceStamp()) {
+            ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
+                    createSourceStampSignerConfig();
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos =
+                    new HashMap<>();
+            if (mV3SigningEnabled) {
+                signatureSchemeDigestInfos.put(
+                        VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+            }
+            if (mV2SigningEnabled) {
+                signatureSchemeDigestInfos.put(
+                        VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+            }
+            if (mV1SigningEnabled) {
+                Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>();
+                try {
+                    // Jar signing related variables must have been already populated at this point
+                    // if V1 signing is enabled since it is happening before computations on the APK
+                    // signing block (V2/V3/V4/SourceStamp signing).
+                    byte[] inputJarManifest =
+                            (mInputJarManifestEntryDataRequest != null)
+                                    ? mInputJarManifestEntryDataRequest.getData()
+                                    : null;
+                    byte[] jarManifest =
+                            V1SchemeSigner.generateManifestFile(
+                                            mV1ContentDigestAlgorithm,
+                                            mOutputJarEntryDigests,
+                                            inputJarManifest)
+                                    .contents;
+                    // The digest of the jar manifest does not need to be computed in chunks due to
+                    // the small size of the manifest.
+                    v1SigningSchemeDigests.put(
+                            ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+                } catch (ApkFormatException e) {
+                    throw new RuntimeException("Failed to generate manifest file", e);
+                }
+                signatureSchemeDigestInfos.put(
+                        VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+            }
+            signingSchemeBlocks.add(
+                    V2SourceStampSigner.generateSourceStampBlock(
+                            sourceStampSignerConfig, signatureSchemeDigestInfos));
         }
 
-        // create APK Signing Block with v2 and/or v3 blocks
-        byte[] apkSigningBlock =
-                ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+        // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
+        byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
 
-        mAddSigningBlockRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock,
-                padSizeBeforeApkSigningBlock);
+        mAddSigningBlockRequest =
+                new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock);
         return mAddSigningBlockRequest;
     }
 
@@ -821,6 +940,62 @@
     }
 
     @Override
+    public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+            throws SignatureException {
+        if (outputFile == null) {
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("Missing V4 output file.");
+        }
+        try {
+            ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+            V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
+        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+            if (ignoreFailures) {
+                return;
+            }
+            throw new SignatureException("V4 signing failed", e);
+        }
+    }
+
+    /** For external use only to generate V4 & tree separately. */
+    public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+            throws SignatureException {
+        if (sigOutput == null) {
+            throw new SignatureException("Missing V4 output streams.");
+        }
+        try {
+            ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+            Pair<V4Signature, byte[]> pair =
+                    V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+            pair.getFirst().writeTo(sigOutput);
+            return pair.getSecond();
+        } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+            throw new SignatureException("V4 signing failed", e);
+        }
+    }
+
+    @Override
+    public boolean isEligibleForSourceStamp() {
+        return mSourceStampSignerConfig != null
+                && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
+    }
+
+    @Override
+    public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+        if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+            throw new SignatureException("No certificates configured for stamp");
+        }
+        try {
+            return computeSha256DigestBytes(
+                    mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException("Failed to encode source stamp certificate", e);
+        }
+    }
+
+    @Override
     public void close() {
         mClosed = true;
 
@@ -877,15 +1052,17 @@
                     "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
                             + " been fulfilled");
         }
-        for (Map.Entry<String, byte[]> expectedOutputEntry
-                : mEmittedSignatureJarEntryData.entrySet()) {
+        for (Map.Entry<String, byte[]> expectedOutputEntry :
+                mEmittedSignatureJarEntryData.entrySet()) {
             String entryName = expectedOutputEntry.getKey();
             byte[] expectedData = expectedOutputEntry.getValue();
             GetJarEntryDataRequest actualDataRequest =
                     mOutputSignatureJarEntryDataRequests.get(entryName);
             if (actualDataRequest == null) {
                 throw new IllegalStateException(
-                        "APK entry " + entryName + " not yet output despite this having been"
+                        "APK entry "
+                                + entryName
+                                + " not yet output despite this having been"
                                 + " requested");
             } else if (!actualDataRequest.isDone()) {
                 throw new IllegalStateException(
@@ -918,8 +1095,7 @@
         mV3SignaturePending = false;
     }
 
-    private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected()
-            throws SignatureException {
+    private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() throws SignatureException {
         if (mDebuggableApkPermitted) {
             return;
         }
@@ -936,8 +1112,8 @@
     }
 
     /**
-     * Returns whether the output APK is debuggable according to its
-     * {@code android:debuggable} declaration.
+     * Returns whether the output APK is debuggable according to its {@code android:debuggable}
+     * declaration.
      */
     private boolean isOutputApkDebuggable() throws ApkFormatException {
         if (mDebuggable != null) {
@@ -966,9 +1142,7 @@
         mDebuggable = null;
     }
 
-    /**
-     * Returns the output policy for the provided input JAR entry.
-     */
+    /** Returns the output policy for the provided input JAR entry. */
     private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
         if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
             return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
@@ -1036,9 +1210,7 @@
         }
     }
 
-    /**
-     * JAR entry inspection request which obtain the entry's uncompressed data.
-     */
+    /** JAR entry inspection request which obtain the entry's uncompressed data. */
     private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
         private final String mEntryName;
         private final Object mLock = new Object();
@@ -1104,9 +1276,7 @@
         }
     }
 
-    /**
-     * JAR entry inspection request which obtains the digest of the entry's uncompressed data.
-     */
+    /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */
     private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
         private final String mEntryName;
         private final String mJcaDigestAlgorithm;
@@ -1189,9 +1359,7 @@
         }
     }
 
-    /**
-     * JAR entry inspection request which transparently satisfies multiple such requests.
-     */
+    /** JAR entry inspection request which transparently satisfies multiple such requests. */
     private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest {
         private final String mEntryName;
         private final InspectJarEntryRequest[] mRequests;
@@ -1243,24 +1411,18 @@
         private final List<X509Certificate> mCertificates;
 
         private SignerConfig(
-                String name,
-                PrivateKey privateKey,
-                List<X509Certificate> certificates) {
+                String name, PrivateKey privateKey, List<X509Certificate> certificates) {
             mName = name;
             mPrivateKey = privateKey;
             mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
         }
 
-        /**
-         * Returns the name of this signer.
-         */
+        /** Returns the name of this signer. */
         public String getName() {
             return mName;
         }
 
-        /**
-         * Returns the signing key of this signer.
-         */
+        /** Returns the signing key of this signer. */
         public PrivateKey getPrivateKey() {
             return mPrivateKey;
         }
@@ -1273,9 +1435,7 @@
             return mCertificates;
         }
 
-        /**
-         * Builder of {@link SignerConfig} instances.
-         */
+        /** Builder of {@link SignerConfig} instances. */
         public static class Builder {
             private final String mName;
             private final PrivateKey mPrivateKey;
@@ -1285,15 +1445,12 @@
              * Constructs a new {@code Builder}.
              *
              * @param name signer's name. The name is reflected in the name of files comprising the
-             *        JAR signature of the APK.
+             *     JAR signature of the APK.
              * @param privateKey signing key
              * @param certificates list of one or more X.509 certificates. The subject public key of
-             *        the first certificate must correspond to the {@code privateKey}.
+             *     the first certificate must correspond to the {@code privateKey}.
              */
-            public Builder(
-                    String name,
-                    PrivateKey privateKey,
-                    List<X509Certificate> certificates) {
+            public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
                 if (name.isEmpty()) {
                     throw new IllegalArgumentException("Empty name");
                 }
@@ -1307,24 +1464,21 @@
              * this builder.
              */
             public SignerConfig build() {
-                return new SignerConfig(
-                        mName,
-                        mPrivateKey,
-                        mCertificates);
+                return new SignerConfig(mName, mPrivateKey, mCertificates);
             }
         }
     }
 
-    /**
-     * Builder of {@link DefaultApkSignerEngine} instances.
-     */
+    /** Builder of {@link DefaultApkSignerEngine} instances. */
     public static class Builder {
         private List<SignerConfig> mSignerConfigs;
+        private SignerConfig mStampSignerConfig;
         private final int mMinSdkVersion;
 
         private boolean mV1SigningEnabled = true;
         private boolean mV2SigningEnabled = true;
         private boolean mV3SigningEnabled = true;
+        private boolean mVerityEnabled = false;
         private boolean mDebuggableApkPermitted = true;
         private boolean mOtherSignersSignaturesPreserved;
         private String mCreatedBy = "1.0 (Android)";
@@ -1344,15 +1498,13 @@
          * Constructs a new {@code Builder}.
          *
          * @param signerConfigs information about signers with which the APK will be signed. At
-         *        least one signer configuration must be provided.
+         *     least one signer configuration must be provided.
          * @param minSdkVersion API Level of the oldest Android platform on which the APK is
-         *        supposed to be installed. See {@code minSdkVersion} attribute in the APK's
-         *        {@code AndroidManifest.xml}. The higher the version, the stronger signing features
-         *        will be enabled.
+         *     supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code
+         *     AndroidManifest.xml}. The higher the version, the stronger signing features will be
+         *     enabled.
          */
-        public Builder(
-                List<SignerConfig> signerConfigs,
-                int minSdkVersion) {
+        public Builder(List<SignerConfig> signerConfigs, int minSdkVersion) {
             if (signerConfigs.isEmpty()) {
                 throw new IllegalArgumentException("At least one signer config must be provided");
             }
@@ -1373,8 +1525,9 @@
         public DefaultApkSignerEngine build() throws InvalidKeyException {
 
             if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
-                throw new IllegalStateException("Builder configured to both enable and disable APK "
-                        + "Signature Scheme v3 signing");
+                throw new IllegalStateException(
+                        "Builder configured to both enable and disable APK "
+                                + "Signature Scheme v3 signing");
             }
             if (mV3SigningExplicitlyDisabled) {
                 mV3SigningEnabled = false;
@@ -1391,31 +1544,43 @@
                         // this is a strange situation: we've provided a valid rotation history, but
                         // are only signing with v1/v2.  blow up, since we don't know for sure with
                         // which signer the user intended to sign
-                        throw new IllegalStateException("Provided multiple signers which are part "
-                                + "of the SigningCertificateLineage, but not signing with APK "
-                                + "Signature Scheme v3");
+                        throw new IllegalStateException(
+                                "Provided multiple signers which are part of the"
+                                        + " SigningCertificateLineage, but not signing with APK"
+                                        + " Signature Scheme v3");
                     }
                 } catch (IllegalArgumentException e) {
-                    throw new IllegalStateException("Provided signer configs do not match the "
-                            + "provided SigningCertificateLineage", e);
+                    throw new IllegalStateException(
+                            "Provided signer configs do not match the "
+                                    + "provided SigningCertificateLineage",
+                            e);
                 }
             } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
-                throw new IllegalStateException("Multiple signing certificates provided for use "
-                + "with APK Signature Scheme v3 without an accompanying SigningCertificateLineage");
+                throw new IllegalStateException(
+                        "Multiple signing certificates provided for use with APK Signature Scheme"
+                                + " v3 without an accompanying SigningCertificateLineage");
             }
 
             return new DefaultApkSignerEngine(
                     mSignerConfigs,
+                    mStampSignerConfig,
                     mMinSdkVersion,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
                     mV3SigningEnabled,
+                    mVerityEnabled,
                     mDebuggableApkPermitted,
                     mOtherSignersSignaturesPreserved,
                     mCreatedBy,
                     mSigningCertificateLineage);
         }
 
+        /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */
+        public Builder setStampSignerConfig(SignerConfig stampSignerConfig) {
+            mStampSignerConfig = stampSignerConfig;
+            return this;
+        }
+
         /**
          * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
          *
@@ -1454,8 +1619,20 @@
         }
 
         /**
-         * Sets whether the APK should be signed even if it is marked as debuggable
-         * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+         * Sets whether the APK should be signed using the verity signature algorithm in the v2 and
+         * v3 signature blocks.
+         *
+         * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and
+         * v3 signature schemes.
+         */
+        public Builder setVerityEnabled(boolean enabled) {
+            mVerityEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the APK should be signed even if it is marked as debuggable ({@code
+         * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
          * compatibility reasons, the default value of this setting is {@code true}.
          *
          * <p>It is dangerous to sign debuggable APKs with production/release keys because Android
@@ -1478,9 +1655,7 @@
             return this;
         }
 
-        /**
-         * Sets the value of the {@code Created-By} field in JAR signature files.
-         */
+        /** Sets the value of the {@code Created-By} field in JAR signature files. */
         public Builder setCreatedBy(String createdBy) {
             if (createdBy == null) {
                 throw new NullPointerException();
@@ -1490,7 +1665,7 @@
         }
 
         /**
-         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme.  This
+         * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
          * structure provides proof of signing certificate rotation linking {@link SignerConfig}
          * objects to previous ones.
          */
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 135d815..c6cbf5f 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -27,6 +27,8 @@
 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;
@@ -41,6 +43,9 @@
      */
     public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
 
+    /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+    public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+
     private ApkUtils() {}
 
     /**
@@ -601,4 +606,15 @@
                     e);
         }
     }
+
+    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/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 7c13586..f027525 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,10 @@
 
 package com.android.apksig.internal.apk;
 
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
+
 import com.android.apksig.ApkVerifier;
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.apk.ApkFormatException;
@@ -25,6 +29,15 @@
 import com.android.apksig.internal.asn1.Asn1DecodingException;
 import com.android.apksig.internal.asn1.Asn1DerEncoder;
 import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.SignedData;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.pkcs7.SignerInfo;
 import com.android.apksig.internal.util.ByteBufferDataSource;
 import com.android.apksig.internal.util.ChainedDataSource;
 import com.android.apksig.internal.util.Pair;
@@ -36,8 +49,8 @@
 import com.android.apksig.util.DataSinks;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
-
 import com.android.apksig.util.RunnablesExecutor;
+
 import java.io.IOException;
 import java.math.BigInteger;
 import java.nio.BufferUnderflowException;
@@ -60,6 +73,7 @@
 import java.security.spec.X509EncodedKeySpec;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -67,24 +81,29 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
+
+import javax.security.auth.x500.X500Principal;
 
 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;
-    public static final byte[] APK_SIGNING_BLOCK_MAGIC =
+    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
           new byte[] {
               0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
               0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
           };
     private static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
 
+    private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
+            {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
+
+    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;
 
     /**
      * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
@@ -191,7 +210,7 @@
                             centralDir,
                             new ByteBufferDataSource(modifiedEocd));
             // Special checks for the verity algorithm requirements.
-            if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+            if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) {
                 if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
                     throw new RuntimeException(
                             "APK Signing Block is not aligned on 4k boundary: " +
@@ -418,17 +437,20 @@
             DataSource centralDir,
             DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
         Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>();
-        Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = digestAlgorithms.stream()
-                .filter(a -> a == ContentDigestAlgorithm.CHUNKED_SHA256 ||
-                             a == ContentDigestAlgorithm.CHUNKED_SHA512)
-                .collect(Collectors.toSet());
+        Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>();
+        for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) {
+            if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+                    || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+                oneMbChunkBasedAlgorithm.add(digestAlgorithm);
+            }
+        }
         computeOneMbChunkContentDigests(
                 executor,
                 oneMbChunkBasedAlgorithm,
                 new DataSource[] { beforeCentralDir, centralDir, eocd },
                 contentDigests);
 
-        if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+        if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
             computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
         }
         return contentDigests;
@@ -627,17 +649,17 @@
                 for (ChunkSupplier.Chunk chunk = dataSupplier.get();
                      chunk != null;
                      chunk = dataSupplier.get()) {
-                    long size = chunk.dataSource.size();
+                    int size = chunk.size;
                     if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
                         throw new RuntimeException("Chunk size greater than expected: " + size);
                     }
 
                     // First update with the chunk prefix.
-                    setUnsignedInt32LittleEndian((int)size, chunkContentPrefix, 1);
+                    setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
                     mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
 
                     // Then update with the chunk data.
-                    chunk.dataSource.feed(0, size, mdSink);
+                    mdSink.consume(chunk.data);
 
                     // Now finalize chunk for all algorithms.
                     for (int i = 0; i < chunkDigests.size(); i++) {
@@ -685,7 +707,7 @@
                                     i));
                 }
                 chunkCounts[i] = (int)chunkCount;
-                totalChunkCount += chunkCount;
+                totalChunkCount = (int) (totalChunkCount + chunkCount);
             }
             this.totalChunkCount = totalChunkCount;
             nextIndex = new AtomicInteger(0);
@@ -705,7 +727,7 @@
             }
 
             int dataSourceIndex = 0;
-            int dataSourceChunkOffset = index;
+            long dataSourceChunkOffset = index;
             for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
                 if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
                     break;
@@ -717,49 +739,90 @@
                     dataSources[dataSourceIndex].size() -
                             dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
                     CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
-            // Note that slicing may involve its own locking. We may wish to reimplement the
-            // underlying mechanism to get rid of that lock (e.g. ByteBufferDataSource should
-            // probably get reimplemented to a delegate model, such that grabbing a slice
-            // doesn't incur a lock).
-            return new Chunk(
-                    dataSources[dataSourceIndex].slice(
-                            dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
-                            remainingSize),
-                    index);
+
+            final int size = (int)remainingSize;
+            final ByteBuffer buffer = ByteBuffer.allocate(size);
+            try {
+                dataSources[dataSourceIndex].copyTo(
+                        dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
+                        buffer);
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to read chunk", e);
+            }
+            buffer.rewind();
+
+            return new Chunk(index, buffer, size);
         }
 
         static class Chunk {
             private final int chunkIndex;
-            private final DataSource dataSource;
+            private final ByteBuffer data;
+            private final int size;
 
-            private Chunk(DataSource parentSource, int chunkIndex) {
+            private Chunk(int chunkIndex, ByteBuffer data, int size) {
                 this.chunkIndex = chunkIndex;
-                dataSource = parentSource;
+                this.data = data;
+                this.size = size;
             }
         }
     }
 
+    @SuppressWarnings("ByteBufferBackingArray")
     private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
             DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
             throws IOException, NoSuchAlgorithmException {
+        ByteBuffer encoded = createVerityDigestBuffer(true);
+        // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
+        // kernel to use.
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) {
+            byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir,
+                    eocd);
+            encoded.put(rootHash);
+            encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
+            outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array());
+        }
+    }
+
+    private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
         // FORMAT:
         // OFFSET       DATA TYPE  DESCRIPTION
         // * @+0  bytes uint8[32]  Merkle tree root hash of SHA-256
-        // * @+32 bytes int64      Length of source data
+        // * @+32 bytes int64      (optional) Length of source data
         int backBufferSize =
-                ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes() +
-                Long.SIZE / Byte.SIZE;
+                VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+        if (includeSourceDataSize) {
+            backBufferSize += Long.SIZE / Byte.SIZE;
+        }
         ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
         encoded.order(ByteOrder.LITTLE_ENDIAN);
+        return encoded;
+    }
 
+    public static class VerityTreeAndDigest {
+        public final ContentDigestAlgorithm contentDigestAlgorithm;
+        public final byte[] rootHash;
+        public final byte[] tree;
+
+        VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash,
+                byte[] tree) {
+            this.contentDigestAlgorithm = contentDigestAlgorithm;
+            this.rootHash = rootHash;
+            this.tree = tree;
+        }
+    }
+
+    @SuppressWarnings("ByteBufferBackingArray")
+    public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
+            throws IOException, NoSuchAlgorithmException {
+        ByteBuffer encoded = createVerityDigestBuffer(false);
         // Use 0s as salt for now.  This also needs to be consistent in the fsverify header for
         // kernel to use.
-        VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8]);
-        byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd);
-        encoded.put(rootHash);
-        encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
-
-        outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
+            ByteBuffer tree = builder.generateVerityTree(dataSource);
+            byte[] rootHash = builder.getRootHashFromTree(tree);
+            encoded.put(rootHash);
+            return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
+        }
     }
 
     private static long getChunkCount(long inputSize, long chunkSize) {
@@ -1145,15 +1208,17 @@
         if (minSdkVersion < minProvidedSignaturesVersion) {
             throw new NoSupportedSignaturesException(
                     "Minimum provided signature version " + minProvidedSignaturesVersion +
-                    " < minSdkVersion " + minSdkVersion);
+                    " > minSdkVersion " + minSdkVersion);
         }
         if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
             throw new NoSupportedSignaturesException("No supported signature");
         }
-        return bestSigAlgorithmOnSdkVersion.values().stream()
-                .sorted((sig1, sig2) -> Integer.compare(
-                        sig1.algorithm.getId(), sig2.algorithm.getId()))
-                .collect(Collectors.toList());
+        List<SupportedSignature> signaturesToVerify =
+                new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+        Collections.sort(
+                signaturesToVerify,
+                (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+        return signaturesToVerify;
     }
 
     public static class NoSupportedSignaturesException extends Exception {
@@ -1235,6 +1300,70 @@
     }
 
     /**
+     * Wrap the signature according to CMS PKCS #7 RFC 5652.
+     * The high-level simplified structure is as follows:
+     * // ContentInfo
+     *     //   digestAlgorithm
+     *     //   SignedData
+     *     //     bag of certificates
+     *     //     SignerInfo
+     *     //       signing cert issuer and serial number (for locating the cert in the above bag)
+     *     //       digestAlgorithm
+     *     //       signatureAlgorithm
+     *     //       signature
+     *
+     * @throws Asn1EncodingException if the ASN.1 structure could not be encoded
+     */
+    public static byte[] generatePkcs7DerEncodedMessage(
+            byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts,
+            AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)
+            throws Asn1EncodingException, CertificateEncodingException {
+        SignerInfo signerInfo = new SignerInfo();
+        signerInfo.version = 1;
+        X509Certificate signingCert = signerCerts.get(0);
+        X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
+        signerInfo.sid =
+                new SignerIdentifier(
+                        new IssuerAndSerialNumber(
+                                new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
+                                signingCert.getSerialNumber()));
+
+        signerInfo.digestAlgorithm = digestAlgorithmId;
+        signerInfo.signatureAlgorithm = signatureAlgorithmId;
+        signerInfo.signature = ByteBuffer.wrap(signatureBytes);
+
+        SignedData signedData = new SignedData();
+        signedData.certificates = new ArrayList<>(signerCerts.size());
+        for (X509Certificate cert : signerCerts) {
+            signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
+        }
+        signedData.version = 1;
+        signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
+        signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
+        // If data is not null, data will be embedded as is in the result -- an attached pcsk7
+        signedData.encapContentInfo.content = data;
+        signedData.signerInfos = Collections.singletonList(signerInfo);
+        ContentInfo contentInfo = new ContentInfo();
+        contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
+        contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
+        return Asn1DerEncoder.encode(contentInfo);
+    }
+
+    /**
+     * Picks the correct v2/v3 digest for v4 signature verification.
+     *
+     * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils.
+     */
+    public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) {
+        for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) {
+            if (contentDigests.containsKey(algo)) {
+                return contentDigests.get(algo);
+            }
+        }
+        return null;
+    }
+
+    /**
      * Signer configuration.
      */
     public static class SignerConfig {
@@ -1263,7 +1392,7 @@
         /** Whether the APK's APK Signature Scheme signature verifies. */
         public boolean verified;
 
-        public final List<SignerInfo> signers = new ArrayList<>();
+        public final List<Result.SignerInfo> signers = new ArrayList<>();
         public SigningCertificateLineage signingCertificateLineage = null;
         private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
         private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
@@ -1277,7 +1406,7 @@
                 return true;
             }
             if (!signers.isEmpty()) {
-                for (SignerInfo signer : signers) {
+                for (Result.SignerInfo signer : signers) {
                     if (signer.containsErrors()) {
                         return true;
                     }
@@ -1286,6 +1415,20 @@
             return false;
         }
 
+        public boolean containsWarnings() {
+            if (!mWarnings.isEmpty()) {
+                return true;
+            }
+            if (!signers.isEmpty()) {
+                for (Result.SignerInfo signer : signers) {
+                    if (signer.containsWarnings()) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
         public void addError(ApkVerifier.Issue msg, Object... parameters) {
             mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
         }
@@ -1330,6 +1473,10 @@
                 return !mErrors.isEmpty();
             }
 
+            public boolean containsWarnings() {
+                return !mWarnings.isEmpty();
+            }
+
             public List<ApkVerifier.IssueWithParams> getErrors() {
                 return mErrors;
             }
@@ -1403,4 +1550,16 @@
             this.signature = signature;
         }
     }
+
+    public static class SigningSchemeBlockAndDigests {
+        public final Pair<byte[], Integer> signingSchemeBlock;
+        public final Map<ContentDigestAlgorithm, byte[]> digestInfo;
+
+        public SigningSchemeBlockAndDigests(
+                Pair<byte[], Integer> signingSchemeBlock,
+                Map<ContentDigestAlgorithm, byte[]> digestInfo) {
+            this.signingSchemeBlock = signingSchemeBlock;
+            this.digestInfo = digestInfo;
+        }
+    }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
index b222474..b806d1e 100644
--- a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
@@ -16,28 +16,36 @@
 
 package com.android.apksig.internal.apk;
 
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
+/** APK Signature Scheme v2 content digest algorithm. */
 public enum ContentDigestAlgorithm {
     /** SHA2-256 over 1 MB chunks. */
-    CHUNKED_SHA256("SHA-256", 256 / 8),
+    CHUNKED_SHA256(1, "SHA-256", 256 / 8),
 
     /** SHA2-512 over 1 MB chunks. */
-    CHUNKED_SHA512("SHA-512", 512 / 8),
+    CHUNKED_SHA512(2, "SHA-512", 512 / 8),
 
     /** SHA2-256 over 4 KB chunks for APK verity. */
-    VERITY_CHUNKED_SHA256("SHA-256", 256 / 8);
+    VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
 
+    /** Non-chunk SHA2-256. */
+    SHA256(4, "SHA-256", 256 / 8);
+
+    private final int mId;
     private final String mJcaMessageDigestAlgorithm;
     private final int mChunkDigestOutputSizeBytes;
 
     private ContentDigestAlgorithm(
-            String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+            int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+        mId = id;
         mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
         mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
     }
 
+    /** Returns the ID of the digest algorithm used on the APK. */
+    public int getId() {
+        return mId;
+    }
+
     /**
      * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
      * chunks by this content digest algorithm.
@@ -46,10 +54,8 @@
         return mJcaMessageDigestAlgorithm;
     }
 
-    /**
-     * Returns the size (in bytes) of the digest of a chunk of content.
-     */
+    /** Returns the size (in bytes) of the digest of a chunk of content. */
     int getChunkDigestOutputSizeBytes() {
         return mChunkDigestOutputSizeBytes;
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
new file mode 100644
index 0000000..2f4c3ba
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -0,0 +1,265 @@
+/*
+ * 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 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 java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ */
+class SourceStampVerifier {
+    /** Hidden constructor to prevent instantiation. */
+    private SourceStampVerifier() {}
+
+    /**
+     * Parses the SourceStamp block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over digest provided.
+     *
+     * <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 verifyV1SourceStamp(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result,
+            byte[] apkDigest,
+            byte[] sourceStampCertificateDigest,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        X509Certificate sourceStampCertificate =
+                verifySourceStampCertificate(
+                        sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+        if (result.containsWarnings() || result.containsErrors()) {
+            return;
+        }
+
+        verifySourceStampSignature(
+                apkDigest,
+                minSdkVersion,
+                maxSdkVersion,
+                sourceStampCertificate,
+                sourceStampBlockData,
+                result);
+    }
+
+    /**
+     * Parses the SourceStamp block and populates the {@code result}.
+     *
+     * <p>This verifies signatures over digest of multiple signature schemes provided.
+     *
+     * <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 verifyV2SourceStamp(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result,
+            Map<Integer, byte[]> signatureSchemeApkDigests,
+            byte[] sourceStampCertificateDigest,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws ApkFormatException, NoSuchAlgorithmException {
+        X509Certificate sourceStampCertificate =
+                verifySourceStampCertificate(
+                        sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+        if (result.containsWarnings() || result.containsErrors()) {
+            return;
+        }
+
+        // Parse signed signature schemes block.
+        ByteBuffer signedSignatureSchemes =
+                ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+        Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
+        while (signedSignatureSchemes.hasRemaining()) {
+            ByteBuffer signedSignatureScheme =
+                    ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
+            int signatureSchemeId = signedSignatureScheme.getInt();
+            signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
+        }
+
+        for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
+                signatureSchemeApkDigests.entrySet()) {
+            if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
+                result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+                return;
+            }
+            verifySourceStampSignature(
+                    signatureSchemeApkDigest.getValue(),
+                    minSdkVersion,
+                    maxSdkVersion,
+                    sourceStampCertificate,
+                    signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
+                    result);
+            if (result.containsWarnings() || result.containsWarnings()) {
+                return;
+            }
+        }
+    }
+
+    private static X509Certificate verifySourceStampCertificate(
+            ByteBuffer sourceStampBlockData,
+            CertificateFactory certFactory,
+            byte[] sourceStampCertificateDigest,
+            ApkSigningBlockUtils.Result.SignerInfo result)
+            throws NoSuchAlgorithmException, ApkFormatException {
+        // Parse the SourceStamp certificate.
+        byte[] sourceStampEncodedCertificate =
+                ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+        X509Certificate sourceStampCertificate;
+        try {
+            sourceStampCertificate =
+                    X509CertificateUtils.generateCertificate(
+                            sourceStampEncodedCertificate, certFactory);
+        } catch (CertificateException e) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+            return null;
+        }
+        // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+        // form. Without this, getEncoded may return a different form from what was stored in
+        // the signature. This is because some X509Certificate(Factory) implementations
+        // re-encode certificates.
+        sourceStampCertificate =
+                new GuaranteedEncodedFormX509Certificate(
+                        sourceStampCertificate, sourceStampEncodedCertificate);
+        result.certs.add(sourceStampCertificate);
+        // Verify the SourceStamp certificate found in the signing block is the same as the
+        // SourceStamp certificate found in the APK.
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+        messageDigest.update(sourceStampEncodedCertificate);
+        byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+        if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+            result.addWarning(
+                    ApkVerifier.Issue
+                            .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+                    ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
+                    ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
+            return null;
+        }
+        return sourceStampCertificate;
+    }
+
+    private static void verifySourceStampSignature(
+            byte[] apkDigest,
+            int minSdkVersion,
+            int maxSdkVersion,
+            X509Certificate sourceStampCertificate,
+            ByteBuffer signedData,
+            ApkSigningBlockUtils.Result.SignerInfo result)
+            throws ApkFormatException {
+        // Parse the signatures block and identify supported signatures
+        ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+        int signatureCount = 0;
+        List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+        while (signatures.hasRemaining()) {
+            signatureCount++;
+            try {
+                ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+                int sigAlgorithmId = signature.getInt();
+                byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+                SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+                if (signatureAlgorithm == null) {
+                    result.addWarning(
+                            ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+                    continue;
+                }
+                supportedSignatures.add(
+                        new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+            } catch (ApkFormatException | BufferUnderflowException e) {
+                result.addWarning(
+                        ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+                return;
+            }
+        }
+        if (supportedSignatures.isEmpty()) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+            return;
+        }
+        // Verify signatures over digests using the SourceStamp's certificate.
+        List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+        try {
+            signaturesToVerify =
+                    ApkSigningBlockUtils.getSignaturesToVerify(
+                            supportedSignatures, minSdkVersion, maxSdkVersion);
+        } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+            result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+            return;
+        }
+        for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+            SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+            String jcaSignatureAlgorithm =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+            AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+            PublicKey publicKey = sourceStampCertificate.getPublicKey();
+            try {
+                Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+                sig.initVerify(publicKey);
+                if (jcaSignatureAlgorithmParams != null) {
+                    sig.setParameter(jcaSignatureAlgorithmParams);
+                }
+                sig.update(apkDigest);
+                byte[] sigBytes = signature.signature;
+                if (!sig.verify(sigBytes)) {
+                    result.addWarning(
+                            ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+                    return;
+                }
+            } catch (InvalidKeyException
+                    | InvalidAlgorithmParameterException
+                    | SignatureException
+                    | NoSuchAlgorithmException e) {
+                result.addWarning(
+                        ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+                return;
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..dacd0be
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+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.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.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <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;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SourceStampSigner() {}
+
+    public static Pair<byte[], Integer> generateSourceStampBlock(
+            SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (sourceStampSignerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+            digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+        SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+        try {
+            sourceStampBlock.stampCertificate =
+                    sourceStampSignerConfig.certificates.get(0).getEncoded();
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException(
+                    "Retrieving the encoded form of the stamp certificate failed", e);
+        }
+
+        byte[] digestBytes =
+                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+        sourceStampBlock.signedDigests =
+                ApkSigningBlockUtils.generateSignaturesOverData(
+                        sourceStampSignerConfig, digestBytes);
+
+        // FORMAT:
+        // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+        // * length-prefixed sequence of length-prefixed signatures:
+        //   * uint32: signature algorithm ID
+        //   * length-prefixed bytes: signature of signed data
+        byte[] sourceStampSignerBlock =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            sourceStampBlock.stampCertificate,
+                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                    sourceStampBlock.signedDigests),
+                        });
+
+        // FORMAT:
+        // * length-prefixed stamp block.
+        return Pair.of(
+                encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_SOURCE_STAMP_BLOCK_ID);
+    }
+
+    private static final class SourceStampBlock {
+        public byte[] stampCertificate;
+        public List<Pair<Integer, byte[]>> signedDigests;
+    }
+}
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
new file mode 100644
index 0000000..8a3e776
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package 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 com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
+ */
+public abstract class V1SourceStampVerifier {
+
+    /** Hidden constructor to prevent instantiation. */
+    private V1SourceStampVerifier() {}
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+     * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see {@link
+     * ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *     required cryptographic algorithm implementation is missing
+     * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+     *     found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException,
+                    ApkSigningBlockUtils.SignatureNotFoundException {
+        ApkSigningBlockUtils.Result result =
+                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(
+                        apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
+
+        verify(
+                signatureInfo.signatureBlock,
+                sourceStampCertificateDigest,
+                apkContentDigests,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+     * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+     * more information about the contract of this method.
+     */
+    private static void verify(
+            ByteBuffer sourceStampBlock,
+            byte[] sourceStampCertificateDigest,
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result)
+            throws NoSuchAlgorithmException {
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        result.signers.add(signerInfo);
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            ByteBuffer sourceStampBlockData =
+                    ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+            byte[] digestBytes =
+                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                            getApkDigests(apkContentDigests));
+            SourceStampVerifier.verifyV1SourceStamp(
+                    sourceStampBlockData,
+                    certFactory,
+                    signerInfo,
+                    digestBytes,
+                    sourceStampCertificateDigest,
+                    minSdkVersion,
+                    maxSdkVersion);
+            result.verified = !result.containsErrors() && !result.containsWarnings();
+        } 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);
+        }
+    }
+
+    private static List<Pair<Integer, byte[]>> getApkDigests(
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+                apkContentDigests.entrySet()) {
+            digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+        return digests;
+    }
+}
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
new file mode 100644
index 0000000..16062bf
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.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.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+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.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <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;
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SourceStampSigner() {}
+
+    public static Pair<byte[], Integer> generateSourceStampBlock(
+            SignerConfig sourceStampSignerConfig,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (sourceStampSignerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+
+        // Extract the digests for signature schemes.
+        List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
+        getSignedDigestsFor(
+                VERSION_APK_SIGNATURE_SCHEME_V3,
+                signatureSchemeDigestInfos,
+                sourceStampSignerConfig,
+                signatureSchemeDigests);
+        getSignedDigestsFor(
+                VERSION_APK_SIGNATURE_SCHEME_V2,
+                signatureSchemeDigestInfos,
+                sourceStampSignerConfig,
+                signatureSchemeDigests);
+        getSignedDigestsFor(
+                VERSION_JAR_SIGNATURE_SCHEME,
+                signatureSchemeDigestInfos,
+                sourceStampSignerConfig,
+                signatureSchemeDigests);
+        signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst));
+
+        SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+        try {
+            sourceStampBlock.stampCertificate =
+                    sourceStampSignerConfig.certificates.get(0).getEncoded();
+        } catch (CertificateEncodingException e) {
+            throw new SignatureException(
+                    "Retrieving the encoded form of the stamp certificate failed", e);
+        }
+
+        sourceStampBlock.signedDigests = signatureSchemeDigests;
+
+        // 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
+        byte[] sourceStampSignerBlock =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            sourceStampBlock.stampCertificate,
+                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                    sourceStampBlock.signedDigests),
+                        });
+
+        // FORMAT:
+        // * length-prefixed stamp block.
+        return Pair.of(
+                encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID);
+    }
+
+    private static void getSignedDigestsFor(
+            int signatureSchemeVersion,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos,
+            SignerConfig sourceStampSignerConfig,
+            List<Pair<Integer, byte[]>> signatureSchemeDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+            return;
+        }
+
+        Map<ContentDigestAlgorithm, byte[]> digestInfo =
+                signatureSchemeDigestInfos.get(signatureSchemeVersion);
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+            digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+        }
+        digests.sort(Comparator.comparing(Pair::getFirst));
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed digests:
+        //   * uint32: digest algorithm id
+        //   * length-prefixed bytes: digest of the respective digest algorithm
+        byte[] digestBytes =
+                encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signed digests:
+        //   * uint32: signature algorithm id
+        //   * length-prefixed bytes: signed digest for the respective signature algorithm
+        List<Pair<Integer, byte[]>> signedDigest =
+                ApkSigningBlockUtils.generateSignaturesOverData(
+                        sourceStampSignerConfig, digestBytes);
+
+        // FORMAT:
+        // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+        //   * uint32: signature scheme id
+        //   * length-prefixed bytes: signed digests for the respective signature scheme
+        signatureSchemeDigests.add(
+                Pair.of(
+                        signatureSchemeVersion,
+                        encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                signedDigest)));
+    }
+
+    private static final class SourceStampBlock {
+        public byte[] stampCertificate;
+        public List<Pair<Integer, byte[]>> signedDigests;
+    }
+}
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
new file mode 100644
index 0000000..8a776fc
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
+ */
+public abstract class V2SourceStampVerifier {
+
+    /** Hidden constructor to prevent instantiation. */
+    private V2SourceStampVerifier() {}
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+     * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+     * {@code true}. If verification fails, the result will contain errors -- see {@link
+     * ApkSigningBlockUtils.Result#getErrors()}.
+     *
+     * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+     *     required cryptographic algorithm implementation is missing
+     * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+     *     found
+     * @throws IOException if an I/O error occurs when reading the APK
+     */
+    public static ApkSigningBlockUtils.Result verify(
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            byte[] sourceStampCertificateDigest,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException,
+                    ApkSigningBlockUtils.SignatureNotFoundException {
+        ApkSigningBlockUtils.Result result =
+                new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(
+                        apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result);
+
+        verify(
+                signatureInfo.signatureBlock,
+                sourceStampCertificateDigest,
+                signatureSchemeApkContentDigests,
+                minSdkVersion,
+                maxSdkVersion,
+                result);
+        return result;
+    }
+
+    /**
+     * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+     * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+     * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+     * more information about the contract of this method.
+     */
+    private static void verify(
+            ByteBuffer sourceStampBlock,
+            byte[] sourceStampCertificateDigest,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+            int minSdkVersion,
+            int maxSdkVersion,
+            ApkSigningBlockUtils.Result result)
+            throws NoSuchAlgorithmException {
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        result.signers.add(signerInfo);
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            ByteBuffer sourceStampBlockData =
+                    ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+            SourceStampVerifier.verifyV2SourceStamp(
+                    sourceStampBlockData,
+                    certFactory,
+                    signerInfo,
+                    getSignatureSchemeDigests(signatureSchemeApkContentDigests),
+                    sourceStampCertificateDigest,
+                    minSdkVersion,
+                    maxSdkVersion);
+            result.verified = !result.containsErrors() && !result.containsWarnings();
+        } 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);
+        }
+    }
+
+    private static Map<Integer, byte[]> getSignatureSchemeDigests(
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
+        Map<Integer, byte[]> digests = new HashMap<>();
+        for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
+                signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
+            List<Pair<Integer, byte[]>> apkDigests =
+                    getApkDigests(signatureSchemeApkContentDigest.getValue());
+            digests.put(
+                    signatureSchemeApkContentDigest.getKey(),
+                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
+        }
+        return digests;
+    }
+
+    private static List<Pair<Integer, byte[]>> getApkDigests(
+            Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+        List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+        for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+                apkContentDigests.entrySet()) {
+            digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+        }
+        Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+        return digests;
+    }
+}
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 f900211..89f16d5 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
@@ -16,26 +16,20 @@
 
 package com.android.apksig.internal.apk.v1;
 
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
+
 import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.internal.asn1.Asn1DerEncoder;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.asn1.Asn1EncodingException;
-import com.android.apksig.internal.asn1.Asn1OpaqueObject;
-import com.android.apksig.internal.asn1.ber.BerEncoding;
 import com.android.apksig.internal.jar.ManifestWriter;
 import com.android.apksig.internal.jar.SignatureFileWriter;
 import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
-import com.android.apksig.internal.pkcs7.ContentInfo;
-import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
-import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
-import com.android.apksig.internal.pkcs7.Pkcs7Constants;
-import com.android.apksig.internal.pkcs7.SignedData;
-import com.android.apksig.internal.pkcs7.SignerIdentifier;
-import com.android.apksig.internal.pkcs7.SignerInfo;
 import com.android.apksig.internal.util.Pair;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -43,6 +37,7 @@
 import java.security.PublicKey;
 import java.security.Signature;
 import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -57,7 +52,6 @@
 import java.util.TreeMap;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
-import javax.security.auth.x500.X500Principal;
 
 /**
  * APK signer which uses JAR signing (aka v1 signing scheme).
@@ -487,9 +481,7 @@
         return out.toByteArray();
     }
 
-    /** ASN.1 DER-encoded {@code NULL}. */
-    private static final Asn1OpaqueObject ASN1_DER_NULL =
-            new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
+
 
     /**
      * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
@@ -541,126 +533,21 @@
                     e);
         }
 
-        // Wrap the signature into the JAR signature block which is created according to CMS PKCS #7
-        // RFC 5652.
-        // The high-level simplified structure is as follows:
-        // ContentInfo
-        //   digestAlgorithm
-        //   SignedData
-        //     bag of certificates
-        //     SignerInfo
-        //       signing cert issuer and serial number (for locating the cert in the above bag)
-        //       digestAlgorithm
-        //       signatureAlgorithm
-        //       signature
+        AlgorithmIdentifier digestAlgorithmId =
+                getSignerInfoDigestAlgorithmOid(digestAlgorithm);
+        AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
         try {
-            SignerInfo signerInfo = new SignerInfo();
-            signerInfo.version = 1;
-            X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
-            signerInfo.sid =
-                    new SignerIdentifier(
-                            new IssuerAndSerialNumber(
-                                    new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
-                                    signingCert.getSerialNumber()));
-            AlgorithmIdentifier digestAlgorithmId =
-                    getSignerInfoDigestAlgorithmOid(digestAlgorithm);
-            AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
-            signerInfo.digestAlgorithm = digestAlgorithmId;
-            signerInfo.signatureAlgorithm = signatureAlgorithmId;
-            signerInfo.signature = ByteBuffer.wrap(signatureBytes);
-
-            SignedData signedData = new SignedData();
-            signedData.certificates = new ArrayList<>(signerCerts.size());
-            for (X509Certificate cert : signerCerts) {
-                signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
-            }
-            signedData.version = 1;
-            signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
-            signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
-            signedData.signerInfos = Collections.singletonList(signerInfo);
-
-            ContentInfo contentInfo = new ContentInfo();
-            contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
-            contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
-            return Asn1DerEncoder.encode(contentInfo);
-        } catch (Asn1EncodingException e) {
-            throw new SignatureException("Failed to encode signature block", e);
+            return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
+                    signatureBytes,
+                    null,
+                    signerCerts, digestAlgorithmId,
+                    signatureAlgorithmId);
+        } catch (Asn1EncodingException | CertificateEncodingException ex) {
+            throw new SignatureException("Failed to encode signature block");
         }
     }
 
-    /**
-     * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
-     * algorithm.
-     */
-    private static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
-            DigestAlgorithm digestAlgorithm) {
-        switch (digestAlgorithm) {
-            case SHA1:
-                return new AlgorithmIdentifier(
-                        V1SchemeVerifier.Signer.OID_DIGEST_SHA1, ASN1_DER_NULL);
-            case SHA256:
-                return new AlgorithmIdentifier(
-                        V1SchemeVerifier.Signer.OID_DIGEST_SHA256, ASN1_DER_NULL);
-            default:
-                throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
-        }
-    }
 
-    /**
-     * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
-     * when signing with the specified key and digest algorithm.
-     */
-    private static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
-            PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
-        String keyAlgorithm = publicKey.getAlgorithm();
-        String jcaDigestPrefixForSigAlg;
-        switch (digestAlgorithm) {
-            case SHA1:
-                jcaDigestPrefixForSigAlg = "SHA1";
-                break;
-            case SHA256:
-                jcaDigestPrefixForSigAlg = "SHA256";
-                break;
-            default:
-                throw new IllegalArgumentException(
-                        "Unexpected digest algorithm: " + digestAlgorithm);
-        }
-        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-            return Pair.of(
-                    jcaDigestPrefixForSigAlg + "withRSA",
-                    new AlgorithmIdentifier(V1SchemeVerifier.Signer.OID_SIG_RSA, ASN1_DER_NULL));
-        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-            AlgorithmIdentifier sigAlgId;
-            switch (digestAlgorithm) {
-                case SHA1:
-                    sigAlgId =
-                            new AlgorithmIdentifier(
-                                    V1SchemeVerifier.Signer.OID_SIG_DSA, ASN1_DER_NULL);
-                    break;
-                case SHA256:
-                    // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
-                    // 21 and higher. However, there are two ways to specify their SignedData
-                    // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
-                    // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
-                    // the former.
-                    sigAlgId =
-                            new AlgorithmIdentifier(
-                                    V1SchemeVerifier.Signer.OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
-                    break;
-                default:
-                    throw new IllegalArgumentException(
-                            "Unexpected digest algorithm: " + digestAlgorithm);
-            }
-            return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
-        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-            return Pair.of(
-                    jcaDigestPrefixForSigAlg + "withECDSA",
-                    new AlgorithmIdentifier(
-                            V1SchemeVerifier.Signer.OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
-        } else {
-            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
-        }
-    }
 
     private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
         switch (digestAlgorithm) {
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 47d5b01..111ac71 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
@@ -16,6 +16,12 @@
 
 package com.android.apksig.internal.apk.v1;
 
+import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
+import static com.android.apksig.internal.x509.Certificate.findCertificate;
+import static com.android.apksig.internal.x509.Certificate.parseCertificates;
+
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.ApkVerifier.IssueWithParams;
 import com.android.apksig.apk.ApkFormatException;
@@ -27,18 +33,15 @@
 import com.android.apksig.internal.asn1.Asn1OpaqueObject;
 import com.android.apksig.internal.asn1.Asn1Type;
 import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.oid.OidConstants;
 import com.android.apksig.internal.pkcs7.Attribute;
 import com.android.apksig.internal.pkcs7.ContentInfo;
-import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
 import com.android.apksig.internal.pkcs7.Pkcs7Constants;
 import com.android.apksig.internal.pkcs7.Pkcs7DecodingException;
 import com.android.apksig.internal.pkcs7.SignedData;
-import com.android.apksig.internal.pkcs7.SignerIdentifier;
 import com.android.apksig.internal.pkcs7.SignerInfo;
 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.InclusiveIntRange;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -48,7 +51,6 @@
 import com.android.apksig.zip.ZipFormatException;
 
 import java.io.IOException;
-import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.security.InvalidKeyException;
@@ -74,8 +76,6 @@
 import java.util.StringTokenizer;
 import java.util.jar.Attributes;
 
-import javax.security.auth.x500.X500Principal;
-
 /**
  * APK verifier which uses JAR signing (aka v1 signing scheme).
  *
@@ -606,13 +606,13 @@
                     desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
             if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
                 String digestAlgorithmUserFriendly =
-                        OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                        OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
                                 digestAlgorithmOid);
                 if (digestAlgorithmUserFriendly == null) {
                     digestAlgorithmUserFriendly = digestAlgorithmOid;
                 }
                 String signatureAlgorithmUserFriendly =
-                        OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                        OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
                                 signatureAlgorithmOid);
                 if (signatureAlgorithmUserFriendly == null) {
                     signatureAlgorithmUserFriendly = signatureAlgorithmOid;
@@ -768,42 +768,7 @@
             return signingCertificate;
         }
 
-        private static List<X509Certificate> parseCertificates(
-                List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
-            if (encodedCertificates.isEmpty()) {
-                return Collections.emptyList();
-            }
 
-            List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
-            for (int i = 0; i < encodedCertificates.size(); i++) {
-                Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
-                X509Certificate certificate;
-                byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
-                try {
-                    certificate = X509CertificateUtils.generateCertificate(encodedForm);
-                } catch (CertificateException e) {
-                    throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
-                }
-                // 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 and/or some implementations of
-                // X509Certificate.getEncoded() re-encode certificates.
-                certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
-                result.add(certificate);
-            }
-            return result;
-        }
-
-        public static X509Certificate findCertificate(
-                Collection<X509Certificate> certs, SignerIdentifier id) {
-            for (X509Certificate cert : certs) {
-                if (isMatchingCerticicate(cert, id)) {
-                    return cert;
-                }
-            }
-            return null;
-        }
 
         public static List<X509Certificate> getCertificateChain(
                 List<X509Certificate> certs, X509Certificate leaf) {
@@ -832,495 +797,8 @@
             return result;
         }
 
-        private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) {
-            if (id.issuerAndSerialNumber == null) {
-                // Android doesn't support any other means of identifying the signing certificate
-                return false;
-            }
-            IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber;
-            byte[] encodedIssuer =
-                    ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded());
-            X500Principal idIssuer = new X500Principal(encodedIssuer);
-            BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber;
-            return idSerialNumber.equals(cert.getSerialNumber())
-                    && idIssuer.equals(cert.getIssuerX500Principal());
-        }
-
-        private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
-        static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
-        private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
-        static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
-        private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
-        private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
-
-        static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
-        private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
-        private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
-        private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
-        private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
-        private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
-        private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
-
-        static final String OID_SIG_DSA = "1.2.840.10040.4.1";
-        private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
-        private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
-        static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
-        static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
-        static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
-
-        static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
-        private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
-        private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
-        private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
-        private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
-        private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
-
-        private static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
-                new HashMap<>();
-        {
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_RSA,
-                    InclusiveIntRange.from(0));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_RSA,
-                    InclusiveIntRange.from(0));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.from(0));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_RSA,
-                    InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_RSA,
-                    InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_RSA,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_RSA,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
-                    InclusiveIntRange.fromTo(21, 21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
-                    InclusiveIntRange.from(21));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_DSA,
-                    InclusiveIntRange.from(0));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.from(9));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_DSA,
-                    InclusiveIntRange.from(22));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_DSA,
-                    InclusiveIntRange.from(22));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.from(21));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
-                    InclusiveIntRange.from(18));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.from(18));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.from(21));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
-                    InclusiveIntRange.fromTo(21, 23));
-            addSupportedSigAlg(
-                    OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
-                    InclusiveIntRange.from(21));
-        }
-
-        private static void addSupportedSigAlg(
-                String digestAlgorithmOid,
-                String signatureAlgorithmOid,
-                InclusiveIntRange... supportedApiLevels) {
-            SUPPORTED_SIG_ALG_OIDS.put(
-                    digestAlgorithmOid + "with" + signatureAlgorithmOid,
-                    Arrays.asList(supportedApiLevels));
-        }
-
-        private List<InclusiveIntRange> getSigAlgSupportedApiLevels(
-                String digestAlgorithmOid,
-                String signatureAlgorithmOid) {
-            List<InclusiveIntRange> result =
-                    SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
-            return (result != null) ? result : Collections.emptyList();
-        }
-
-        private static class OidToUserFriendlyNameMapper {
-            private OidToUserFriendlyNameMapper() {}
-
-            private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
-            static {
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
-
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
 
 
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
-
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
-                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
-            }
-
-            public static String getUserFriendlyNameForOid(String oid) {
-                return OID_TO_USER_FRIENDLY_NAME.get(oid);
-            }
-        }
-
-        private static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
-        static {
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
-            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
-        }
-
-        private static String getJcaDigestAlgorithm(String oid)
-                throws SignatureException {
-            String result = OID_TO_JCA_DIGEST_ALG.get(oid);
-            if (result == null) {
-                throw new SignatureException("Unsupported digest algorithm: " + oid);
-            }
-            return result;
-        }
-
-        private static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
-        static {
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
-
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
-
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
-            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
-        }
-
-        private static String getJcaSignatureAlgorithm(
-                String digestAlgorithmOid,
-                String signatureAlgorithmOid) throws SignatureException {
-            // First check whether the signature algorithm OID alone is sufficient
-            String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
-            if (result != null) {
-                return result;
-            }
-
-            // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
-            // with signature algorithm OID.
-            String suffix;
-            if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
-                suffix = "RSA";
-            } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
-                suffix = "DSA";
-            } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
-                suffix = "ECDSA";
-            } else {
-                throw new SignatureException(
-                        "Unsupported JCA Signature algorithm"
-                                + " . Digest algorithm: " + digestAlgorithmOid
-                                + ", signature algorithm: " + signatureAlgorithmOid);
-            }
-            String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
-            // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
-            // SHA algorithms.
-            if (jcaDigestAlg.startsWith("SHA-")) {
-                jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
-            }
-            return jcaDigestAlg + "with" + suffix;
-        }
 
         public void verifySigFileAgainstManifest(
                 byte[] manifestBytes,
@@ -1827,13 +1305,10 @@
         Collections.sort(
                 cdRecordsSortedByLocalFileHeaderOffset,
                 CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
-        Set<String> manifestEntryNamesMissingFromApk =
-                new HashSet<>(entryNameToManifestSection.keySet());
         List<Signer> firstSignedEntrySigners = null;
         String firstSignedEntryName = null;
         for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
             String entryName = cdRecord.getName();
-            manifestEntryNamesMissingFromApk.remove(entryName);
             if (!isJarEntryDigestNeededInManifest(entryName)) {
                 continue;
             }
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 f325f8b..e812c3f 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
@@ -80,14 +80,12 @@
      * provided key.
      *
      * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
-     *        AndroidManifest.xml minSdkVersion attribute).
-     *
-     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
-     *         APK Signature Scheme v2
+     *     AndroidManifest.xml minSdkVersion attribute).
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+     *     Signature Scheme v2
      */
-    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
-            PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
-            throws InvalidKeyException {
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
         String keyAlgorithm = signingKey.getAlgorithm();
         if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
             // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -100,7 +98,7 @@
                 // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
                 List<SignatureAlgorithm> algorithms = new ArrayList<>();
                 algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
-                if (apkSigningBlockPaddingSupported) {
+                if (verityEnabled) {
                     algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
                 }
                 return algorithms;
@@ -113,7 +111,7 @@
             // DSA is supported only with SHA-256.
             List<SignatureAlgorithm> algorithms = new ArrayList<>();
             algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
-            if (apkSigningBlockPaddingSupported) {
+            if (verityEnabled) {
                 algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
             }
             return algorithms;
@@ -124,7 +122,7 @@
                 // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
                 List<SignatureAlgorithm> algorithms = new ArrayList<>();
                 algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
-                if (apkSigningBlockPaddingSupported) {
+                if (verityEnabled) {
                     algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
                 }
                 return algorithms;
@@ -138,28 +136,30 @@
         }
     }
 
-    public static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
-            RunnablesExecutor executor,
-            DataSource beforeCentralDir,
-            DataSource centralDir,
-            DataSource eocd,
-            List<SignerConfig> signerConfigs,
-            boolean v3SigningEnabled)
+    public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+            generateApkSignatureSchemeV2Block(
+                    RunnablesExecutor executor,
+                    DataSource beforeCentralDir,
+                    DataSource centralDir,
+                    DataSource eocd,
+                    List<SignerConfig> signerConfigs,
+                    boolean v3SigningEnabled)
                     throws IOException, InvalidKeyException, NoSuchAlgorithmException,
                             SignatureException {
-        Pair<List<SignerConfig>,
-                Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+        Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
                 ApkSigningBlockUtils.computeContentDigests(
                         executor, beforeCentralDir, centralDir, eocd, signerConfigs);
-        return generateApkSignatureSchemeV2Block(
-                digestInfo.getFirst(), digestInfo.getSecond(),v3SigningEnabled);
+        return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+                generateApkSignatureSchemeV2Block(
+                        digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled),
+                digestInfo.getSecond());
     }
 
     private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
             List<SignerConfig> signerConfigs,
             Map<ContentDigestAlgorithm, byte[]> contentDigests,
             boolean v3SigningEnabled)
-                    throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         // FORMAT:
         // * length-prefixed sequence of length-prefixed signer blocks.
 
@@ -178,17 +178,19 @@
             signerBlocks.add(signerBlock);
         }
 
-        return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
-                }), APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+        return Pair.of(
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+                        }),
+                APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
     }
 
     private static byte[] generateSignerBlock(
             SignerConfig signerConfig,
             Map<ContentDigestAlgorithm, byte[]> contentDigests,
             boolean v3SigningEnabled)
-                    throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         if (signerConfig.certificates.isEmpty()) {
             throw new SignatureException("No certificates configured for signer");
         }
@@ -211,7 +213,9 @@
             byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
             if (contentDigest == null) {
                 throw new RuntimeException(
-                        contentDigestAlgorithm + " content digest for " + signatureAlgorithm
+                        contentDigestAlgorithm
+                                + " content digest for "
+                                + signatureAlgorithm
                                 + " not computed");
             }
             digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
@@ -230,12 +234,15 @@
         //   * uint32: ID
         //   * (length - 4) bytes: value
 
-        signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
-            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
-            encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
-            signedData.additionalAttributes,
-            new byte[0],
-        });
+        signer.signedData =
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+                                    signedData.digests),
+                            encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
+                            signedData.additionalAttributes,
+                            new byte[0],
+                        });
         signer.publicKey = encodedPublicKey;
         signer.signatures = new ArrayList<>();
         signer.signatures =
@@ -290,5 +297,4 @@
             public byte[] additionalAttributes;
         }
     }
-
 }
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 0ef74a6..9b821a7 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
@@ -175,7 +175,7 @@
      * expected to be encountered on an Android platform version in the
      * {@code [minSdkVersion, maxSdkVersion]} range.
      */
-    private static void parseSigners(
+    public static void parseSigners(
             ByteBuffer apkSignatureSchemeV2Block,
             Set<ContentDigestAlgorithm> contentDigestsToVerify,
             Map<Integer, String> supportedApkSigSchemeNames,
@@ -294,7 +294,7 @@
                     ApkSigningBlockUtils.getSignaturesToVerify(
                             supportedSignatures, minSdkVersion, maxSdkVersion);
         } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
-            result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
+            result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
             return;
         }
         for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
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 722b304..56ab60e 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
@@ -52,10 +52,9 @@
  * Signature Scheme v2 goals.
  *
  * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
- *
- * <p> The main contribution of APK Signature Scheme v3 is the introduction of the
- * {@link SigningCertificateLineage}, which enables an APK to change its signing
- * certificate as long as it can prove the new siging certificate was signed by the old.
+ *     <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
+ *     SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
+ *     it can prove the new siging certificate was signed by the old.
  */
 public abstract class V3SchemeSigner {
 
@@ -69,14 +68,12 @@
      * provided key.
      *
      * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
-     *        AndroidManifest.xml minSdkVersion attribute).
-     *
-     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
-     *         APK Signature Scheme v3
+     *     AndroidManifest.xml minSdkVersion attribute).
+     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+     *     Signature Scheme v3
      */
-    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
-            PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
-            throws InvalidKeyException {
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
         String keyAlgorithm = signingKey.getAlgorithm();
         if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
             // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -89,7 +86,7 @@
                 // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
                 List<SignatureAlgorithm> algorithms = new ArrayList<>();
                 algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
-                if (apkSigningBlockPaddingSupported) {
+                if (verityEnabled) {
                     algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
                 }
                 return algorithms;
@@ -102,7 +99,7 @@
             // DSA is supported only with SHA-256.
             List<SignatureAlgorithm> algorithms = new ArrayList<>();
             algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
-            if (apkSigningBlockPaddingSupported) {
+            if (verityEnabled) {
                 algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
             }
             return algorithms;
@@ -113,7 +110,7 @@
                 // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
                 List<SignatureAlgorithm> algorithms = new ArrayList<>();
                 algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
-                if (apkSigningBlockPaddingSupported) {
+                if (verityEnabled) {
                     algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
                 }
                 return algorithms;
@@ -127,28 +124,28 @@
         }
     }
 
-    public static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
-            RunnablesExecutor executor,
-            DataSource beforeCentralDir,
-            DataSource centralDir,
-            DataSource eocd,
-            List<SignerConfig> signerConfigs)
+    public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+            generateApkSignatureSchemeV3Block(
+                    RunnablesExecutor executor,
+                    DataSource beforeCentralDir,
+                    DataSource centralDir,
+                    DataSource eocd,
+                    List<SignerConfig> signerConfigs)
                     throws IOException, InvalidKeyException, NoSuchAlgorithmException,
                             SignatureException {
-        Pair<List<SignerConfig>,
-                Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+        Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
                 ApkSigningBlockUtils.computeContentDigests(
                         executor, beforeCentralDir, centralDir, eocd, signerConfigs);
-        return generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond());
+        return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+                generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
+                digestInfo.getSecond());
     }
 
     private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
-            List<SignerConfig> signerConfigs,
-            Map<ContentDigestAlgorithm, byte[]> contentDigests)
-                    throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+            List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         // FORMAT:
         // * length-prefixed sequence of length-prefixed signer blocks.
-
         List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
         int signerNumber = 0;
         for (SignerConfig signerConfig : signerConfigs) {
@@ -164,16 +161,17 @@
             signerBlocks.add(signerBlock);
         }
 
-        return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
-                }), APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+        return Pair.of(
+                encodeAsSequenceOfLengthPrefixedElements(
+                        new byte[][] {
+                            encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+                        }),
+                APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
     }
 
     private static byte[] generateSignerBlock(
-            SignerConfig signerConfig,
-            Map<ContentDigestAlgorithm, byte[]> contentDigests)
-                    throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+            SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         if (signerConfig.certificates.isEmpty()) {
             throw new SignatureException("No certificates configured for signer");
         }
@@ -196,7 +194,9 @@
             byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
             if (contentDigest == null) {
                 throw new RuntimeException(
-                        contentDigestAlgorithm + " content digest for " + signatureAlgorithm
+                        contentDigestAlgorithm
+                                + " content digest for "
+                                + signatureAlgorithm
                                 + " not computed");
             }
             digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
@@ -216,7 +216,6 @@
         signer.signatures =
                 ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
 
-
         return encodeSigner(signer);
     }
 
@@ -236,12 +235,7 @@
         //   * uint32: signature algorithm ID
         //   * length-prefixed bytes: signature of signed data
         // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
-        int payloadSize =
-                signedData.length
-                + 4
-                + 4
-                + signatures.length
-                + publicKey.length;
+        int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
 
         ByteBuffer result = ByteBuffer.allocate(payloadSize);
         result.order(ByteOrder.LITTLE_ENDIAN);
@@ -277,12 +271,7 @@
         //   * (length - 4) bytes: value
         //   * uint32: Proof-of-rotation ID: 0x3ba06f8c
         //   * length-prefixed roof-of-rotation structure
-        int payloadSize =
-                digests.length
-                        + certs.length
-                        + 4
-                        + 4
-                        + attributes.length;
+        int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
 
         ByteBuffer result = ByteBuffer.allocate(payloadSize);
         result.order(ByteOrder.LITTLE_ENDIAN);
@@ -321,5 +310,4 @@
             public byte[] additionalAttributes;
         }
     }
-
 }
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 f263323..659d379 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
@@ -53,6 +53,7 @@
 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;
@@ -228,7 +229,7 @@
      * expected to be encountered on an Android platform version in the
      * {@code [minSdkVersion, maxSdkVersion]} range.
      */
-    private static void parseSigners(
+    public static void parseSigners(
             ByteBuffer apkSignatureSchemeV3Block,
             Set<ContentDigestAlgorithm> contentDigestsToVerify,
             ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
@@ -521,5 +522,4 @@
             }
         }
     }
-
 }
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
new file mode 100644
index 0000000..73ba46f
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -0,0 +1,356 @@
+/*
+ * 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.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 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;
+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.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
+ * The fields:
+ * <p>
+ * 1. hashingInfo - verity root hash and hashing info,
+ * 2. signingInfo - certificate, public key and signature,
+ * For more details see V4Signature.
+ * </p>
+ * (optional) verityTree: integer size prepended bytes of the verity hash tree.
+ * <p>
+ * TODO(schfan): Add v4 unit tests
+ */
+public abstract class V4SchemeSigner {
+    /**
+     * Hidden constructor to prevent instantiation.
+     */
+    private V4SchemeSigner() {
+    }
+
+    /**
+     * Based on a public key, return a signing algorithm that supports verity.
+     */
+    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+            int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+            throws InvalidKeyException {
+        List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+                signingKey, minSdkVersion,
+                apkSigningBlockPaddingSupported);
+        // Keeping only supported algorithms.
+        for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+            final SignatureAlgorithm algorithm = iter.next();
+            if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
+                iter.remove();
+            }
+        }
+        return algorithms;
+    }
+
+    /**
+     * Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
+     * output file.
+     */
+    public static void generateV4Signature(
+        DataSource apkContent, SignerConfig signerConfig, File outputFile)
+        throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+      Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
+      try (final OutputStream output = new FileOutputStream(outputFile)) {
+        pair.getFirst().writeTo(output);
+        V4Signature.writeBytes(output, pair.getSecond());
+      } catch (IOException e) {
+        outputFile.delete();
+        throw e;
+      }
+    }
+
+    /** Generate v4 signature and hash tree for a given APK. */
+    public static Pair<V4Signature, byte[]> generateV4Signature(
+            DataSource apkContent,
+            SignerConfig signerConfig)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+        // Salt has to stay empty for fs-verity compatibility.
+        final byte[] salt = null;
+        // Not used by apksigner.
+        final byte[] additionalData = null;
+
+        final long fileSize = apkContent.size();
+
+        // Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512).
+        final byte[] apkDigest = getApkDigest(apkContent);
+
+        // Obtaining the merkle tree and the root hash in verity format.
+        ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
+                ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+        final ContentDigestAlgorithm verityContentDigestAlgorithm =
+                verityContentDigestInfo.contentDigestAlgorithm;
+        final byte[] rootHash = verityContentDigestInfo.rootHash;
+        final byte[] tree = verityContentDigestInfo.tree;
+
+        final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
+                verityContentDigestAlgorithm);
+        final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
+                hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
+                salt, rootHash);
+
+        // Generating SigningInfo and combining everything into V4Signature.
+        final V4Signature signature;
+        try {
+            signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData,
+                    fileSize);
+        } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
+            throw new InvalidKeyException("Signer failed", e);
+        }
+
+        return Pair.of(signature, tree);
+    }
+
+    private static V4Signature generateSignature(
+            SignerConfig signerConfig,
+            V4Signature.HashingInfo hashingInfo,
+            byte[] apkDigest, byte[] additionaData, long fileSize)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+            CertificateEncodingException {
+        if (signerConfig.certificates.isEmpty()) {
+            throw new SignatureException("No certificates configured for signer");
+        }
+        if (signerConfig.certificates.size() != 1) {
+            throw new CertificateEncodingException("Should only have one certificate");
+        }
+
+        // Collecting data for signing.
+        final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+        final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+        final byte[] encodedCertificate = encodedCertificates.get(0);
+
+        final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
+                encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
+
+        final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+                signingInfoNoSignature);
+
+        // Signing.
+        final List<Pair<Integer, byte[]>> signatures =
+                ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
+        if (signatures.size() != 1) {
+            throw new SignatureException("Should only be one signature generated");
+        }
+
+        final int signatureAlgorithmId = signatures.get(0).getFirst();
+        final byte[] signature = signatures.get(0).getSecond();
+
+        final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
+                encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+                signature);
+
+        return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+                signingInfo.toByteArray());
+    }
+
+    // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
+    private static byte[] getApkDigest(DataSource apk) throws IOException {
+        ApkUtils.ZipSections zipSections;
+        try {
+            zipSections = ApkUtils.findZipSections(apk);
+        } catch (ZipFormatException e) {
+            throw new IOException("Malformed APK: not a ZIP archive", e);
+        }
+
+        final SignatureException v3Exception;
+        try {
+            return getBestV3Digest(apk, zipSections);
+        } catch (SignatureException e) {
+            v3Exception = e;
+        }
+
+        final SignatureException v2Exception;
+        try {
+            return getBestV2Digest(apk, zipSections);
+        } catch (SignatureException e) {
+            v2Exception = e;
+        }
+
+        throw new IOException(
+                "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+                        + v2Exception);
+    }
+
+    private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws SignatureException {
+        final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+        try {
+            final SignatureInfo signatureInfo =
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+            final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+            V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
+                    result);
+        } catch (Exception e) {
+            throw new SignatureException("Failed to extract and parse v3 block", e);
+        }
+
+        if (result.signers.size() != 1) {
+            throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+        }
+
+        ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+        if (signer.containsErrors()) {
+            throw new SignatureException("Parsing failed: " + signer.getErrors());
+        }
+
+        final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+                result.signers.get(0).contentDigests;
+        return pickBestDigest(contentDigests);
+    }
+
+    private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+            throws SignatureException {
+        final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+        final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        try {
+            final SignatureInfo signatureInfo =
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
+            final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
+            V2SchemeVerifier.parseSigners(
+                    apkSignatureSchemeV2Block,
+                    contentDigestsToVerify,
+                    Collections.emptyMap(),
+                    foundApkSigSchemeIds,
+                    Integer.MAX_VALUE,
+                    Integer.MAX_VALUE,
+                    result);
+        } catch (Exception e) {
+            throw new SignatureException("Failed to extract and parse v2 block", e);
+        }
+
+        if (result.signers.size() != 1) {
+            throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+        }
+
+        ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+        if (signer.containsErrors()) {
+            throw new SignatureException("Parsing failed: " + signer.getErrors());
+        }
+
+        final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+                signer.contentDigests;
+        return pickBestDigest(contentDigests);
+    }
+
+    private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
+        if (contentDigests == null || contentDigests.isEmpty()) {
+            throw new SignatureException("Should have at least one digest");
+        }
+
+        int bestAlgorithmOrder = -1;
+        byte[] bestDigest = null;
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+            final SignatureAlgorithm signatureAlgorithm =
+                    SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+            final ContentDigestAlgorithm contentDigestAlgorithm =
+                    signatureAlgorithm.getContentDigestAlgorithm();
+            if (!isSupported(contentDigestAlgorithm, true)) {
+                continue;
+            }
+            final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
+            if (bestAlgorithmOrder < algorithmOrder) {
+                bestAlgorithmOrder = algorithmOrder;
+                bestDigest = contentDigest.getValue();
+            }
+        }
+        if (bestDigest == null) {
+            throw new SignatureException("Failed to find a supported digest in the source APK");
+        }
+        return bestDigest;
+    }
+
+    // Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest
+    // verification in framework works.
+    public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
+        switch (contentDigestAlgorithm) {
+            case CHUNKED_SHA256:
+                return 0;
+            case VERITY_CHUNKED_SHA256:
+                return 1;
+            case CHUNKED_SHA512:
+                return 2;
+        }
+        return -1;
+    }
+
+    private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
+            boolean forV3Digest) {
+        if (contentDigestAlgorithm == null) {
+            return false;
+        }
+        if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+                || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
+                || (forV3Digest
+                     && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
+            throws NoSuchAlgorithmException {
+        switch (algorithm) {
+            case VERITY_CHUNKED_SHA256:
+                return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
+                        V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
+            default:
+                throw new NoSuchAlgorithmException(
+                        "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
+        }
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
new file mode 100644
index 0000000..0a8484b
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -0,0 +1,250 @@
+/*
+ * 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.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.ApkVerifier.Issue;
+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.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+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.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+/**
+ * APK Signature Scheme V4 verifier.
+ * <p>
+ * Verifies the serialized V4Signature file against an APK.
+ */
+public abstract class V4SchemeVerifier {
+    /**
+     * Hidden constructor to prevent instantiation.
+     */
+    private V4SchemeVerifier() {
+    }
+
+    /**
+     * <p>
+     * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
+     * signature block against the raw root hash bytes in the proto field 3) verifies that the raw
+     * root hash matches with the actual hash tree root of the give APK 4) if the file contains a
+     * verity tree, verifies that it matches with the actual verity tree computed from the given
+     * APK.
+     * </p>
+     */
+    public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
+            throws IOException, NoSuchAlgorithmException {
+        final V4Signature signature;
+        final byte[] tree;
+        try (InputStream input = new FileInputStream(v4SignatureFile)) {
+            signature = V4Signature.readFrom(input);
+            tree = V4Signature.readBytes(input);
+        }
+
+        final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+
+        if (signature == null) {
+            result.addError(Issue.V4_SIG_NO_SIGNATURES,
+                    "Signature file does not contain a v4 signature.");
+            return result;
+        }
+
+        if (signature.version != V4Signature.CURRENT_VERSION) {
+            result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
+                    V4Signature.CURRENT_VERSION);
+        }
+
+        V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+                signature.hashingInfo);
+        V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+                signature.signingInfo);
+
+        final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+
+        // First, verify the signature over signedData.
+        ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
+                signingInfo, signedData);
+        result.signers.add(signerInfo);
+        if (result.containsErrors()) {
+            return result;
+        }
+
+        // Second, check if the root hash and the tree are correct.
+        verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
+        if (!result.containsErrors()) {
+            result.verified = true;
+        }
+
+        return result;
+    }
+
+    /**
+     * Parses the provided signature block and populates the {@code result}.
+     * <p>
+     * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
+     * contained in the signature block. This method adds one or more errors to the {@code result}.
+     */
+    private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
+            V4Signature.SigningInfo signingInfo,
+            final byte[] signedData) throws NoSuchAlgorithmException {
+        final ApkSigningBlockUtils.Result.SignerInfo result =
+                new ApkSigningBlockUtils.Result.SignerInfo();
+        result.index = 0;
+
+        final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
+        final byte[] sigBytes = signingInfo.signature;
+        result.signatures.add(
+                new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+
+        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+        if (signatureAlgorithm == null) {
+            result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+            return result;
+        }
+
+        String jcaSignatureAlgorithm =
+                signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+        AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+                signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+
+        String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+
+        final byte[] publicKeyBytes = signingInfo.publicKey;
+        PublicKey publicKey;
+        try {
+            publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
+                    new X509EncodedKeySpec(publicKeyBytes));
+        } catch (Exception e) {
+            result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
+            return result;
+        }
+
+        try {
+            Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+            sig.initVerify(publicKey);
+            if (jcaSignatureAlgorithmParams != null) {
+                sig.setParameter(jcaSignatureAlgorithmParams);
+            }
+            sig.update(signedData);
+            if (!sig.verify(sigBytes)) {
+                result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+                return result;
+            }
+            result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException
+                | SignatureException e) {
+            result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+            return result;
+        }
+
+        if (signingInfo.certificate == null) {
+            result.addError(Issue.V4_SIG_NO_CERTIFICATE);
+            return result;
+        }
+
+        final X509Certificate certificate;
+        try {
+            // 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(
+                    X509CertificateUtils.generateCertificate(signingInfo.certificate),
+                    signingInfo.certificate);
+        } catch (CertificateException e) {
+            result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
+            return result;
+        }
+        result.certs.add(certificate);
+
+        byte[] certificatePublicKeyBytes;
+        try {
+            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+                    certificate.getPublicKey());
+        } catch (InvalidKeyException e) {
+            System.out.println("Caught an exception encoding the public key: " + e);
+            e.printStackTrace();
+            certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+        }
+        if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+            result.addError(
+                    Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+                    ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+                    ApkSigningBlockUtils.toHex(publicKeyBytes));
+            return result;
+        }
+
+        // Add apk digest from the file to the result.
+        ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
+                new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+                        0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
+        result.contentDigests.add(contentDigest);
+
+        return result;
+    }
+
+    private static void verifyRootHashAndTree(DataSource apkContent,
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
+            byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
+        ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
+                ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+        ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
+        final byte[] actualDigest = actualContentDigestInfo.rootHash;
+        final byte[] actualTree = actualContentDigestInfo.tree;
+
+        if (!Arrays.equals(expectedDigest, actualDigest)) {
+            signerInfo.addError(
+                    ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
+                    algorithm,
+                    toHex(expectedDigest),
+                    toHex(actualDigest));
+            return;
+        }
+        // Only check verity tree if it is not empty
+        if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
+            signerInfo.addError(
+                    ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
+                    algorithm,
+                    toHex(expectedDigest),
+                    toHex(actualDigest));
+            return;
+        }
+
+        signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
new file mode 100644
index 0000000..e36ed60
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -0,0 +1,225 @@
+/*
+ * 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.v4;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class V4Signature {
+    public static final int CURRENT_VERSION = 2;
+
+    public static final int HASHING_ALGORITHM_SHA256 = 1;
+    public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+
+    public static class HashingInfo {
+        public final int hashAlgorithm; // only 1 == SHA256 supported
+        public final byte log2BlockSize; // only 12 (block size 4096) supported now
+        public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
+        public final byte[] rawRootHash; // salted digest of the first Merkle tree page
+
+        HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
+            this.hashAlgorithm = hashAlgorithm;
+            this.log2BlockSize = log2BlockSize;
+            this.salt = salt;
+            this.rawRootHash = rawRootHash;
+        }
+
+        static HashingInfo fromByteArray(byte[] bytes) throws IOException {
+            ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+            final int hashAlgorithm = buffer.getInt();
+            final byte log2BlockSize = buffer.get();
+            byte[] salt = readBytes(buffer);
+            byte[] rawRootHash = readBytes(buffer);
+            return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
+        }
+
+        byte[] toByteArray() {
+            final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+                    + bytesSize(this.rawRootHash);
+            ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+            buffer.putInt(this.hashAlgorithm);
+            buffer.put(this.log2BlockSize);
+            writeBytes(buffer, this.salt);
+            writeBytes(buffer, this.rawRootHash);
+            return buffer.array();
+        }
+    }
+
+    public static class SigningInfo {
+        public final byte[] apkDigest;  // used to match with the corresponding APK
+        public final byte[] certificate; // ASN.1 DER form
+        public final byte[] additionalData; // a free-form binary data blob
+        public final byte[] publicKey; // ASN.1 DER, must match the certificate
+        public final int signatureAlgorithmId; // see the APK v2 doc for the list
+        public final byte[] signature;
+
+        SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
+                byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+            this.apkDigest = apkDigest;
+            this.certificate = certificate;
+            this.additionalData = additionalData;
+            this.publicKey = publicKey;
+            this.signatureAlgorithmId = signatureAlgorithmId;
+            this.signature = signature;
+        }
+
+        static SigningInfo fromByteArray(byte[] bytes) throws IOException {
+            ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+            byte[] apkDigest = readBytes(buffer);
+            byte[] certificate = readBytes(buffer);
+            byte[] additionalData = readBytes(buffer);
+            byte[] publicKey = readBytes(buffer);
+            int signatureAlgorithmId = buffer.getInt();
+            byte[] signature = readBytes(buffer);
+            return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
+                    signatureAlgorithmId, signature);
+        }
+
+        byte[] toByteArray() {
+            final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
+                    this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+                    + bytesSize(this.signature);
+            ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+            writeBytes(buffer, this.apkDigest);
+            writeBytes(buffer, this.certificate);
+            writeBytes(buffer, this.additionalData);
+            writeBytes(buffer, this.publicKey);
+            buffer.putInt(this.signatureAlgorithmId);
+            writeBytes(buffer, this.signature);
+            return buffer.array();
+        }
+    }
+
+    public final int version; // Always 2 for now.
+    public final byte[] hashingInfo;
+    public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
+
+    V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
+        this.version = version;
+        this.hashingInfo = hashingInfo;
+        this.signingInfo = signingInfo;
+    }
+
+    static V4Signature readFrom(InputStream stream) throws IOException {
+        final int version = readIntLE(stream);
+        if (version != CURRENT_VERSION) {
+            throw new IOException("Invalid signature version.");
+        }
+        final byte[] hashingInfo = readBytes(stream);
+        final byte[] signingInfo = readBytes(stream);
+        return new V4Signature(version, hashingInfo, signingInfo);
+    }
+
+    public void writeTo(OutputStream stream) throws IOException {
+        writeIntLE(stream, this.version);
+        writeBytes(stream, this.hashingInfo);
+        writeBytes(stream, this.signingInfo);
+    }
+
+    static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+        final int size =
+                4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
+                        hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
+                        signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
+                        signingInfo.additionalData);
+        ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putInt(size);
+        buffer.putLong(fileSize);
+        buffer.putInt(hashingInfo.hashAlgorithm);
+        buffer.put(hashingInfo.log2BlockSize);
+        writeBytes(buffer, hashingInfo.salt);
+        writeBytes(buffer, hashingInfo.rawRootHash);
+        writeBytes(buffer, signingInfo.apkDigest);
+        writeBytes(buffer, signingInfo.certificate);
+        writeBytes(buffer, signingInfo.additionalData);
+        return buffer.array();
+    }
+
+    // Utility methods.
+    static int bytesSize(byte[] bytes) {
+        return 4/*length*/ + (bytes == null ? 0 : bytes.length);
+    }
+
+    static void readFully(InputStream stream, byte[] buffer) throws IOException {
+        int len = buffer.length;
+        int n = 0;
+        while (n < len) {
+            int count = stream.read(buffer, n, len - n);
+            if (count < 0) {
+                throw new EOFException();
+            }
+            n += count;
+        }
+    }
+
+    static int readIntLE(InputStream stream) throws IOException {
+        final byte[] buffer = new byte[4];
+        readFully(stream, buffer);
+        return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
+    }
+
+    static void writeIntLE(OutputStream stream, int v) throws IOException {
+        final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
+        stream.write(buffer);
+    }
+
+    static byte[] readBytes(InputStream stream) throws IOException {
+        try {
+            final int size = readIntLE(stream);
+            final byte[] bytes = new byte[size];
+            readFully(stream, bytes);
+            return bytes;
+        } catch (EOFException ignored) {
+            return null;
+        }
+    }
+
+    static byte[] readBytes(ByteBuffer buffer) throws IOException {
+        if (buffer.remaining() < 4) {
+            throw new EOFException();
+        }
+        final int size = buffer.getInt();
+        if (buffer.remaining() < size) {
+            throw new EOFException();
+        }
+        final byte[] bytes = new byte[size];
+        buffer.get(bytes);
+        return bytes;
+    }
+
+    static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
+        if (bytes == null) {
+            writeIntLE(stream, 0);
+            return;
+        }
+        writeIntLE(stream, bytes.length);
+        stream.write(bytes);
+    }
+
+    static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+        if (bytes == null) {
+            buffer.putInt(0);
+            return;
+        }
+        buffer.putInt(bytes.length);
+        buffer.put(bytes);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
index d4a6fb6..160dc4e 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -507,23 +507,22 @@
 
     private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
         BigInteger value = integerToBigInteger(encoded);
-        try {
-            return value.intValueExact();
-        } catch (ArithmeticException e) {
+        if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+            || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
             throw new Asn1DecodingException(
-                    String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e);
+                String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
         }
+        return value.intValue();
     }
 
     private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
         BigInteger value = integerToBigInteger(encoded);
-        try {
-            return value.longValueExact();
-        } catch (ArithmeticException e) {
+        if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
+                || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
             throw new Asn1DecodingException(
-                    String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value),
-                    e);
+                String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
         }
+        return value.longValue();
     }
 
     private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
index 22a432f..901f5f3 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
@@ -590,4 +590,7 @@
                     "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
         }
     }
+    /** ASN.1 DER-encoded {@code NULL}. */
+    public static final Asn1OpaqueObject ASN1_DER_NULL =
+            new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
 }
diff --git a/src/main/java/com/android/apksig/internal/oid/OidConstants.java b/src/main/java/com/android/apksig/internal/oid/OidConstants.java
new file mode 100644
index 0000000..d80cbaa
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/oid/OidConstants.java
@@ -0,0 +1,463 @@
+/*
+ * 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.oid;
+
+import com.android.apksig.internal.util.InclusiveIntRange;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OidConstants {
+    public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
+    public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
+    public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
+    public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
+    public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
+    public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
+
+    public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
+    public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
+    public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
+    public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
+    public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
+    public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
+    public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
+
+    public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
+    public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
+    public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
+    public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+    public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+    public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
+
+    public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
+    public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
+    public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
+    public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
+    public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
+    public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
+
+    public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
+            new HashMap<>();
+    static {
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_RSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_RSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
+                InclusiveIntRange.fromTo(21, 21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
+                InclusiveIntRange.from(21));
+
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_DSA,
+                InclusiveIntRange.from(0));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.from(9));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_DSA,
+                InclusiveIntRange.from(22));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_DSA,
+                InclusiveIntRange.from(22));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.from(21));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
+                InclusiveIntRange.from(18));
+
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.from(18));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
+                InclusiveIntRange.fromTo(21, 23));
+        addSupportedSigAlg(
+                OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
+                InclusiveIntRange.from(21));
+    }
+
+    public static void addSupportedSigAlg(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid,
+            InclusiveIntRange... supportedApiLevels) {
+        SUPPORTED_SIG_ALG_OIDS.put(
+                digestAlgorithmOid + "with" + signatureAlgorithmOid,
+                Arrays.asList(supportedApiLevels));
+    }
+
+    public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid) {
+        List<InclusiveIntRange> result =
+                SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
+        return (result != null) ? result : Collections.emptyList();
+    }
+
+    public static class OidToUserFriendlyNameMapper {
+        private OidToUserFriendlyNameMapper() {}
+
+        private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
+        static {
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
+
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
+
+
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
+
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
+            OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
+        }
+
+        public static String getUserFriendlyNameForOid(String oid) {
+            return OID_TO_USER_FRIENDLY_NAME.get(oid);
+        }
+    }
+
+    public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
+    static {
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
+        OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
+    }
+
+    public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
+    static {
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
+
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
+
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
+        OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
+    }
+
+    private OidConstants() {}
+}
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
index 39bce94..c27c487 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
@@ -16,10 +16,27 @@
 
 package com.android.apksig.internal.pkcs7;
 
+import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
+
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
 import com.android.apksig.internal.asn1.Asn1Class;
 import com.android.apksig.internal.asn1.Asn1Field;
 import com.android.apksig.internal.asn1.Asn1OpaqueObject;
 import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
 
 /**
  * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
@@ -39,4 +56,114 @@
         this.algorithm = algorithmOid;
         this.parameters = parameters;
     }
+
+    /**
+     * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
+     * algorithm.
+     */
+    public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
+            DigestAlgorithm digestAlgorithm) {
+        switch (digestAlgorithm) {
+            case SHA1:
+                return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
+            case SHA256:
+                return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
+        }
+        throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+    }
+
+    /**
+     * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
+     * when signing with the specified key and digest algorithm.
+     */
+    public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
+            PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+        String keyAlgorithm = publicKey.getAlgorithm();
+        String jcaDigestPrefixForSigAlg;
+        switch (digestAlgorithm) {
+            case SHA1:
+                jcaDigestPrefixForSigAlg = "SHA1";
+                break;
+            case SHA256:
+                jcaDigestPrefixForSigAlg = "SHA256";
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unexpected digest algorithm: " + digestAlgorithm);
+        }
+        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+            return Pair.of(
+                    jcaDigestPrefixForSigAlg + "withRSA",
+                    new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
+        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+            AlgorithmIdentifier sigAlgId;
+            switch (digestAlgorithm) {
+                case SHA1:
+                    sigAlgId =
+                            new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
+                    break;
+                case SHA256:
+                    // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
+                    // 21 and higher. However, there are two ways to specify their SignedData
+                    // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
+                    // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
+                    // the former.
+                    sigAlgId =
+                            new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unexpected digest algorithm: " + digestAlgorithm);
+            }
+            return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
+        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+            return Pair.of(
+                    jcaDigestPrefixForSigAlg + "withECDSA",
+                    new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
+        } else {
+            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+        }
+    }
+
+    public static String getJcaSignatureAlgorithm(
+            String digestAlgorithmOid,
+            String signatureAlgorithmOid) throws SignatureException {
+        // First check whether the signature algorithm OID alone is sufficient
+        String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
+        if (result != null) {
+            return result;
+        }
+
+        // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
+        // with signature algorithm OID.
+        String suffix;
+        if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
+            suffix = "RSA";
+        } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
+            suffix = "DSA";
+        } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
+            suffix = "ECDSA";
+        } else {
+            throw new SignatureException(
+                    "Unsupported JCA Signature algorithm"
+                            + " . Digest algorithm: " + digestAlgorithmOid
+                            + ", signature algorithm: " + signatureAlgorithmOid);
+        }
+        String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
+        // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
+        // SHA algorithms.
+        if (jcaDigestAlg.startsWith("SHA-")) {
+            jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
+        }
+        return jcaDigestAlg + "with" + suffix;
+    }
+
+    public static String getJcaDigestAlgorithm(String oid)
+            throws SignatureException {
+        String result = OID_TO_JCA_DIGEST_ALG.get(oid);
+        if (result == null) {
+            throw new SignatureException("Unsupported digest algorithm: " + oid);
+        }
+        return result;
+    }
 }
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 615d251..4ef67c7 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -47,4 +47,7 @@
 
     /** Android P. */
     public static final int P = 28;
+
+    /** Android R. */
+    public static final int R = 30;
 }
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 75497a7..81026ba 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -16,6 +16,8 @@
 
 package com.android.apksig.internal.util;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
 import com.android.apksig.internal.zip.ZipUtils;
 import com.android.apksig.util.DataSink;
 import com.android.apksig.util.DataSource;
@@ -24,31 +26,71 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.util.ArrayList;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
 /**
  * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
  * The root hash can be used on device for on-access verification.  The tree itself is reproducible
  * on device, and is not shipped with the APK.
  */
-public class VerityTreeBuilder {
+public class VerityTreeBuilder implements AutoCloseable {
 
-    /** Maximum size (in bytes) of each node of the tree. */
+    /**
+     * Maximum size (in bytes) of each node of the tree.
+     */
     private final static int CHUNK_SIZE = 4096;
+    /**
+     * Maximum parallelism while calculating digests.
+     */
+    private final static int DIGEST_PARALLELISM = Math.min(32,
+            Runtime.getRuntime().availableProcessors());
+    /**
+     * Queue size.
+     */
+    private final static int MAX_OUTSTANDING_CHUNKS = 4;
+    /**
+     * Typical prefetch size.
+     */
+    private final static int MAX_PREFETCH_CHUNKS = 1024;
+    /**
+     * Minimum chunks to be processed by a single worker task.
+     */
+    private final static int MIN_CHUNKS_PER_WORKER = 8;
 
-    /** Digest algorithm (JCA Digest algorithm name) used in the tree. */
+    /**
+     * Digest algorithm (JCA Digest algorithm name) used in the tree.
+     */
     private final static String JCA_ALGORITHM = "SHA-256";
 
-    /** Optional salt to apply before each digestion. */
+    /**
+     * Optional salt to apply before each digestion.
+     */
     private final byte[] mSalt;
 
     private final MessageDigest mMd;
 
+    private final ExecutorService mExecutor =
+            new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
+                    0L, MILLISECONDS,
+                    new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+
     public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
         mSalt = salt;
-        mMd = MessageDigest.getInstance(JCA_ALGORITHM);
+        mMd = getNewMessageDigest();
+    }
+
+    @Override
+    public void close() {
+        mExecutor.shutdownNow();
     }
 
     /**
@@ -81,6 +123,14 @@
 
     /**
      * Returns the root hash of the verity tree built from the data source.
+     */
+    public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+        ByteBuffer verityBuffer = generateVerityTree(fileSource);
+        return getRootHashFromTree(verityBuffer);
+    }
+
+    /**
+     * Returns the byte buffer that contains the whole verity tree.
      *
      * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
      * input file.  If the total size is larger than 4 KB, take this level as input and repeat the
@@ -91,10 +141,8 @@
      *
      * The tree is currently stored only in memory and is never written out.  Nevertheless, it is
      * the actual verity tree format on disk, and is supposed to be re-generated on device.
-     *
-     * This is package-private for testing purpose.
      */
-    byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+    public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException {
         int digestSize = mMd.getDigestLength();
 
         // Calculate the summed area table of level size. In other word, this is the offset
@@ -113,7 +161,7 @@
                 digestDataByChunks(src, middleBufferSink);
             } else {
                 src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
-                            levelOffset[i + 1], levelOffset[i + 2]));
+                        levelOffset[i + 1], levelOffset[i + 2]));
                 digestDataByChunks(src, middleBufferSink);
             }
 
@@ -125,8 +173,13 @@
                 middleBufferSink.consume(padding, 0, padding.length);
             }
         }
+        return verityBuffer;
+    }
 
-        // Finally, calculate the root hash from the top level (only page).
+    /**
+     * Returns the digested root hash from the top level (only page) of a verity tree.
+     */
+    public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException {
         ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE);
         return saltedDigest(firstPage);
     }
@@ -167,37 +220,70 @@
      * chunk before digesting.
      */
     private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
-        long size = dataSource.size();
-        long offset = 0;
-        for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
-            ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE);
-            dataSource.copyTo(offset, CHUNK_SIZE, buffer);
+        final long size = dataSource.size();
+        final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
+
+        /** Single IO operation size, in chunks. */
+        final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
+
+        final byte[][] hashes = new byte[chunks][];
+
+        Phaser tasks = new Phaser(1);
+
+        // Reading the input file as fast as we can.
+        final long maxReadSize = ioSizeChunks * CHUNK_SIZE;
+
+        long readOffset = 0;
+        int startChunkIndex = 0;
+        while (readOffset < size) {
+            final long readLimit = Math.min(readOffset + maxReadSize, size);
+            final int readSize = (int) (readLimit - readOffset);
+            final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE);
+
+            // Overllocating to zero-pad last chunk.
+            // With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB.
+            final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE);
+            dataSource.copyTo(readOffset, readSize, buffer);
             buffer.rewind();
-            byte[] hash = saltedDigest(buffer);
-            dataSink.consume(hash, 0, hash.length);
+
+            final int readChunkIndex = startChunkIndex;
+            Runnable task = () -> {
+                final MessageDigest md = cloneMessageDigest();
+                for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex;
+                        offset < finish; offset += CHUNK_SIZE, ++chunkIndex) {
+                    ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE);
+                    hashes[chunkIndex] = saltedDigest(md, chunk);
+                }
+                tasks.arriveAndDeregister();
+            };
+            tasks.register();
+            mExecutor.execute(task);
+
+            startChunkIndex += bufferSizeChunks;
+            readOffset += readSize;
         }
 
-        // Send the last incomplete chunk with 0 padding to the sink at once.
-        int remaining = (int) (size % CHUNK_SIZE);
-        if (remaining > 0) {
-            ByteBuffer buffer;
-            buffer = ByteBuffer.allocate(CHUNK_SIZE);  // initialized to 0.
-            dataSource.copyTo(offset, remaining, buffer);
-            buffer.rewind();
+        // Waiting for the tasks to complete.
+        tasks.arriveAndAwaitAdvance();
 
-            byte[] hash = saltedDigest(buffer);
+        // Streaming hashes back.
+        for (byte[] hash : hashes) {
             dataSink.consume(hash, 0, hash.length);
         }
     }
 
-    /** Returns the digest of data with salt prepanded. */
+    /** Returns the digest of data with salt prepended. */
     private byte[] saltedDigest(ByteBuffer data) {
-        mMd.reset();
+        return saltedDigest(mMd, data);
+    }
+
+    private byte[] saltedDigest(MessageDigest md, ByteBuffer data) {
+        md.reset();
         if (mSalt != null) {
-            mMd.update(mSalt);
+            md.update(mSalt);
         }
-        mMd.update(data);
-        return mMd.digest();
+        md.update(data);
+        return md.digest();
     }
 
     /** Divides a number and round up to the closest integer. */
@@ -213,4 +299,27 @@
         b.position(begin);
         return b.slice();
     }
+
+    /**
+     * Obtains a new instance of the message digest algorithm.
+     */
+    private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(JCA_ALGORITHM);
+    }
+
+    /**
+     * Clones the existing message digest, or creates a new instance if clone is unavailable.
+     */
+    private MessageDigest cloneMessageDigest() {
+        try {
+            return (MessageDigest) mMd.clone();
+        } catch (CloneNotSupportedException ignored) {
+            try {
+                return getNewMessageDigest();
+            } catch (NoSuchAlgorithmException e) {
+                throw new IllegalStateException(
+                        "Failed to obtain an instance of a previously available message digest", e);
+            }
+        }
+    }
 }
diff --git a/src/main/java/com/android/apksig/internal/x509/Certificate.java b/src/main/java/com/android/apksig/internal/x509/Certificate.java
index abb3c15..70ff6a1 100644
--- a/src/main/java/com/android/apksig/internal/x509/Certificate.java
+++ b/src/main/java/com/android/apksig/internal/x509/Certificate.java
@@ -18,10 +18,25 @@
 
 import com.android.apksig.internal.asn1.Asn1Class;
 import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
 import com.android.apksig.internal.asn1.Asn1Type;
 import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
 
+import java.math.BigInteger;
 import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
 
 /**
  * X509 {@code Certificate} as specified in RFC 5280.
@@ -36,4 +51,55 @@
 
     @Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
     public ByteBuffer signature;
+
+    public static X509Certificate findCertificate(
+            Collection<X509Certificate> certs, SignerIdentifier id) {
+        for (X509Certificate cert : certs) {
+            if (isMatchingCerticicate(cert, id)) {
+                return cert;
+            }
+        }
+        return null;
+    }
+
+    private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) {
+        if (id.issuerAndSerialNumber == null) {
+            // Android doesn't support any other means of identifying the signing certificate
+            return false;
+        }
+        IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber;
+        byte[] encodedIssuer =
+                ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded());
+        X500Principal idIssuer = new X500Principal(encodedIssuer);
+        BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber;
+        return idSerialNumber.equals(cert.getSerialNumber())
+                && idIssuer.equals(cert.getIssuerX500Principal());
+    }
+
+    public static List<X509Certificate> parseCertificates(
+            List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
+        if (encodedCertificates.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
+        for (int i = 0; i < encodedCertificates.size(); i++) {
+            Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
+            X509Certificate certificate;
+            byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
+            try {
+                certificate = X509CertificateUtils.generateCertificate(encodedForm);
+            } catch (CertificateException e) {
+                throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
+            }
+            // 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 and/or some implementations of
+            // X509Certificate.getEncoded() re-encode certificates.
+            certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
+            result.add(certificate);
+        }
+        return result;
+    }
 }
diff --git a/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
index 4215810..74017f8 100644
--- a/src/main/java/com/android/apksig/util/RunnablesExecutor.java
+++ b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
@@ -16,8 +16,46 @@
 
 package com.android.apksig.util;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+
 public interface RunnablesExecutor {
-    RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+    static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+
+    static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
+        private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors());
+        private final int QUEUE_SIZE = 4;
+
+        @Override
+        public void execute(RunnablesProvider provider) {
+            final ExecutorService mExecutor =
+                    new ThreadPoolExecutor(PARALLELISM, PARALLELISM,
+                            0L, MILLISECONDS,
+                            new ArrayBlockingQueue<>(QUEUE_SIZE),
+                            new ThreadPoolExecutor.CallerRunsPolicy());
+
+            Phaser tasks = new Phaser(1);
+
+            for (int i = 0; i < PARALLELISM; ++i) {
+                Runnable task = () -> {
+                    Runnable r = provider.createRunnable();
+                    r.run();
+                    tasks.arriveAndDeregister();
+                };
+                tasks.register();
+                mExecutor.execute(task);
+            }
+
+            // Waiting for the tasks to complete.
+            tasks.arriveAndAwaitAdvance();
+
+            mExecutor.shutdownNow();
+        }
+    };
 
     void execute(RunnablesProvider provider);
 }
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 65d9149..560202c 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -16,6 +16,13 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.findZipSections;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -24,23 +31,35 @@
 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.v1.V1SchemeVerifier;
 import com.android.apksig.internal.apk.v2.V2SchemeSigner;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.asn1.Asn1BerParser;
-import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.Resources;
 import com.android.apksig.internal.x509.RSAPublicKey;
 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.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
 import java.io.File;
 import java.io.IOException;
+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;
 import java.security.SignatureException;
@@ -48,11 +67,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.math.BigInteger;
 
 @RunWith(JUnit4.class)
 public class ApkSignerTest {
@@ -83,19 +97,25 @@
 
     private static void generateGoldenFiles(File outDir) throws Exception {
         System.out.println(
-                "Generating golden files " + ApkSignerTest.class.getSimpleName()
-                    + " into " + outDir);
-        if (!outDir.mkdirs()) {
-            throw new IOException("Failed to create directory: " + outDir);
+                "Generating golden files "
+                        + ApkSignerTest.class.getSimpleName()
+                        + " into "
+                        + outDir);
+        if (!outDir.exists()) {
+            if (!outDir.mkdirs()) {
+                throw new IOException("Failed to create directory: " + outDir);
+            }
         }
         List<ApkSigner.SignerConfig> rsa2048SignerConfig =
                 Collections.singletonList(
                         getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
-        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = Arrays.asList(
-                rsa2048SignerConfig.get(0),
-                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
-        SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(
-                ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        rsa2048SignerConfig.get(0),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
 
         signGolden(
                 "golden-unaligned-in.apk",
@@ -316,30 +336,38 @@
                         .setSigningCertificateLineage(lineage));
 
         signGolden(
-                "original.apk", new File(outDir, "golden-rsa-out.apk"),
+                "original.apk",
+                new File(outDir, "golden-rsa-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig));
         signGolden(
-                "original.apk", new File(outDir, "golden-rsa-minSdkVersion-1-out.apk"),
+                "original.apk",
+                new File(outDir, "golden-rsa-minSdkVersion-1-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(1));
         signGolden(
-                "original.apk", new File(outDir, "golden-rsa-minSdkVersion-18-out.apk"),
+                "original.apk",
+                new File(outDir, "golden-rsa-minSdkVersion-18-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(18));
         signGolden(
-                "original.apk", new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"),
+                "original.apk",
+                new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"),
                 new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(24));
+        signGolden(
+                "original.apk",
+                new File(outDir, "golden-rsa-verity-out.apk"),
+                new ApkSigner.Builder(rsa2048SignerConfig)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setVerityEnabled(true));
     }
 
     private static void signGolden(
             String inResourceName, File outFile, ApkSigner.Builder apkSignerBuilder)
-                    throws Exception {
+            throws Exception {
         DataSource in =
                 DataSources.asDataSource(
                         ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName)));
-        apkSignerBuilder
-                .setInputApk(in)
-                .setOutputApk(outFile)
-                .build()
-                .sign();
+        apkSignerBuilder.setInputApk(in).setOutputApk(outFile).build().sign();
     }
 
     @Test
@@ -350,70 +378,82 @@
         List<ApkSigner.SignerConfig> rsa2048SignerConfig =
                 Collections.singletonList(
                         getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
-        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = Arrays.asList(
-                rsa2048SignerConfig.get(0),
-                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
-        SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
-                LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        rsa2048SignerConfig.get(0),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
         // Uncompressed entries in this input file are not aligned -- the file was created using
         // the jar utility. temp4.txt entry was then manually added into the archive. This entry's
         // ZIP Local File Header "extra" field declares that the entry's data must be aligned to
         // 4 kB boundary, but the data isn't actually aligned in the file.
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v1-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v1-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v2-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v3-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v3-lineage-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v1v2-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v1v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v2v3-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v2v3-lineage-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v1v2v3-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v1v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-unaligned-in.apk", "golden-unaligned-v1v2v3-lineage-out.apk",
+                "golden-unaligned-in.apk",
+                "golden-unaligned-v1v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
@@ -425,60 +465,70 @@
         // archives whose "extra" field are not compliant with APPNOTE.TXT. Hence, this technique
         // was deprecated.
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v1-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v3-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v3-lineage-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v1v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2v3-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2v3-lineage-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2v3-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v1v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2v3-lineage-out.apk",
+                "golden-legacy-aligned-in.apk",
+                "golden-legacy-aligned-v1v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
@@ -489,60 +539,70 @@
         // generated by signapk and apksigner. This padding technique produces "extra" fields which
         // are compliant with APPNOTE.TXT.
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v1-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v1-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v2-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v3-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v3-lineage-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v1v2-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v1v2-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(false));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v2v3-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v2v3-lineage-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v1v2v3-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v1v2v3-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfig)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true));
         assertGolden(
-                "golden-aligned-in.apk", "golden-aligned-v1v2v3-lineage-out.apk",
+                "golden-aligned-in.apk",
+                "golden-aligned-v1v2v3-lineage-out.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
@@ -555,17 +615,21 @@
         // Regression tests for minSdkVersion-based signature/digest algorithm selection
         // NOTE: Expected output files can be re-generated by running the "main" method.
 
-        List<ApkSigner.SignerConfig> rsaSignerConfig = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        List<ApkSigner.SignerConfig> rsaSignerConfig =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
         assertGolden("original.apk", "golden-rsa-out.apk", new ApkSigner.Builder(rsaSignerConfig));
         assertGolden(
-                "original.apk", "golden-rsa-minSdkVersion-1-out.apk",
+                "original.apk",
+                "golden-rsa-minSdkVersion-1-out.apk",
                 new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(1));
         assertGolden(
-                "original.apk", "golden-rsa-minSdkVersion-18-out.apk",
+                "original.apk",
+                "golden-rsa-minSdkVersion-18-out.apk",
                 new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(18));
         assertGolden(
-                "original.apk", "golden-rsa-minSdkVersion-24-out.apk",
+                "original.apk",
+                "golden-rsa-minSdkVersion-24-out.apk",
                 new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(24));
 
         // TODO: Add tests for DSA and ECDSA. This is non-trivial because the default
@@ -574,9 +638,26 @@
     }
 
     @Test
+    public void testVerityEnabled_Golden() throws Exception {
+        List<ApkSigner.SignerConfig> rsaSignerConfig =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertGolden(
+                "original.apk",
+                "golden-rsa-verity-out.apk",
+                new ApkSigner.Builder(rsaSignerConfig)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setVerityEnabled(true));
+    }
+
+    @Test
     public void testRsaSignedVerifies() throws Exception {
-        List<ApkSigner.SignerConfig> signers = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
         String in = "original.apk";
 
         // Sign so that the APK is guaranteed to verify on API Level 1+
@@ -628,25 +709,28 @@
     public void testV1SigningRejectsInvalidZipEntryNames() throws Exception {
         // ZIP/JAR entry name cannot contain CR, LF, or NUL characters when the APK is being
         // JAR-signed.
-        List<ApkSigner.SignerConfig> signers = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
-        try {
-            sign("v1-only-with-cr-in-entry-name.apk",
-                    new ApkSigner.Builder(signers).setV1SigningEnabled(true));
-            fail();
-        } catch (ApkFormatException expected) {}
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
 
-        try {
-            sign("v1-only-with-lf-in-entry-name.apk",
-                    new ApkSigner.Builder(signers).setV1SigningEnabled(true));
-            fail();
-        } catch (ApkFormatException expected) {}
-
-        try {
-            sign("v1-only-with-nul-in-entry-name.apk",
-                    new ApkSigner.Builder(signers).setV1SigningEnabled(true));
-            fail();
-        } catch (ApkFormatException expected) {}
+        assertThrows(
+                ApkFormatException.class,
+                () ->
+                        sign(
+                                "v1-only-with-cr-in-entry-name.apk",
+                                new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
+        assertThrows(
+                ApkFormatException.class,
+                () ->
+                        sign(
+                                "v1-only-with-lf-in-entry-name.apk",
+                                new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
+        assertThrows(
+                ApkFormatException.class,
+                () ->
+                        sign(
+                                "v1-only-with-nul-in-entry-name.apk",
+                                new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
     }
 
     @Test
@@ -654,8 +738,9 @@
         // Any ZIP compression method other than STORED is treated as DEFLATED by Android.
         // This APK declares compression method 21 (neither STORED nor DEFLATED) for CERT.RSA entry,
         // but the entry is actually Deflate-compressed.
-        List<ApkSigner.SignerConfig> signers = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
         sign("weird-compression-method.apk", new ApkSigner.Builder(signers));
     }
 
@@ -665,53 +750,67 @@
         // uses the compressionMethod from Central Directory instead.
         // In this APK, compression method of CERT.RSA is declared as STORED in Local File Header
         // and as DEFLATED in Central Directory. The entry is actually Deflate-compressed.
-        List<ApkSigner.SignerConfig> signers = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
         sign("mismatched-compression-method.apk", new ApkSigner.Builder(signers));
     }
 
     @Test
     public void testDebuggableApk() throws Exception {
         // APK which uses a boolean value "true" in its android:debuggable
-        String apk = "debuggable-boolean.apk";
-        List<ApkSigner.SignerConfig> signers = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        final String debuggableBooleanApk = "debuggable-boolean.apk";
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
         // Signing debuggable APKs is permitted by default
-        sign(apk, new ApkSigner.Builder(signers));
+        sign(debuggableBooleanApk, new ApkSigner.Builder(signers));
         // Signing debuggable APK succeeds when explicitly requested
-        sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+        sign(debuggableBooleanApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+
         // Signing debuggable APK fails when requested
-        try {
-            sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false));
-            fail();
-        } catch (SignatureException expected) {}
+        assertThrows(
+                SignatureException.class,
+                () ->
+                        sign(
+                                debuggableBooleanApk,
+                                new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)));
 
         // APK which uses a reference value, pointing to boolean "false", in its android:debuggable
-        apk = "debuggable-resource.apk";
+        final String debuggableResourceApk = "debuggable-resource.apk";
         // When we permit signing regardless of whether the APK is debuggable, the value of
         // android:debuggable should be ignored.
-        sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+        sign(debuggableResourceApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
 
         // When we disallow signing debuggable APKs, APKs with android:debuggable being a resource
         // reference must be rejected, because there's no easy way to establish whether the resolved
         // boolean value is the same for all resource configurations.
-        try {
-            sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false));
-            fail();
-        } catch (SignatureException expected) {}
+        assertThrows(
+                SignatureException.class,
+                () ->
+                        sign(
+                                debuggableResourceApk,
+                                new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)));
     }
 
-    @Test(expected = IllegalStateException.class)
+    @Test
     public void testV3SigningWithSignersNotInLineageFails() throws Exception {
         // APKs signed with the v3 scheme after a key rotation must specify the lineage containing
         // the proof of rotation. This test verifies that the signing will fail if the provided
         // signers are not in the specified lineage.
-        List<ApkSigner.SignerConfig> signers = Arrays.asList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
-                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
-        SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
-                "rsa-1024-lineage-2-signers");
-        sign("original.apk", new ApkSigner.Builder(signers).setSigningCertificateLineage(lineage));
+        List<ApkSigner.SignerConfig> signers =
+                Arrays.asList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(getClass(), "rsa-1024-lineage-2-signers");
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        sign(
+                                "original.apk",
+                                new ApkSigner.Builder(signers)
+                                        .setSigningCertificateLineage(lineage)));
     }
 
     @Test
@@ -719,104 +818,131 @@
         // After a key rotation the oldest signer must still be specified for v1 and v2 signing.
         // The lineage contains the proof of rotation and will be used to determine the oldest
         // signer.
-        ApkSigner.SignerConfig firstSigner = getDefaultSignerConfigFromResources(
-                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
-        ApkSigner.SignerConfig secondSigner = getDefaultSignerConfigFromResources(
-                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
-        ApkSigner.SignerConfig thirdSigner = getDefaultSignerConfigFromResources(
-                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
-        SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
-                "rsa-2048-lineage-3-signers");
+        ApkSigner.SignerConfig firstSigner =
+                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig secondSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig thirdSigner =
+                getDefaultSignerConfigFromResources(THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(getClass(), "rsa-2048-lineage-3-signers");
 
         // Verifies that the v1 signing scheme requires the oldest signer after a key rotation.
         List<ApkSigner.SignerConfig> signers = Collections.singletonList(thirdSigner);
         try {
-            sign("original.apk", new ApkSigner.Builder(signers)
-                    .setV1SigningEnabled(true)
-                    .setV2SigningEnabled(false)
-                    .setV3SigningEnabled(true)
-                    .setSigningCertificateLineage(lineage));
-            fail("The signing should have failed due to the oldest signer in the lineage not being"
-                    + " provided for v1 signing");
-        } catch (IllegalArgumentException expected) {}
+            sign(
+                    "original.apk",
+                    new ApkSigner.Builder(signers)
+                            .setV1SigningEnabled(true)
+                            .setV2SigningEnabled(false)
+                            .setV3SigningEnabled(true)
+                            .setSigningCertificateLineage(lineage));
+            fail(
+                    "The signing should have failed due to the oldest signer in the lineage not"
+                            + " being provided for v1 signing");
+        } catch (IllegalArgumentException expected) {
+        }
 
         // Verifies that the v2 signing scheme requires the oldest signer after a key rotation.
         try {
-            sign("original.apk", new ApkSigner.Builder(signers)
-                    .setV1SigningEnabled(false)
-                    .setV2SigningEnabled(true)
-                    .setV3SigningEnabled(true)
-                    .setSigningCertificateLineage(lineage));
-            fail("The signing should have failed due to the oldest signer in the lineage not being"
-                    + " provided for v2 signing");
-        } catch (IllegalArgumentException expected) {}
+            sign(
+                    "original.apk",
+                    new ApkSigner.Builder(signers)
+                            .setV1SigningEnabled(false)
+                            .setV2SigningEnabled(true)
+                            .setV3SigningEnabled(true)
+                            .setSigningCertificateLineage(lineage));
+            fail(
+                    "The signing should have failed due to the oldest signer in the lineage not"
+                            + " being provided for v2 signing");
+        } catch (IllegalArgumentException expected) {
+        }
 
         // Verifies that when only the v3 signing scheme is requested the oldest signer does not
         // need to be provided.
-        sign("original.apk", new ApkSigner.Builder(signers)
-                .setV1SigningEnabled(false)
-                .setV2SigningEnabled(false)
-                .setV3SigningEnabled(true)
-                .setSigningCertificateLineage(lineage));
+        sign(
+                "original.apk",
+                new ApkSigner.Builder(signers)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setSigningCertificateLineage(lineage));
 
         // Verifies that an intermediate signer in the lineage is not sufficient to satisfy the
         // requirement that the oldest signer be provided for v1 and v2 signing.
         signers = Arrays.asList(secondSigner, thirdSigner);
         try {
-            sign("original.apk", new ApkSigner.Builder(signers)
-                    .setV1SigningEnabled(true)
-                    .setV2SigningEnabled(true)
-                    .setV3SigningEnabled(true)
-                    .setSigningCertificateLineage(lineage));
-            fail("The signing should have failed due to the oldest signer in the lineage not being"
-                    + " provided for v1/v2 signing");
-        } catch (IllegalArgumentException expected) {}
+            sign(
+                    "original.apk",
+                    new ApkSigner.Builder(signers)
+                            .setV1SigningEnabled(true)
+                            .setV2SigningEnabled(true)
+                            .setV3SigningEnabled(true)
+                            .setSigningCertificateLineage(lineage));
+            fail(
+                    "The signing should have failed due to the oldest signer in the lineage not"
+                            + " being provided for v1/v2 signing");
+        } catch (IllegalArgumentException expected) {
+        }
 
         // Verifies that the signing is successful when the oldest and newest signers are provided
         // and that intermediate signers are not required.
         signers = Arrays.asList(firstSigner, thirdSigner);
-        sign("original.apk", new ApkSigner.Builder(signers)
-                .setV1SigningEnabled(true)
-                .setV2SigningEnabled(true)
-                .setV3SigningEnabled(true)
-                .setSigningCertificateLineage(lineage));
+        sign(
+                "original.apk",
+                new ApkSigner.Builder(signers)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setSigningCertificateLineage(lineage));
     }
 
-    @Test(expected = IllegalStateException.class)
+    @Test
     public void testV3SigningWithMultipleSignersAndNoLineageFails() throws Exception {
         // The v3 signing scheme does not support multiple signers; if multiple signers are provided
         // it is assumed these signers are part of the lineage. This test verifies v3 signing
         // fails if multiple signers are provided without a lineage.
-        ApkSigner.SignerConfig firstSigner = getDefaultSignerConfigFromResources(
-                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
-        ApkSigner.SignerConfig secondSigner = getDefaultSignerConfigFromResources(
-                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig firstSigner =
+                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig secondSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
         List<ApkSigner.SignerConfig> signers = Arrays.asList(firstSigner, secondSigner);
-        sign("original.apk", new ApkSigner.Builder(signers)
-                .setV1SigningEnabled(true)
-                .setV2SigningEnabled(true)
-                .setV3SigningEnabled(true));
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        sign(
+                                "original.apk",
+                                new ApkSigner.Builder(signers)
+                                        .setV1SigningEnabled(true)
+                                        .setV2SigningEnabled(true)
+                                        .setV3SigningEnabled(true)));
     }
 
     @Test
     public void testLineageCanBeReadAfterV3Signing() throws Exception {
-        SigningCertificateLineage.SignerConfig firstSigner = Resources.toLineageSignerConfig(
-                getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
-        SigningCertificateLineage.SignerConfig secondSigner = Resources.toLineageSignerConfig(
-                getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
-        SigningCertificateLineage lineage = new SigningCertificateLineage.Builder(firstSigner,
-                secondSigner).build();
-        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
-                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
-        DataSource out = sign("original.apk", new ApkSigner.Builder(signerConfigs)
-                .setV3SigningEnabled(true)
-                .setSigningCertificateLineage(lineage));
-        SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
-                out);
-        assertTrue("The first signer was not in the lineage from the signed APK",
+        SigningCertificateLineage.SignerConfig firstSigner =
+                Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage.SignerConfig secondSigner =
+                Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage =
+                new SigningCertificateLineage.Builder(firstSigner, secondSigner).build();
+        List<ApkSigner.SignerConfig> signerConfigs =
+                Arrays.asList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        DataSource out =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signerConfigs)
+                                .setV3SigningEnabled(true)
+                                .setSigningCertificateLineage(lineage));
+        SigningCertificateLineage lineageFromApk =
+                SigningCertificateLineage.readFromApkDataSource(out);
+        assertTrue(
+                "The first signer was not in the lineage from the signed APK",
                 lineageFromApk.isSignerInLineage((firstSigner)));
-        assertTrue("The second signer was not in the lineage from the signed APK",
+        assertTrue(
+                "The second signer was not in the lineage from the signed APK",
                 lineageFromApk.isSignerInLineage((secondSigner)));
     }
 
@@ -829,23 +955,251 @@
         // the correct encoding for the modulus. This test uses an improperly encoded certificate to
         // sign an APK and verifies that the public key in the signing block is corrected with a
         // positive modulus to allow on device installs / updates.
-        List<ApkSigner.SignerConfig> signersList = Collections.singletonList(
-                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
-                        FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
-        DataSource signedApk = sign("original.apk", new ApkSigner.Builder(signersList)
-                .setV1SigningEnabled(true)
-                .setV2SigningEnabled(true)
-                .setV3SigningEnabled(true));
-        RSAPublicKey v2PublicKey = getRSAPublicKeyFromSigningBlock(signedApk,
-                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
-        assertTrue("The modulus in the public key in the V2 signing block must not be negative",
+        List<ApkSigner.SignerConfig> signersList =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(
+                                FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                                FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
+        DataSource signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(true)
+                                .setV3SigningEnabled(true));
+        RSAPublicKey v2PublicKey =
+                getRSAPublicKeyFromSigningBlock(
+                        signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+        assertTrue(
+                "The modulus in the public key in the V2 signing block must not be negative",
                 v2PublicKey.modulus.compareTo(BigInteger.ZERO) > 0);
-        RSAPublicKey v3PublicKey = getRSAPublicKeyFromSigningBlock(signedApk,
-                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
-        assertTrue("The modulus in the public key in the V3 signing block must not be negative",
+        RSAPublicKey v3PublicKey =
+                getRSAPublicKeyFromSigningBlock(
+                        signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+        assertTrue(
+                "The modulus in the public key in the V3 signing block must not be negative",
                 v3PublicKey.modulus.compareTo(BigInteger.ZERO) > 0);
     }
 
+    @Test
+    public void testV4State_disableV2V3EnableV4_fails() throws Exception {
+        ApkSigner.SignerConfig signer =
+                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        sign(
+                                "original.apk",
+                                new ApkSigner.Builder(Collections.singletonList(signer))
+                                        .setV1SigningEnabled(true)
+                                        .setV2SigningEnabled(false)
+                                        .setV3SigningEnabled(false)
+                                        .setV4SigningEnabled(true)));
+    }
+
+    @Test
+    public void testSignApk_stampFile() throws Exception {
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        ApkSigner.SignerConfig sourceStampSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+        messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded());
+        byte[] expectedStampCertificateDigest = messageDigest.digest();
+
+        DataSource signedApk =
+                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;
+            }
+        }
+        assertNotNull(stampCdRecord);
+        byte[] actualStampCertificateDigest =
+                LocalFileRecord.getUncompressedData(
+                        signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
+        assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
+    }
+
+    @Test
+    public void testSignApk_existingStampFile_sameSourceStamp() throws Exception {
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        ApkSigner.SignerConfig sourceStampSigner =
+                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        DataSource signedApk =
+                sign(
+                        "original-with-stamp-file.apk",
+                        new ApkSigner.Builder(signers)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(true)
+                                .setV3SigningEnabled(true)
+                                .setSourceStampSignerConfig(sourceStampSigner));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verify(signedApk, /* minSdkVersionOverride= */ null);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
+    @Test
+    public void testSignApk_existingStampFile_differentSourceStamp() throws Exception {
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        ApkSigner.SignerConfig sourceStampSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        Exception exception =
+                assertThrows(
+                        ApkFormatException.class,
+                        () ->
+                                sign(
+                                        "original-with-stamp-file.apk",
+                                        new ApkSigner.Builder(signers)
+                                                .setV1SigningEnabled(true)
+                                                .setV2SigningEnabled(true)
+                                                .setV3SigningEnabled(true)
+                                                .setSourceStampSignerConfig(sourceStampSigner)));
+        assertEquals(
+                String.format(
+                        "Cannot generate SourceStamp. APK contains an existing entry with the"
+                                + " name: %s, and it is different than the provided source stamp"
+                                + " certificate",
+                        SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME),
+                exception.getMessage());
+    }
+
+    @Test
+    public void testSignApk_existingStampFile_differentSourceStamp_forceOverwrite()
+            throws Exception {
+        List<ApkSigner.SignerConfig> signers =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        ApkSigner.SignerConfig sourceStampSigner =
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        DataSource signedApk =
+                sign(
+                        "original-with-stamp-file.apk",
+                        new ApkSigner.Builder(signers)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(true)
+                                .setV3SigningEnabled(true)
+                                .setForceSourceStampOverwrite(true)
+                                .setSourceStampSignerConfig(sourceStampSigner));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verify(signedApk, /* minSdkVersionOverride= */ null);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
+    @Test
+    public void testSignApk_stampBlock_noStampGenerated() throws Exception {
+        List<ApkSigner.SignerConfig> signersList =
+                Collections.singletonList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        DataSource signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(true)
+                                .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));
+    }
+
+    @Test
+    public void testSignApk_stampBlock_whenV1SignaturePresent() 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);
+
+        DataSource signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(true)
+                                .setV2SigningEnabled(false)
+                                .setV3SigningEnabled(false)
+                                .setSourceStampSignerConfig(sourceStampSigner));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verify(signedApk, /* minSdkVersionOverride= */ null);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
+    @Test
+    public void testSignApk_stampBlock_whenV2SignaturePresent() 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);
+
+        DataSource signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(false)
+                                .setV2SigningEnabled(true)
+                                .setV3SigningEnabled(false)
+                                .setSourceStampSignerConfig(sourceStampSigner));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
+    @Test
+    public void testSignApk_stampBlock_whenV3SignaturePresent() 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);
+
+        DataSource signedApk =
+                sign(
+                        "original.apk",
+                        new ApkSigner.Builder(signersList)
+                                .setV1SigningEnabled(false)
+                                .setV2SigningEnabled(false)
+                                .setV3SigningEnabled(true)
+                                .setSourceStampSignerConfig(sourceStampSigner));
+
+        ApkVerifier.Result sourceStampVerificationResult =
+                verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+        assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+    }
+
     private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId)
             throws Exception {
         int signatureVersionBlockId;
@@ -860,11 +1214,8 @@
                 throw new Exception(
                         "Invalid signature version ID specified: " + signatureVersionId);
         }
-        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
-        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
-                signatureVersionId);
-        SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
-                signatureVersionBlockId, result);
+        SignatureInfo signatureInfo =
+                getSignatureInfoFromApk(apk, signatureVersionId, signatureVersionBlockId);
         // FORMAT:
         // * length prefixed sequence of length prefixed signers
         //   * length-prefixed signed data
@@ -872,8 +1223,8 @@
         //   * V3+ only - maxSDK (uint32)
         //   * length-prefixed sequence of length-prefixed signatures:
         //   * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
-        ByteBuffer signers = ApkSigningBlockUtils.getLengthPrefixedSlice(
-                signatureInfo.signatureBlock);
+        ByteBuffer signers =
+                ApkSigningBlockUtils.getLengthPrefixedSlice(signatureInfo.signatureBlock);
         ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
         // Since all the data is read from the signer block the signedData and signatures are
         // discarded.
@@ -885,25 +1236,35 @@
         }
         ApkSigningBlockUtils.getLengthPrefixedSlice(signer);
         ByteBuffer publicKey = ApkSigningBlockUtils.getLengthPrefixedSlice(signer);
-        SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse(publicKey,
-                SubjectPublicKeyInfo.class);
+        SubjectPublicKeyInfo subjectPublicKeyInfo =
+                Asn1BerParser.parse(publicKey, SubjectPublicKeyInfo.class);
         ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey;
         // The SubjectPublicKey is stored as a bit string in the SubjectPublicKeyInfo with the first
         // byte indicating the number of padding bits in the public key. Read this first byte to
         // allow parsing the rest of the RSAPublicKey as a sequence.
         subjectPublicKeyBuffer.get();
-        RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer,
-                RSAPublicKey.class);
-        return rsaPublicKey;
+        return Asn1BerParser.parse(subjectPublicKeyBuffer, RSAPublicKey.class);
+    }
+
+    private static SignatureInfo getSignatureInfoFromApk(
+            DataSource apk, 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);
     }
 
     /**
-     * Asserts that signing the specified golden input file using the provided signing
-     * configuration produces output identical to the specified golden output file.
+     * Asserts that signing the specified golden input file using the provided signing configuration
+     * produces output identical to the specified golden output file.
      */
     private void assertGolden(
-            String inResourceName, String expectedOutResourceName,
-            ApkSigner.Builder apkSignerBuilder) throws Exception {
+            String inResourceName,
+            String expectedOutResourceName,
+            ApkSigner.Builder apkSignerBuilder)
+            throws Exception {
         // Sign the provided golden input
         DataSource out = sign(inResourceName, apkSignerBuilder);
 
@@ -950,17 +1311,13 @@
         }
     }
 
-    private DataSource sign(
-            String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception {
+    private DataSource 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();
+        apkSignerBuilder.setInputApk(in).setOutputApk(out).build().sign();
         return out;
     }
 
@@ -982,6 +1339,18 @@
         ApkVerifierTest.assertVerified(result);
     }
 
+    private static void assertSourceStampVerified(DataSource signedApk, ApkVerifier.Result result)
+            throws ApkSigningBlockUtils.SignatureNotFoundException, IOException,
+                    ZipFormatException {
+        SignatureInfo signatureInfo =
+                getSignatureInfoFromApk(
+                        signedApk,
+                        ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+                        V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID);
+        assertNotNull(signatureInfo.signatureBlock);
+        assertTrue(result.isSourceStampVerified());
+    }
+
     private static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
         ApkVerifierTest.assertVerificationFailure(result, expectedIssue);
     }
@@ -997,10 +1366,10 @@
 
     private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
             String keyNameInResources, String certNameInResources) throws Exception {
-        PrivateKey privateKey = Resources.toPrivateKey(ApkSignerTest.class,
-                keyNameInResources + ".pk8");
-        List<X509Certificate> certs = Resources.toCertificateChain(ApkSignerTest.class,
-                certNameInResources);
+        PrivateKey privateKey =
+                Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8");
+        List<X509Certificate> certs =
+                Resources.toCertificateChain(ApkSignerTest.class, certNameInResources);
         return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
     }
 }
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 351d0a8..ed154c5 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -17,6 +17,7 @@
 package com.android.apksig;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNoException;
 
@@ -27,6 +28,12 @@
 import com.android.apksig.internal.util.HexEncoding;
 import com.android.apksig.internal.util.Resources;
 import com.android.apksig.util.DataSources;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.security.InvalidKeyException;
@@ -37,12 +44,12 @@
 import java.security.Signature;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 @RunWith(JUnit4.class)
 public class ApkVerifierTest {
@@ -52,8 +59,9 @@
     private static final String[] DSA_KEY_NAMES_2048_AND_LARGER = {"2048", "3072"};
     private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"};
     private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"};
-    private static final String[] RSA_KEY_NAMES_2048_AND_LARGER =
-            {"2048", "3072", "4096", "8192", "16384"};
+    private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = {
+        "2048", "3072", "4096", "8192", "16384"
+    };
 
     @Test
     public void testOriginalAccepted() throws Exception {
@@ -121,46 +129,36 @@
     @Test
     public void testV1OneSignerSHA1withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA224withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA256withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA384withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
     public void testV1OneSignerSHA512withECDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
-        assertVerifiedForEach(
-                "v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
     }
 
     @Test
@@ -233,8 +231,7 @@
     @Test
     public void testV1OneSignerSHA256withDSAAccepted() throws Exception {
         // APK signed with v1 scheme only, one signer
-        assertVerifiedForEach(
-                "v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
+        assertVerifiedForEach("v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
         assertVerifiedForEach(
                 "v1-only-with-dsa-sha256-2.16.840.1.101.3.4.3.2-%s.apk", DSA_KEY_NAMES);
     }
@@ -246,8 +243,7 @@
         // This should fail because the v1 signature indicates that the APK was supposed to be
         // signed with v2 scheme as well, making the platform's anti-stripping protections reject
         // the APK.
-        assertVerificationFailure(
-                "v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
+        assertVerificationFailure("v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
 
         // Similar to above, but the X-Android-APK-Signed anti-stripping header in v1 signature
         // lists unknown signature schemes in addition to APK Signature Scheme v2. Unknown schemes
@@ -262,8 +258,7 @@
         // APK signed with v2 and v3 schemes, but v3 signature was stripped from the file by
         // modifying the v3 block ID to be the verity padding block ID. Without the stripping
         // protection this modification ignores the v3 signing scheme block.
-        assertVerificationFailure(
-                "v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
+        assertVerificationFailure("v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
     }
 
     @Test
@@ -271,10 +266,12 @@
         // The V2 signature scheme was introduced in N, and V3 was introduced in P. This test
         // verifies a max SDK of pre-P ignores the V3 signature and a max SDK of pre-N ignores both
         // the V2 and V3 signatures.
-        assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
-                AndroidSdkVersion.O));
-        assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
-                AndroidSdkVersion.M));
+        assertVerified(
+                verifyForMaxSdkVersion(
+                        "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.O));
+        assertVerified(
+                verifyForMaxSdkVersion(
+                        "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.M));
     }
 
     @Test
@@ -390,13 +387,11 @@
 
         // Based on v2-only-with-rsa-pkcs1-sha512-4096.apk. Obtained by modifying APK signer to
         // flip the leftmost bit in content digest before signing signed-data.
-        assertVerificationFailure(
-                "v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
+        assertVerificationFailure("v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
 
         // Based on v2-only-with-ecdsa-sha256-p256.apk. Obtained by modifying APK signer to flip the
         // leftmost bit in content digest before signing signed-data.
-        assertVerificationFailure(
-                "v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
+        assertVerificationFailure("v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
     }
 
     @Test
@@ -425,21 +420,18 @@
         // Obtained from v2-only-with-rsa-pkcs1-sha512-4096.apk by flipping a bit in the magic
         // field in the footer of APK Signing Block. This makes the APK Signing Block disappear.
         assertVerificationFailure(
-                "v2-only-wrong-apk-sig-block-magic.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-wrong-apk-sig-block-magic.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying APK signer to insert "GARBAGE" between ZIP Central Directory and
         // End of Central Directory. The APK is otherwise fine and is signed with APK Signature
         // Scheme v2. Based on v2-only-with-rsa-pkcs1-sha256.apk.
         assertVerificationFailure(
-                "v2-only-garbage-between-cd-and-eocd.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-garbage-between-cd-and-eocd.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying the size in APK Signature Block header. Based on
         // v2-only-with-ecdsa-sha512-p521.apk.
         assertVerificationFailure(
-                "v2-only-apk-sig-block-size-mismatch.apk",
-                Issue.JAR_SIG_NO_MANIFEST);
+                "v2-only-apk-sig-block-size-mismatch.apk", Issue.JAR_SIG_NO_MANIFEST);
 
         // Obtained by modifying the ID under which APK Signature Scheme v2 Block is stored in
         // APK Signing Block and by modifying the APK signer to not insert anti-stripping
@@ -539,7 +531,8 @@
         // APK's v2 and v3 signatures contain unknown additional attributes before and after the
         // anti-stripping and lineage attributes.
         assertVerified(
-                verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));    }
+                verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));
+    }
 
     @Test
     public void testV2MismatchBetweenSignaturesAndDigestsBlockRejected() throws Exception {
@@ -585,16 +578,14 @@
     public void testV2SignerBlockWithNoCertificatesRejected() throws Exception {
         // APK is signed with v2 only. There are no certificates listed in the signer block.
         // Obtained by modifying APK signer to output no certificates.
-        assertVerificationFailure(
-                "v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
+        assertVerificationFailure("v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
     }
 
     @Test
     public void testV3SignerBlockWithNoCertificatesRejected() throws Exception {
         // APK is signed with v3 only. There are no certificates listed in the signer block.
         // Obtained by modifying APK signer to output no certificates.
-        assertVerificationFailure(
-                "v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
+        assertVerificationFailure("v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
     }
 
     @Test
@@ -739,25 +730,29 @@
         try {
             verifyForMinSdkVersion("empty-unsigned.apk", 1);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // JAR-signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v1-only-empty.apk", 18);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // APK Signature Scheme v2 signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v2-only-empty.apk", AndroidSdkVersion.N);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
 
         // APK Signature Scheme v3 signed empty ZIP archive
         try {
             verifyForMinSdkVersion("v3-only-empty.apk", AndroidSdkVersion.P);
             fail("ApkFormatException should've been thrown");
-        } catch (ApkFormatException expected) {}
+        } catch (ApkFormatException expected) {
+        }
     }
 
     @Test
@@ -786,6 +781,47 @@
     }
 
     @Test
+    public void testTargetSdkMinSchemeVersionNotMet() throws Exception {
+        // Android 11 / SDK version 30 requires apps targeting this SDK version or higher must be
+        // signed with at least the V2 signature scheme. This test verifies if an app is targeting
+        // this SDK version and is only signed with a V1 signature then the verifier reports the
+        // platform will not accept it.
+        assertVerificationFailure(verify("v1-ec-p256-targetSdk-30.apk"),
+                Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET);
+    }
+
+    @Test
+    public void testTargetSdkMinSchemeVersionMet() throws Exception {
+        // This test verifies if an app is signed with the minimum required signature scheme version
+        // for the target SDK version then the verifier reports the platform will accept it.
+        assertVerified(verify("v2-ec-p256-targetSdk-30.apk"));
+
+        // If an app is only signed with a signature scheme higher than the required version for the
+        // target SDK the verifier should also report that the platform will accept it.
+        assertVerified(verify("v3-ec-p256-targetSdk-30.apk"));
+    }
+
+    @Test
+    public void testTargetSdkMinSchemeVersionNotMetMaxLessThanTarget() throws Exception {
+        // If the minimum signature scheme for the target SDK version is not met but the maximum
+        // SDK version is less than the target then the verifier should report that the platform
+        // will accept it since the specified max SDK version does not know about the minimum
+        // signature scheme requirement.
+        verifyForMaxSdkVersion("v1-ec-p256-targetSdk-30.apk", 29);
+    }
+
+    @Test
+    public void testTargetSdkNoUsesSdkElement() throws Exception {
+        // The target SDK minimum signature scheme version check will attempt to obtain the
+        // targetSdkVersion attribute value from the uses-sdk element in the AndroidManifest. If
+        // the targetSdkVersion is not specified then the verifier should behave the same as the
+        // platform; the minSdkVersion should be used when available and when neither the minimum or
+        // target SDK are specified a default value of 1 should be used. This test verifies that the
+        // verifier does not fail when the uses-sdk element is not specified.
+        verify("v1-only-no-uses-sdk.apk");
+    }
+
+    @Test
     public void testV1MultipleDigestAlgsInManifestAndSignatureFile() throws Exception {
         // MANIFEST.MF contains SHA-1 and SHA-256 digests for each entry, .SF contains only SHA-1
         // digests. This file was obtained by:
@@ -954,8 +990,7 @@
                 verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
                 Issue.JAR_SIG_VERIFY_EXCEPTION);
         assertVerificationFailure(
-                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
-                Issue.JAR_SIG_VERIFY_EXCEPTION);
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
         // Assert that this issue fails verification of the entire signature block, rather than
         // skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
         // verification does not get there.
@@ -964,8 +999,7 @@
                 verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
                 Issue.JAR_SIG_VERIFY_EXCEPTION);
         assertVerificationFailure(
-                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
-                Issue.JAR_SIG_VERIFY_EXCEPTION);
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
     }
 
     @Test
@@ -1020,6 +1054,66 @@
         assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
     }
 
+    @Test
+    public void testSourceStampBlock_correctSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verify("valid-stamp.apk");
+        // Verifies the signature of the APK.
+        assertVerified(verificationResult);
+        // Verifies the signature of source stamp.
+        assertTrue(verificationResult.isSourceStampVerified());
+    }
+
+    @Test
+    public void testSourceStampBlock_signatureMissing() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-without-block.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING);
+    }
+
+    @Test
+    public void testSourceStampBlock_certificateMismatch() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(
+                verificationResult,
+                Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+    }
+
+    @Test
+    public void testSourceStampBlock_apkHashMismatch_v1SignatureScheme() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v1.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void testSourceStampBlock_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.
+        assertVerified(verificationResult);
+        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.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void testSourceStampBlock_malformedSignature() throws Exception {
+        ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk");
+        // A broken stamp should not block a signing scheme verified APK.
+        assertVerified(verificationResult);
+        assertSourceStampVerificationFailure(
+                verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+    }
+
     private ApkVerifier.Result verify(String apkFilenameInResources)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, null);
@@ -1027,13 +1121,13 @@
 
     private ApkVerifier.Result verifyForMinSdkVersion(
             String apkFilenameInResources, int minSdkVersion)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, minSdkVersion, null);
     }
 
     private ApkVerifier.Result verifyForMaxSdkVersion(
             String apkFilenameInResources, int maxSdkVersion)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, maxSdkVersion);
     }
 
@@ -1041,7 +1135,7 @@
             String apkFilenameInResources,
             Integer minSdkVersionOverride,
             Integer maxSdkVersionOverride)
-                    throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            throws IOException, ApkFormatException, NoSuchAlgorithmException {
         byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
 
         ApkVerifier.Builder builder =
@@ -1077,8 +1171,12 @@
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
-                msg.append("JAR signer ").append(signerName).append(": ")
-                        .append(issue.getIssue()).append(": ").append(issue);
+                msg.append("JAR signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1088,8 +1186,25 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v2 signer ")
-                        .append(signerName).append(": ")
-                        .append(issue.getIssue()).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
+            }
+        }
+        for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+            String signerName = "signer #" + (signer.getIndex() + 1);
+            for (IssueWithParams issue : signer.getErrors()) {
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK Signature Scheme v3 signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
             }
         }
 
@@ -1099,7 +1214,8 @@
     private void assertVerified(
             String apkFilenameInResources,
             Integer minSdkVersionOverride,
-            Integer maxSdkVersionOverride) throws Exception {
+            Integer maxSdkVersionOverride)
+            throws Exception {
         assertVerified(
                 verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride),
                 apkFilenameInResources);
@@ -1130,8 +1246,12 @@
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
-                msg.append("JAR signer ").append(signerName).append(": ")
-                        .append(issue.getIssue()).append(" ").append(issue);
+                msg.append("JAR signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(" ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1144,7 +1264,9 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v2 signer ")
-                        .append(signerName).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue);
             }
         }
         for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
@@ -1157,22 +1279,78 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v3 signer ")
-                        .append(signerName).append(": ").append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue);
             }
         }
 
-        fail("APK failed verification for the wrong reason"
-                + ". Expected: " + expectedIssue + ", actual: " + msg);
+        fail(
+                "APK failed verification for the wrong reason"
+                        + ". Expected: "
+                        + expectedIssue
+                        + ", actual: "
+                        + msg);
+    }
+
+    private static void assertSourceStampVerificationFailure(
+            ApkVerifier.Result result, Issue expectedIssue) {
+        if (result.isSourceStampVerified()) {
+            fail(
+                    "APK source stamp verification succeeded instead of failing with "
+                            + expectedIssue);
+            return;
+        }
+
+        StringBuilder msg = new StringBuilder();
+        List<IssueWithParams> resultIssueWithParams =
+                Stream.of(result.getErrors(), result.getWarnings())
+                        .filter(Objects::nonNull)
+                        .flatMap(Collection::stream)
+                        .collect(Collectors.toList());
+        for (IssueWithParams issue : resultIssueWithParams) {
+            if (expectedIssue.equals(issue.getIssue())) {
+                return;
+            }
+            if (msg.length() > 0) {
+                msg.append('\n');
+            }
+            msg.append(issue);
+        }
+
+        ApkVerifier.Result.SourceStampInfo signer = result.getSourceStampInfo();
+        if (signer != null) {
+            List<IssueWithParams> sourceStampIssueWithParams =
+                    Stream.of(signer.getErrors(), signer.getWarnings())
+                            .filter(Objects::nonNull)
+                            .flatMap(Collection::stream)
+                            .collect(Collectors.toList());
+            for (IssueWithParams issue : sourceStampIssueWithParams) {
+                if (expectedIssue.equals(issue.getIssue())) {
+                    return;
+                }
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK SourceStamp signer").append(": ").append(issue);
+            }
+        }
+
+        fail(
+                "APK source stamp failed verification for the wrong reason"
+                        + ". Expected: "
+                        + expectedIssue
+                        + ", actual: "
+                        + msg);
     }
 
     private void assertVerificationFailure(
-            String apkFilenameInResources, ApkVerifier.Issue expectedIssue)
-                    throws Exception {
+            String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
         assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
     }
 
-    private void assertVerifiedForEach(
-            String apkFilenamePatternInResources, String[] args) throws Exception {
+    private void assertVerifiedForEach(String apkFilenamePatternInResources, String[] args)
+            throws Exception {
         assertVerifiedForEach(apkFilenamePatternInResources, args, null, null);
     }
 
@@ -1180,7 +1358,8 @@
             String apkFilenamePatternInResources,
             String[] args,
             Integer minSdkVersionOverride,
-            Integer maxSdkVersionOverride) throws Exception {
+            Integer maxSdkVersionOverride)
+            throws Exception {
         for (String arg : args) {
             String apkFilenameInResources =
                     String.format(Locale.US, apkFilenamePatternInResources, arg);
@@ -1189,12 +1368,11 @@
     }
 
     private void assertVerifiedForEachForMinSdkVersion(
-            String apkFilenameInResources, String[] args, int minSdkVersion)
-                    throws Exception {
+            String apkFilenameInResources, String[] args, int minSdkVersion) throws Exception {
         assertVerifiedForEach(apkFilenameInResources, args, minSdkVersion, null);
     }
 
-    private static byte[] sha256(byte[] msg) throws Exception {
+    private static byte[] sha256(byte[] msg) {
         try {
             return MessageDigest.getInstance("SHA-256").digest(msg);
         } catch (NoSuchAlgorithmException e) {
@@ -1202,7 +1380,7 @@
         }
     }
 
-    private static void assumeThatRsaPssAvailable() throws Exception {
+    private static void assumeThatRsaPssAvailable() {
         Assume.assumeTrue(Security.getProviders("Signature.SHA256withRSA/PSS") != null);
     }
 }
diff --git a/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java b/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
index 9578926..12f08f1 100644
--- a/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
+++ b/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
@@ -90,14 +90,14 @@
         assertArrayEquals(expectedBytes, resultBytes);
     }
 
-    private byte[] getDataSinkBytes(ByteArrayDataSink dataSink) {
+    private static byte[] getDataSinkBytes(ByteArrayDataSink dataSink) {
         ByteBuffer result = dataSink.getByteBuffer(0, (int)dataSink.size());
         byte[] resultBytes = new byte[result.limit()];
         result.get(resultBytes);
         return resultBytes;
     }
 
-    private byte[] createFileContent(int fileSize) {
+    private static byte[] createFileContent(int fileSize) {
         byte[] fullFileContent = new byte[fileSize];
         for (int i = 0; i < fileSize; ++i) {
             fullFileContent[i] = (byte) (i % 255);
diff --git a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
index 1fa1e40..85e9e90 100644
--- a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
+++ b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
@@ -66,8 +66,7 @@
     private static String generateRootHash(String inputResource, byte[] salt) throws IOException {
         byte[] input = Resources.toByteArray(VerityTreeBuilderTest.class, inputResource);
         assertNotNull(input);
-        try {
-            VerityTreeBuilder builder = new VerityTreeBuilder(salt);
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(salt)) {
             return HexEncoding.encode(builder.generateVerityTreeRootHash(
                     DataSources.asDataSource(ByteBuffer.wrap(input))));
         } catch (NoSuchAlgorithmException e) {
@@ -88,8 +87,7 @@
         };
         String expectedRootHash =
                 "7694e72c242107a5b4ce6091faf867e2f13c033b6b64faddcb13b3d698a8495a";
-        {
-            VerityTreeBuilder builder = new VerityTreeBuilder(null);
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
             byte[] rootHash = builder.generateVerityTreeRootHash(
                     DataSources.asDataSource(ByteBuffer.allocate(4096)),  // before Signing Block
                     makeStringDataSource("this is central directory (fake data)"),
@@ -100,13 +98,12 @@
         // File ending with different length of zeros get the same hash.  This is to match the
         // behavior of fs-verity, where it expects the client to add salt for the same level of
         // protection.
-        {
+        try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
             ByteBuffer fileTailWithDifferentLengthOfZeros = ByteBuffer.allocate(
                     sampleEoCDFromDisk.length + 1);
             fileTailWithDifferentLengthOfZeros.put(sampleEoCDFromDisk);
             fileTailWithDifferentLengthOfZeros.rewind();
 
-            VerityTreeBuilder builder = new VerityTreeBuilder(null);
             byte[] rootHash = builder.generateVerityTreeRootHash(
                     DataSources.asDataSource(ByteBuffer.allocate(4096)),  // before Signing Block
                     makeStringDataSource("this is central directory (fake data)"),
diff --git a/src/test/resources/com/android/apksig/golden-aligned-out.apk b/src/test/resources/com/android/apksig/golden-aligned-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
index 5133049..1c0edeb 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
index b9dc782..965f901 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
index d947e3c..69f5e64 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
index 88c571b..13a3bc9 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
index 25f35cc..19931ed 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
index 30e1f72..36f4825 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
index f97cbeb..48f9aa3 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
index cc03744..cf04aa2 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
index e359da7..f6b60d6 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
index 68f07ed..f34a8e5 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
index 4b51e4f..b0debf6 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
index 7177862..9940c4f 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
index bd3e668..f8b4171 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
index 67a7d3f..80a70b9 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
index 7289853..5ae642a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
new file mode 100644
index 0000000..5ce588a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-out.apk b/src/test/resources/com/android/apksig/golden-rsa-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
new file mode 100644
index 0000000..232db96
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
index c708211..5194496 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
index dd6324b..02ecee0 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
index 4fdc18c..92e5099 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
index 4e523ca..08538a0 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
index 74e7dbc..c18c445 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
index 831c756..9bcbc7a 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
index 3196267..4db4579 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-with-stamp-file.apk b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
new file mode 100644
index 0000000..604fe6f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
new file mode 100644
index 0000000..add4aa0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
new file mode 100644
index 0000000..e55eb90
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
new file mode 100644
index 0000000..de23558
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
new file mode 100644
index 0000000..1dc1e99
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
new file mode 100644
index 0000000..f1105f9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
new file mode 100644
index 0000000..d28774a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk
new file mode 100644
index 0000000..604fe6f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..6a561c0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
new file mode 100644
index 0000000..714f9ff
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..a29dd6c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..c58ec8b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk
new file mode 100644
index 0000000..2f2a592
--- /dev/null
+++ b/src/test/resources/com/android/apksig/valid-stamp.apk
Binary files differ