Add support for APK Signature Scheme v3.1 for rotation targeting

This commit adds support for a new v3.1 APK signature scheme that
allows APK signing key rotation to target T+. This version will still
default to using the v3.0 signing block for rotation, but once the
build system is updated to support the new rotation-min-sdk-version
option v3.1 will be enabled by default for all key rotations.

This commit also updates all of the golden APKs that use key rotation
because the minSdkVersion of the v3 signer is updated from 24 (the
first API level that supports the signature algorithm) to 28 (the
first API level that supports v3).

Bug: 192301300
Test: gradlew test
Change-Id: I49cc98ea803d18d53131a78be668921d58ac5f4b
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 9fd0c34..5a9daf6 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -22,6 +22,7 @@
 import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 
@@ -147,6 +148,7 @@
         int minSdkVersion = 1;
         boolean minSdkVersionSpecified = false;
         int maxSdkVersion = Integer.MAX_VALUE;
+        int rotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
         List<SignerParams> signers = new ArrayList<>(1);
         SignerParams signerParams = new SignerParams();
         SigningCertificateLineage lineage = null;
@@ -175,6 +177,9 @@
                 minSdkVersionSpecified = true;
             } else if ("max-sdk-version".equals(optionName)) {
                 maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
+            } else if ("rotation-min-sdk-version".equals(optionName)) {
+                rotationMinSdkVersion = optionsParser.getRequiredIntValue(
+                        "Minimum API Level for Rotation");
             } else if ("v1-signing-enabled".equals(optionName)) {
                 v1SigningEnabled = optionsParser.getOptionalBooleanValue(true);
             } else if ("v2-signing-enabled".equals(optionName)) {
@@ -362,7 +367,8 @@
                         .setVerityEnabled(verityEnabled)
                         .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
                         .setDebuggableApkPermitted(debuggableApkPermitted)
-                        .setSigningCertificateLineage(lineage);
+                        .setSigningCertificateLineage(lineage)
+                        .setMinSdkVersionForRotation(rotationMinSdkVersion);
         if (minSdkVersionSpecified) {
             apkSignerBuilder.setMinSdkVersion(minSdkVersion);
         }
@@ -568,6 +574,9 @@
                         "Verified using v3 scheme (APK Signature Scheme v3): "
                                 + result.isVerifiedUsingV3Scheme());
                 System.out.println(
+                        "Verified using v3.1 scheme (APK Signature Scheme v3.1): "
+                                + result.isVerifiedUsingV31Scheme());
+                System.out.println(
                         "Verified using v4 scheme (APK Signature Scheme v4): "
                                 + result.isVerifiedUsingV4Scheme());
                 System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
@@ -576,10 +585,28 @@
                 }
             }
             if (printCerts) {
-                int signerNumber = 0;
-                for (X509Certificate signerCert : signerCerts) {
-                    signerNumber++;
-                    printCertificate(signerCert, "Signer #" + signerNumber, verbose);
+                // The v3.1 signature scheme allows key rotation to target T+ while the original
+                // signing key can still be used with v3.0; if a v3.1 block is present then also
+                // include the target SDK versions for both rotation and the original signing key.
+                if (result.isVerifiedUsingV31Scheme()) {
+                    for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+                        printCertificate(signer.getCertificate(),
+                                "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+                                        + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+                                verbose);
+                    }
+                    for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+                        printCertificate(signer.getCertificate(),
+                                "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+                                        + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+                                verbose);
+                    }
+                } else {
+                    int signerNumber = 0;
+                    for (X509Certificate signerCert : signerCerts) {
+                        signerNumber++;
+                        printCertificate(signerCert, "Signer #" + signerNumber, verbose);
+                    }
                 }
                 if (sourceStampInfo != null) {
                     printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
@@ -634,6 +661,20 @@
                         "WARNING: APK Signature Scheme v3 " + signerName + ": " + warning);
             }
         }
+        for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+            String signerName = "signer #" + (signer.getIndex() + 1) + "(minSdkVersion="
+                    + signer.getMinSdkVersion() + ", maxSdkVersion=" + signer.getMaxSdkVersion()
+                    + ")";
+            for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
+                System.err.println(
+                        "ERROR: APK Signature Scheme v3.1 " + signerName + ": " + error);
+            }
+            for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
+                warningsEncountered = true;
+                warningsOut.println(
+                        "WARNING: APK Signature Scheme v3.1 " + signerName + ": " + warning);
+            }
+        }
 
         if (sourceStampInfo != null) {
             for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index d66b7a3..372c891 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -61,6 +61,15 @@
 --max-sdk-version     Highest API Level on which this APK's signatures will be
                       verified. By default, the highest possible value is used.
 
+--rotation-min-sdk-version  Lowest API Level for which an APK's rotated signing
+                      key should be used to produce the APK's signature. The
+                      original signing key for the APK will be used for all
+                      previous platform versions. Specifying a value <= 32
+                      (Android Sv2) will result in the original V3 signing block
+                      being used without platform targeting. By default,
+                      rotated signing keys will be used with the V3.1 signing
+                      block which supports Android T+.
+
 --debuggable-apk-permitted  Whether to permit signing android:debuggable="true"
                       APKs. Android disables some of its security protections
                       for such apps. For example, anybody with ADB shell access
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index ca792c4..917d283 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -22,6 +22,7 @@
 import com.android.apksig.apk.ApkSigningBlockNotFoundException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.util.ByteBufferDataSource;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.EocdRecord;
@@ -89,6 +90,7 @@
     private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
     private final boolean mForceSourceStampOverwrite;
     private final Integer mMinSdkVersion;
+    private final int mRotationMinSdkVersion;
     private final boolean mV1SigningEnabled;
     private final boolean mV2SigningEnabled;
     private final boolean mV3SigningEnabled;
@@ -118,6 +120,7 @@
             SigningCertificateLineage sourceStampSigningCertificateLineage,
             boolean forceSourceStampOverwrite,
             Integer minSdkVersion,
+            int rotationMinSdkVersion,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
@@ -141,6 +144,7 @@
         mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
         mForceSourceStampOverwrite = forceSourceStampOverwrite;
         mMinSdkVersion = minSdkVersion;
+        mRotationMinSdkVersion = rotationMinSdkVersion;
         mV1SigningEnabled = v1SigningEnabled;
         mV2SigningEnabled = v2SigningEnabled;
         mV3SigningEnabled = v3SigningEnabled;
@@ -296,7 +300,8 @@
                             .setVerityEnabled(mVerityEnabled)
                             .setDebuggableApkPermitted(mDebuggableApkPermitted)
                             .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
-                            .setSigningCertificateLineage(mSigningCertificateLineage);
+                            .setSigningCertificateLineage(mSigningCertificateLineage)
+                            .setMinSdkVersionForRotation(mRotationMinSdkVersion);
             if (mCreatedBy != null) {
                 signerEngineBuilder.setCreatedBy(mCreatedBy);
             }
@@ -1093,6 +1098,7 @@
         private boolean mOtherSignersSignaturesPreserved;
         private String mCreatedBy;
         private Integer mMinSdkVersion;
+        private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
 
         private final ApkSignerEngine mSignerEngine;
 
@@ -1301,6 +1307,28 @@
         }
 
         /**
+         * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+         * key should be used to produce the APK's signature. The original signing key for the APK
+         * will be used for all previous platform versions. If a rotated key with signing lineage is
+         * not provided then this method is a noop. This method is useful for overriding the
+         * default behavior where Android T is set as the minimum API level for rotation.
+         *
+         * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+         * in the original V3 signing block being used without platform targeting.
+         *
+         * <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}
+         */
+        public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+            checkInitializedWithoutEngine();
+            mRotationMinSdkVersion = minSdkVersion;
+            return this;
+        }
+
+        /**
          * 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
@@ -1538,6 +1566,7 @@
                     mSourceStampSigningCertificateLineage,
                     mForceSourceStampOverwrite,
                     mMinSdkVersion,
+                    mRotationMinSdkVersion,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
                     mV3SigningEnabled,
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 354dfbd..6bdec91 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -24,6 +24,7 @@
 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.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
 
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
@@ -201,23 +202,58 @@
         Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
         if (maxSdkVersion >= AndroidSdkVersion.N) {
             RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
-            // Android P and newer attempts to verify APKs using APK Signature Scheme v3
-            if (maxSdkVersion >= AndroidSdkVersion.P) {
+            // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+            // also includes stripping protection for the minimum SDK version on which the rotated
+            // signing key should be used.
+            int rotationMinSdkVersion = 0;
+            if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
                 try {
-                    ApkSigningBlockUtils.Result v3Result =
-                            V3SchemeVerifier.verify(
-                                    executor,
-                                    apk,
-                                    zipSections,
-                                    Math.max(minSdkVersion, AndroidSdkVersion.P),
-                                    maxSdkVersion);
+                    ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+                            zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+                            maxSdkVersion)
+                            .setRunnablesExecutor(executor)
+                            .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+                            .build()
+                            .verify();
+                    foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+                    rotationMinSdkVersion = v31Result.signers.stream().mapToInt(
+                            signer -> signer.minSdkVersion).min().orElse(0);
+                    result.mergeFrom(v31Result);
+                    signatureSchemeApkContentDigests.put(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31,
+                            getApkContentDigestsFromSigningSchemeResult(v31Result));
+                } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                    // v3.1 signature not required
+                }
+                if (result.containsErrors()) {
+                    return result;
+                }
+            }
+            // Android P and newer attempts to verify APKs using APK Signature Scheme v3
+            if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) {
+                try {
+                    V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+                            zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+                            maxSdkVersion)
+                            .setRunnablesExecutor(executor)
+                            .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                    if (rotationMinSdkVersion > 0) {
+                        builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+                    }
+                    ApkSigningBlockUtils.Result v3Result = builder.build().verify();
                     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
+                    // v3 signature not required unless a v3.1 signature was found as a v3.1
+                    // signature is intended to support key rotation on T+ with the v3 signature
+                    // containing the original signing key.
+                    if (foundApkSigSchemeIds.contains(
+                            ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31)) {
+                        result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+                    }
                 }
                 if (result.containsErrors()) {
                     return result;
@@ -1005,6 +1041,7 @@
         private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
         private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
         private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+        private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>();
         private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
         private SourceStampInfo mSourceStampInfo;
 
@@ -1012,6 +1049,7 @@
         private boolean mVerifiedUsingV1Scheme;
         private boolean mVerifiedUsingV2Scheme;
         private boolean mVerifiedUsingV3Scheme;
+        private boolean mVerifiedUsingV31Scheme;
         private boolean mVerifiedUsingV4Scheme;
         private boolean mSourceStampVerified;
         private boolean mWarningsAsErrors;
@@ -1050,6 +1088,13 @@
         }
 
         /**
+         * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+         */
+        public boolean isVerifiedUsingV31Scheme() {
+            return mVerifiedUsingV31Scheme;
+        }
+
+        /**
          * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
          */
         public boolean isVerifiedUsingV4Scheme() {
@@ -1115,6 +1160,18 @@
             return mV3SchemeSigners;
         }
 
+        /**
+         * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+         * signature.
+         *
+         * <note> Multiple signers represent different targeted platform versions, not
+         * a signing identity of multiple signers.  APK Signature Scheme v3.1 only supports single
+         * signer identities.</note>
+         */
+        public List<V3SchemeSignerInfo> getV31SchemeSigners() {
+            return mV31SchemeSigners;
+        }
+
         private List<V4SchemeSignerInfo> getV4SchemeSigners() {
             return mV4SchemeSigners;
         }
@@ -1212,6 +1269,13 @@
                     }
                     mSigningCertificateLineage = source.signingCertificateLineage;
                     break;
+                case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31:
+                    mVerifiedUsingV31Scheme = source.verified;
+                    for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+                        mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+                    }
+                    mSigningCertificateLineage = source.signingCertificateLineage;
+                    break;
                 case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
                     mVerifiedUsingV4Scheme = source.verified;
                     for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
@@ -1497,6 +1561,8 @@
             private final List<IssueWithParams> mWarnings;
             private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
                     mContentDigests;
+            private final int mMinSdkVersion;
+            private final int mMaxSdkVersion;
 
             private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
                 mIndex = result.index;
@@ -1504,6 +1570,8 @@
                 mErrors = result.getErrors();
                 mWarnings = result.getWarnings();
                 mContentDigests = result.contentDigests;
+                mMinSdkVersion = result.minSdkVersion;
+                mMaxSdkVersion = result.maxSdkVersion;
             }
 
             /**
@@ -1549,6 +1617,20 @@
             public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
                 return mContentDigests;
             }
+
+            /**
+             * Returns the minimum SDK version on which this signer should be verified.
+             */
+            public int getMinSdkVersion() {
+                return mMinSdkVersion;
+            }
+
+            /**
+             * Returns the maximum SDK version on which this signer should be verified.
+             */
+            public int getMaxSdkVersion() {
+                return mMaxSdkVersion;
+            }
         }
 
         /**
@@ -2527,6 +2609,51 @@
                 + " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
 
         /**
+         * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+         * was not found.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+         * </ul>
+         */
+        V31_BLOCK_MISSING(
+                "The v3 signer indicates key rotation should be supported starting from SDK "
+                        + "version %1$s, but a v3.1 block was not found"),
+
+        /**
+         * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+         * targeting rotation in the v3.1 signer block.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+         * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+         * </ul>
+         */
+        V31_ROTATION_MIN_SDK_MISMATCH(
+                "The v3 signer indicates key rotation should be supported starting from SDK "
+                        + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+        /**
+         * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+         * SDK version stripping protection attribute was not written to the v3 signer.
+         *
+         * <ul>
+         * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+         * </ul>
+         */
+        V31_ROTATION_MIN_SDK_ATTR_MISSING(
+                "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+                        + " not contain the attribute to detect if this signature is stripped"),
+
+        /**
+         * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+         * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+         * same as the SDK version for rotation then this should be written to a v3.0 block.
+         */
+        V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+                "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+        /**
          * APK Signing Block contains an unknown entry.
          *
          * <ul>
diff --git a/src/main/java/com/android/apksig/Constants.java b/src/main/java/com/android/apksig/Constants.java
index 32c7375..f64064c 100644
--- a/src/main/java/com/android/apksig/Constants.java
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -32,6 +32,7 @@
     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_V31 = 31;
     public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
 
     public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
@@ -41,6 +42,8 @@
 
     public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
             V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+    public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+            V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
     public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
 
     public static final int V1_SOURCE_STAMP_BLOCK_ID =
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index e2256da..c57ca39 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -22,6 +22,7 @@
 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.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
 
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
@@ -34,6 +35,7 @@
 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.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.apk.v4.V4SchemeSigner;
 import com.android.apksig.internal.apk.v4.V4Signature;
@@ -66,6 +68,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -102,6 +105,7 @@
     private final List<SignerConfig> mSignerConfigs;
     private final SignerConfig mSourceStampSignerConfig;
     private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+    private final int mRotationMinSdkVersion;
     private final int mMinSdkVersion;
     private final SigningCertificateLineage mSigningCertificateLineage;
 
@@ -184,6 +188,7 @@
             SignerConfig sourceStampSignerConfig,
             SigningCertificateLineage sourceStampSigningCertificateLineage,
             int minSdkVersion,
+            int rotationMinSdkVersion,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
@@ -211,6 +216,7 @@
         mSourceStampSignerConfig = sourceStampSignerConfig;
         mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
         mMinSdkVersion = minSdkVersion;
+        mRotationMinSdkVersion = rotationMinSdkVersion;
         mSigningCertificateLineage = signingCertificateLineage;
 
         if (v1SigningEnabled) {
@@ -358,9 +364,15 @@
                 config.maxSdkVersion = currentMinSdk - 1;
             }
             config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
-            if (mSigningCertificateLineage != null) {
+            // Only use a rotated key and signing lineage if the config's max SDK version is greater
+            // than that requested to support rotation.
+            if (mSigningCertificateLineage != null
+                    && config.maxSdkVersion >= mRotationMinSdkVersion) {
                 config.mSigningCertificateLineage =
                         mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
+                if (config.minSdkVersion < mRotationMinSdkVersion) {
+                    config.minSdkVersion = mRotationMinSdkVersion;
+                }
             }
             // we know that this config will be used, so add it to our result, order doesn't matter
             // at this point (and likely only one will be needed
@@ -383,6 +395,23 @@
 
     private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
             boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+        // While the V3 signature scheme supports rotation, it is possible for a caller to specify
+        // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1;
+        // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block
+        // will use the original signing key.
+        if (mSigningCertificateLineage != null
+            && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+            && mMinSdkVersion < mRotationMinSdkVersion) {
+            SigningCertificateLineage subLineage = mSigningCertificateLineage
+                .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0));
+            if (subLineage.size() != 1) {
+                throw new IllegalArgumentException(
+                    "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage"
+                        + " for the v3.0 signing block is missing.  Please provide"
+                        + " the oldest signer to enable v3.1 signing.");
+            }
+        }
+
         return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
                 ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
     }
@@ -993,13 +1022,47 @@
             invalidateV3Signature();
             List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
                     createV3SignerConfigs(apkSigningBlockPaddingSupported);
+            // If the signing key has been rotated, the caller has requested to use the rotated
+            // signing key starting from an SDK version where v3.1 is supported, and the minimum
+            // SDK version for the APK is less than the requested rotation minimum, then the APK
+            // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0
+            // scheme with the original signing key. If the APK's minSdkVersion is >= the requested
+            // SDK version for rotation then just use the v3.0 signing block for this.
+            if (mSigningCertificateLineage != null
+                    && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+                    && mMinSdkVersion < mRotationMinSdkVersion) {
+                List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
+                Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator =
+                        v3SignerConfigs.iterator();
+                while (v3SignerIterator.hasNext()) {
+                    ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+                    // All signing configs with a min SDK version that supports v3.1 should be used
+                    // in the v3.1 signing block and removed from the v3.0 block.
+                    if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
+                        v31SignerConfigs.add(signerConfig);
+                        v3SignerIterator.remove();
+                    }
+                }
+                ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+                        v31SigningSchemeBlockAndDigests =
+                        new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+                                v31SignerConfigs)
+                                .setRunnablesExecutor(mExecutor)
+                                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+                                .build()
+                                .generateApkSignatureSchemeV3BlockAndDigests();
+                signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+            }
+            V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+                zipCentralDirectory, eocd, v3SignerConfigs)
+                .setRunnablesExecutor(mExecutor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+            if (mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+                    && mMinSdkVersion < mRotationMinSdkVersion) {
+                builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
+            }
             v3SigningSchemeBlockAndDigests =
-                    V3SchemeSigner.generateApkSignatureSchemeV3Block(
-                            mExecutor,
-                            beforeCentralDir,
-                            zipCentralDirectory,
-                            eocd,
-                            v3SignerConfigs);
+                builder.build().generateApkSignatureSchemeV3BlockAndDigests();
             signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
         }
         if (isEligibleForSourceStamp()) {
@@ -1630,6 +1693,7 @@
         private boolean mV1SigningEnabled = true;
         private boolean mV2SigningEnabled = true;
         private boolean mV3SigningEnabled = true;
+        private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
         private boolean mVerityEnabled = false;
         private boolean mDebuggableApkPermitted = true;
         private boolean mOtherSignersSignaturesPreserved;
@@ -1718,6 +1782,7 @@
                     mStampSignerConfig,
                     mSourceStampSigningCertificateLineage,
                     mMinSdkVersion,
+                    mRotationMinSdkVersion,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
                     mV3SigningEnabled,
@@ -1840,5 +1905,23 @@
             }
             return this;
         }
+
+        /**
+         * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+         * key should be used to produce the APK's signature. The original signing key for the APK
+         * will be used for all previous platform versions. If a rotated key with signing lineage is
+         * not provided then this method is a noop.
+         *
+         * <p>By default, if a signing lineage is specified with {@link
+         * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+         * V3.1 will be used to only apply the rotation on devices running Android T+.
+         *
+         * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+         * in the original V3 signing block being used without platform targeting.
+         */
+        public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+            mRotationMinSdkVersion = minSdkVersion;
+            return this;
+        }
     }
 }
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 6c505be..43b7f5e 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -191,41 +191,62 @@
      */
     public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
             throws IOException, ApkFormatException {
-        SignatureInfo signatureInfo;
+        ApkUtils.ZipSections zipSections;
         try {
-            ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
-            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
-                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
-            signatureInfo =
-                    ApkSigningBlockUtils.findSignature(apk, zipSections,
-                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+            zipSections = ApkUtils.findZipSections(apk);
         } catch (ZipFormatException e) {
             throw new ApkFormatException(e.getMessage());
-        } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+        }
+
+        List<SignatureInfo> signatureInfoList = new ArrayList<>();
+        try {
+            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+            signatureInfoList.add(
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+        }
+        catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+            // This could be expected if there's only a V3 signature block.
+        }
+        try {
+            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+                    ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+            signatureInfoList.add(
+                    ApkSigningBlockUtils.findSignature(apk, zipSections,
+                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+        }
+        catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+            // This could be expected if the provided APK is not signed with the v3 signature scheme
+        }
+        if (signatureInfoList.isEmpty()) {
             throw new IllegalArgumentException(
                     "The provided APK does not contain a valid V3 signature block.");
         }
 
-        // FORMAT:
-        // * length-prefixed sequence of length-prefixed signers:
-        //   * length-prefixed signed data
-        //   * minSDK
-        //   * maxSDK
-        //   * length-prefixed sequence of length-prefixed signatures
-        //   * length-prefixed public key
-        ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
         List<SigningCertificateLineage> lineages = new ArrayList<>(1);
-        while (signers.hasRemaining()) {
-            ByteBuffer signer = getLengthPrefixedSlice(signers);
-            ByteBuffer signedData = getLengthPrefixedSlice(signer);
-            try {
-                SigningCertificateLineage lineage = readFromSignedData(signedData);
-                lineages.add(lineage);
-            } catch (IllegalArgumentException ignored) {
-                // The current signer block does not contain a valid lineage, but it is possible
-                // another block will.
+        for (SignatureInfo signatureInfo : signatureInfoList) {
+            // FORMAT:
+            // * length-prefixed sequence of length-prefixed signers:
+            //   * length-prefixed signed data
+            //   * minSDK
+            //   * maxSDK
+            //   * length-prefixed sequence of length-prefixed signatures
+            //   * length-prefixed public key
+            ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+            while (signers.hasRemaining()) {
+                ByteBuffer signer = getLengthPrefixedSlice(signers);
+                ByteBuffer signedData = getLengthPrefixedSlice(signer);
+                try {
+                    SigningCertificateLineage lineage = readFromSignedData(signedData);
+                    lineages.add(lineage);
+                } catch (IllegalArgumentException ignored) {
+                    // The current signer block does not contain a valid lineage, but it is possible
+                    // another block will.
+                }
             }
         }
+
         SigningCertificateLineage result;
         if (lineages.isEmpty()) {
             throw new IllegalArgumentException(
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 5b34849..44dcc79 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -104,6 +104,7 @@
     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_V31 = 31;
     public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
 
     /**
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
index 9cd7b1f..59fa791 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -21,6 +21,7 @@
 import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
 
 import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.internal.apk.ApkSignerInfo;
 import com.android.apksig.internal.apk.ApkSupportedSignature;
@@ -137,6 +138,13 @@
 
         for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
                 signatureSchemeApkDigests.entrySet()) {
+            // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
+            // v3.0 block must always be present with a v3.1 block is it sufficient to just use the
+            // v3.0 block?
+            if (signatureSchemeApkDigest.getKey()
+                    == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
+                continue;
+            }
             if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
                 result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
                 return;
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
index 3b70aa0..ac7c80f 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
@@ -16,10 +16,27 @@
 
 package com.android.apksig.internal.apk.v3;
 
+import com.android.apksig.internal.util.AndroidSdkVersion;
+
 /** Constants used by the V3 Signature Scheme signing and verification. */
 public class V3SchemeConstants {
     private V3SchemeConstants() {}
 
     public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+    public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
     public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+    public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
+    public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
+    // TODO(b/192301300): Once the signing config has been updated to support specifying a
+    // minSdkVersion for rotation this should be updated to T.
+    public static final int DEFAULT_ROTATION_MIN_SDK_VERSION  = AndroidSdkVersion.P;
+
+    /**
+     * This attribute is intended to be written to the V3.0 signer block as an additional attribute
+     * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
+     * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
+     * for rotation in the v3.1 signing block is not X, then the APK should be rejected.
+     */
+    public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
 }
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 04260d5..5cb2ba1 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
@@ -24,6 +24,7 @@
 
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
@@ -44,6 +45,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalInt;
 
 /**
  * APK Signature Scheme v3 signer.
@@ -56,13 +58,34 @@
  *     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 {
+public class V3SchemeSigner {
     public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
             V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
     public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
 
-    /** Hidden constructor to prevent instantiation. */
-    private V3SchemeSigner() {}
+    private final RunnablesExecutor mExecutor;
+    private final DataSource mBeforeCentralDir;
+    private final DataSource mCentralDir;
+    private final DataSource mEocd;
+    private final List<SignerConfig> mSignerConfigs;
+    private final int mBlockId;
+    private final OptionalInt mOptionalRotationMinSdkVersion;
+
+    private V3SchemeSigner(DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd,
+            List<SignerConfig> signerConfigs,
+            RunnablesExecutor executor,
+            int blockId,
+            OptionalInt optionalRotationMinSdkVersion) {
+        mBeforeCentralDir = beforeCentralDir;
+        mCentralDir = centralDir;
+        mEocd = eocd;
+        mSignerConfigs = signerConfigs;
+        mExecutor = executor;
+        mBlockId = blockId;
+        mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+    }
 
     /**
      * Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
@@ -129,21 +152,18 @@
         }
     }
 
-    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 =
-                ApkSigningBlockUtils.computeContentDigests(
-                        executor, beforeCentralDir, centralDir, eocd, signerConfigs);
-        return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
-                generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
-                digestInfo.getSecond());
+    public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
+            RunnablesExecutor executor,
+            DataSource beforeCentralDir,
+            DataSource centralDir,
+            DataSource eocd,
+            List<SignerConfig> signerConfigs)
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+        return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
+                .setRunnablesExecutor(executor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+                .build()
+                .generateApkSignatureSchemeV3BlockAndDigests();
     }
 
     public static byte[] generateV3SignerAttribute(
@@ -162,14 +182,49 @@
         return result.array();
     }
 
-    private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
-            List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+    private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+            int rotationMinSdkVersion) {
+        // FORMAT (little endian):
+        // * length-prefixed bytes: attribute pair
+        //   * uint32: ID
+        //   * bytes: value - int32 representing minimum SDK version for rotation
+        int payloadSize = 4 + 4 + 4;
+        ByteBuffer result = ByteBuffer.allocate(payloadSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(payloadSize - 4);
+        result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
+        result.putInt(rotationMinSdkVersion);
+        return result.array();
+    }
+
+    /**
+     * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
+     * signing scheme block and digests based on the parameters provided to the {@link Builder}.
+     *
+     * @throws IOException if an I/O error occurs
+     * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+     *         missing
+     * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
+     * @throws SignatureException if an error occurs when computing digests or generating
+     *         signatures
+     */
+    public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
+            throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+        Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+                ApkSigningBlockUtils.computeContentDigests(
+                        mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
+        return new SigningSchemeBlockAndDigests(
+                generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
+    }
+
+    private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+            Map<ContentDigestAlgorithm, byte[]> contentDigests)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         // FORMAT:
         // * length-prefixed sequence of length-prefixed signer blocks.
-        List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+        List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
         int signerNumber = 0;
-        for (SignerConfig signerConfig : signerConfigs) {
+        for (SignerConfig signerConfig : mSignerConfigs) {
             signerNumber++;
             byte[] signerBlock;
             try {
@@ -187,10 +242,10 @@
                         new byte[][] {
                             encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
                         }),
-                V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+                mBlockId);
     }
 
-    private static byte[] generateSignerBlock(
+    private byte[] generateSignerBlock(
             SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         if (signerConfig.certificates.isEmpty()) {
@@ -240,7 +295,7 @@
         return encodeSigner(signer);
     }
 
-    private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
+    private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
         byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
         byte[] signatures =
                 encodeAsLengthPrefixedElement(
@@ -269,7 +324,7 @@
         return result.array();
     }
 
-    private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
+    private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
         byte[] digests =
                 encodeAsLengthPrefixedElement(
                         encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
@@ -305,11 +360,14 @@
         return result.array();
     }
 
-    private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
-        if (signerConfig.mSigningCertificateLineage == null) {
-            return new byte[0];
+    private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
+        if (signerConfig.mSigningCertificateLineage != null) {
+            return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
+        } else if (mOptionalRotationMinSdkVersion.isPresent()) {
+            return generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+                    mOptionalRotationMinSdkVersion.getAsInt());
         }
-        return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
+        return new byte[0];
     }
 
     private static final class V3SignatureSchemeBlock {
@@ -329,4 +387,75 @@
             public byte[] additionalAttributes;
         }
     }
+
+    /** Builder of {@link V3SchemeSigner} instances. */
+    public static class Builder {
+        private final DataSource mBeforeCentralDir;
+        private final DataSource mCentralDir;
+        private final DataSource mEocd;
+        private final List<SignerConfig> mSignerConfigs;
+
+        private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+        private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+        private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+        /**
+         * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
+         * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
+         * be used to sign the APK.
+         */
+        public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
+                List<SignerConfig> signerConfigs) {
+            mBeforeCentralDir = beforeCentralDir;
+            mCentralDir = centralDir;
+            mEocd = eocd;
+            mSignerConfigs = signerConfigs;
+        }
+
+        /**
+         * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
+         */
+        public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Sets the {@code blockId} to be used for the V3 signature block.
+         *
+         * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+         */
+        public Builder setBlockId(int blockId) {
+            mBlockId = blockId;
+            return this;
+        }
+
+        /**
+         * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
+         * signer's block.
+         *
+         * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
+         * is not modified or removed from the APK's signature block.
+         */
+        public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+            mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+            return this;
+        }
+
+        /**
+         * Returns a new {@link V3SchemeSigner} built with the configuration provided to this
+         * {@code Builder}.
+         */
+        public V3SchemeSigner build() {
+            return new V3SchemeSigner(mBeforeCentralDir,
+                    mCentralDir,
+                    mEocd,
+                    mSignerConfigs,
+                    mExecutor,
+                    mBlockId,
+                    mOptionalRotationMinSdkVersion);
+        }
+    }
 }
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 ea93194..9b9abba 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
@@ -28,7 +28,6 @@
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
-import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferUtils;
 import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.internal.util.X509CertificateUtils;
@@ -38,6 +37,7 @@
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.KeyFactory;
@@ -54,6 +54,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.OptionalInt;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -67,9 +68,42 @@
  *
  * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
  */
-public abstract class V3SchemeVerifier {
-    /** Hidden constructor to prevent instantiation. */
-    private V3SchemeVerifier() {}
+public class V3SchemeVerifier {
+    private final RunnablesExecutor mExecutor;
+    private final DataSource mApk;
+    private final ApkUtils.ZipSections mZipSections;
+    private final ApkSigningBlockUtils.Result mResult;
+    private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+    private final int mMinSdkVersion;
+    private final int mMaxSdkVersion;
+    private final int mBlockId;
+    private final OptionalInt mOptionalRotationMinSdkVersion;
+    private final boolean mFullVerification;
+
+    private ByteBuffer mApkSignatureSchemeV3Block;
+
+    private V3SchemeVerifier(
+            RunnablesExecutor executor,
+            DataSource apk,
+            ApkUtils.ZipSections zipSections,
+            Set<ContentDigestAlgorithm> contentDigestsToVerify,
+            ApkSigningBlockUtils.Result result,
+            int minSdkVersion,
+            int maxSdkVersion,
+            int blockId,
+            OptionalInt optionalRotationMinSdkVersion,
+            boolean fullVerification) {
+        mExecutor = executor;
+        mApk = apk;
+        mZipSections = zipSections;
+        mContentDigestsToVerify = contentDigestsToVerify;
+        mResult = result;
+        mMinSdkVersion = minSdkVersion;
+        mMaxSdkVersion = maxSdkVersion;
+        mBlockId = blockId;
+        mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+        mFullVerification = fullVerification;
+    }
 
     /**
      * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
@@ -84,7 +118,10 @@
      * this method returns a result with one or more errors and whose
      * {@code Result.verified == false}, or this method throws an exception.
      *
-     * @throws ApkFormatException if the APK is malformed
+     * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
+     * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
+     * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
+     *
      * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
      *         required cryptographic algorithm implementation is missing
      * @throws SignatureNotFoundException if no APK Signature Scheme v3
@@ -98,34 +135,11 @@
             int minSdkVersion,
             int maxSdkVersion)
             throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
-        ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
-                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
-        SignatureInfo signatureInfo =
-                ApkSigningBlockUtils.findSignature(apk, zipSections,
-                        V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
-
-        DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
-        DataSource centralDir =
-                apk.slice(
-                        signatureInfo.centralDirOffset,
-                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
-        ByteBuffer eocd = signatureInfo.eocd;
-
-        // v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
-        // platforms
-        if (minSdkVersion < AndroidSdkVersion.P) {
-            minSdkVersion = AndroidSdkVersion.P;
-        }
-
-        verify(executor,
-                beforeApkSigningBlock,
-                signatureInfo.signatureBlock,
-                centralDir,
-                eocd,
-                minSdkVersion,
-                maxSdkVersion,
-                result);
-        return result;
+        return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
+                .setRunnablesExecutor(executor)
+                .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+                .build()
+                .verify();
     }
 
     /**
@@ -134,33 +148,40 @@
      * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
      * int)} for more information about the contract of this method.
      *
-     * @param result result populated by this method with interesting information about the APK,
-     *        such as information about signers, and verification errors and warnings.
+     * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
+     *        APK, such as information about signers, and verification errors and warnings
      */
-    private static void verify(
-            RunnablesExecutor executor,
-            DataSource beforeApkSigningBlock,
-            ByteBuffer apkSignatureSchemeV3Block,
-            DataSource centralDir,
-            ByteBuffer eocd,
-            int minSdkVersion,
-            int maxSdkVersion,
-            ApkSigningBlockUtils.Result result)
-            throws IOException, NoSuchAlgorithmException {
-        Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
-        parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
-
-        if (result.containsErrors()) {
-            return;
+    public ApkSigningBlockUtils.Result verify()
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+        if (mApk == null || mZipSections == null) {
+            throw new IllegalStateException(
+                    "A non-null apk and zip sections must be specified to verify an APK's v3 "
+                            + "signatures");
         }
-        ApkSigningBlockUtils.verifyIntegrity(
-                executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+        SignatureInfo signatureInfo =
+                ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+        mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+
+        DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
+        DataSource centralDir =
+                mApk.slice(
+                        signatureInfo.centralDirOffset,
+                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+        ByteBuffer eocd = signatureInfo.eocd;
+
+        parseSigners();
+
+        if (mResult.containsErrors()) {
+            return mResult;
+        }
+        ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
+                mContentDigestsToVerify, mResult);
 
         // make sure that the v3 signers cover the entire targeted sdk version ranges and that the
         // longest SigningCertificateHistory, if present, corresponds to the newest platform
         // versions
         SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
-        for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
+        for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
             sortedSigners.put(signer.minSdkVersion, signer);
         }
 
@@ -170,7 +191,7 @@
         int lastLineageSize = 0;
 
         // while we're iterating through the signers, build up the list of lineages
-        List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
+        List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
 
         for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
             int currentMin = signer.minSdkVersion;
@@ -180,7 +201,7 @@
                 firstMin = currentMin;
             } else {
                 if (currentMin != lastMax + 1) {
-                    result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
+                    mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
                     break;
                 }
             }
@@ -190,7 +211,7 @@
             if (signer.signingCertificateLineage != null) {
                 int currLineageSize = signer.signingCertificateLineage.size();
                 if (currLineageSize < lastLineageSize) {
-                    result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+                    mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
                     break;
                 }
                 lastLineageSize = currLineageSize;
@@ -198,20 +219,24 @@
             }
         }
 
-        // make sure we support our desired sdk ranges
-        if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
-            result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
+        // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
+        // then the max level only needs to support up to that sdk version for rotation.
+        if (firstMin > mMinSdkVersion
+                || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
+                    ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
+            mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
         }
 
         try {
-             result.signingCertificateLineage =
+             mResult.signingCertificateLineage =
                      SigningCertificateLineage.consolidateLineages(lineages);
         } catch (IllegalArgumentException e) {
-            result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+            mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
         }
-        if (!result.containsErrors()) {
-            result.verified = true;
+        if (!mResult.containsErrors()) {
+            mResult.verified = true;
         }
+        return mResult;
     }
 
     /**
@@ -230,16 +255,49 @@
             ByteBuffer apkSignatureSchemeV3Block,
             Set<ContentDigestAlgorithm> contentDigestsToVerify,
             ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+        try {
+            new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
+                    .setResult(result)
+                    .setContentDigestsToVerify(contentDigestsToVerify)
+                    .setFullVerification(false)
+                    .build()
+                    .parseSigners();
+        } catch (IOException | SignatureNotFoundException e) {
+            // This should never occur since the apkSignatureSchemeV3Block was already provided.
+            throw new IllegalStateException("An exception was encountered when attempting to parse"
+                    + " the signers from the provided APK Signature Scheme v3 block", e);
+        }
+    }
+
+    /**
+     * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
+     * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
+     * returned {@link ApkSigningBlockUtils.Result}.
+     *
+     * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+     * However, this does not verify the integrity of the rest of the APK but rather simply reports
+     * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
+     *
+     * <p>This method adds one or more errors to the returned {@code Result} if a verification error
+     * is encountered when parsing the signers.
+     */
+    public ApkSigningBlockUtils.Result parseSigners()
+            throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
         ByteBuffer signers;
         try {
-            signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
+            if (mApkSignatureSchemeV3Block == null) {
+                SignatureInfo signatureInfo =
+                        ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+                mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+            }
+            signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
         } catch (ApkFormatException e) {
-            result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
-            return;
+            mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
+            return mResult;
         }
         if (!signers.hasRemaining()) {
-            result.addError(Issue.V3_SIG_NO_SIGNERS);
-            return;
+            mResult.addError(Issue.V3_SIG_NO_SIGNERS);
+            return mResult;
         }
 
         CertificateFactory certFactory;
@@ -255,15 +313,16 @@
             ApkSigningBlockUtils.Result.SignerInfo signerInfo =
                     new ApkSigningBlockUtils.Result.SignerInfo();
             signerInfo.index = signerIndex;
-            result.signers.add(signerInfo);
+            mResult.signers.add(signerInfo);
             try {
                 ByteBuffer signer = getLengthPrefixedSlice(signers);
-                parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
+                parseSigner(signer, certFactory, signerInfo);
             } catch (ApkFormatException | BufferUnderflowException e) {
                 signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
-                return;
+                return mResult;
             }
         }
+        return mResult;
     }
 
     /**
@@ -278,12 +337,9 @@
      * expected to be encountered on an Android platform version in the
      * {@code [minSdkVersion, maxSdkVersion]} range.
      */
-    private static void parseSigner(
-            ByteBuffer signerBlock,
-            CertificateFactory certFactory,
-            ApkSigningBlockUtils.Result.SignerInfo result,
-            Set<ContentDigestAlgorithm> contentDigestsToVerify)
-                    throws ApkFormatException, NoSuchAlgorithmException {
+    private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
+            ApkSigningBlockUtils.Result.SignerInfo result)
+            throws ApkFormatException, NoSuchAlgorithmException {
         ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
         byte[] signedDataBytes = new byte[signedData.remaining()];
         signedData.get(signedDataBytes);
@@ -372,7 +428,7 @@
                     return;
                 }
                 result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
-                contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+                mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
             } catch (InvalidKeyException | InvalidAlgorithmParameterException
                     | SignatureException e) {
                 result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
@@ -482,6 +538,7 @@
 
         // Parse the additional attributes block.
         int additionalAttributeCount = 0;
+        boolean rotationAttrFound = false;
         while (additionalAttributes.hasRemaining()) {
             additionalAttributeCount++;
             try {
@@ -509,6 +566,24 @@
                     } catch (Exception e) {
                         result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
                     }
+                } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
+                    rotationAttrFound = true;
+                    // API targeting for rotation was added with V3.1; if the maxSdkVersion
+                    // does not support v3.1 then ignore this attribute.
+                    if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
+                            && mFullVerification) {
+                        int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
+                                .order(ByteOrder.LITTLE_ENDIAN).getInt();
+                        if (mOptionalRotationMinSdkVersion.isPresent()) {
+                            int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
+                            if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
+                                result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
+                                    attrRotationMinSdkVersion, rotationMinSdkVersion);
+                            }
+                        } else {
+                            result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
+                        }
+                    }
                 } else {
                     result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
                 }
@@ -518,5 +593,168 @@
                 return;
             }
         }
+        if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
+            result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
+                    mOptionalRotationMinSdkVersion.getAsInt());
+        }
+    }
+
+    /** Builder of {@link V3SchemeVerifier} instances. */
+    public static class Builder {
+        private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+        private DataSource mApk;
+        private ApkUtils.ZipSections mZipSections;
+        private ByteBuffer mApkSignatureSchemeV3Block;
+        private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+        private ApkSigningBlockUtils.Result mResult;
+        private int mMinSdkVersion;
+        private int mMaxSdkVersion;
+        private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+        private boolean mFullVerification = true;
+        private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+        /**
+         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+         * verify the V3 signing block of the provided {@code apk} with the specified {@code
+         * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
+         */
+        public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
+                int maxSdkVersion) {
+            mApk = apk;
+            mZipSections = zipSections;
+            mMinSdkVersion = minSdkVersion;
+            mMaxSdkVersion = maxSdkVersion;
+        }
+
+        /**
+         * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+         * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
+         * apkSignatureSchemeV3Block}.
+         *
+         * <note>Full verification of the v3 signature is not possible when instantiating a new
+         * {@code V3SchemeVerifier} with this method.</note>
+         */
+        public Builder(ByteBuffer apkSignatureSchemeV3Block) {
+            mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
+        }
+
+        /**
+         * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
+         */
+        public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Sets the V3 {code blockId} to be verified in the provided APK.
+         *
+         * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+         * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+         */
+        public Builder setBlockId(int blockId) {
+            mBlockId = blockId;
+            return this;
+        }
+
+        /**
+         * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
+         * attribute.
+         *
+         * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
+         * block of an APK; in the case of multiple signers targeting different SDK versions in the
+         * v3.1 signing block, the minimum SDK version from all the signers should be used.
+         */
+        public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+            mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+            return this;
+        }
+
+        /**
+         * Sets the {@code result} instance to be used when returning verification results.
+         *
+         * <p>This method can be used when the caller already has a {@link
+         * ApkSigningBlockUtils.Result} and wants to store the verification results in this
+         * instance.
+         */
+        public Builder setResult(ApkSigningBlockUtils.Result result) {
+            mResult = result;
+            return this;
+        }
+
+        /**
+         * Sets the instance to be used to store the {@code contentDigestsToVerify}.
+         *
+         * <p>This method can be used when the caller needs access to the {@code
+         * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
+         */
+        public Builder setContentDigestsToVerify(
+                Set<ContentDigestAlgorithm> contentDigestsToVerify) {
+            mContentDigestsToVerify = contentDigestsToVerify;
+            return this;
+        }
+
+        /**
+         * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
+         * from this instance.
+         *
+         * <note>{@link #verify()} will always verify the content digests for the APK, but this
+         * allows verification of the rotation minimum SDK version stripping attribute to be skipped
+         * for scenarios where this value may not have been parsed from a V3.1 signing block (such
+         * as when only {@link #parseSigners()} will be invoked.</note>
+         */
+        public Builder setFullVerification(boolean fullVerification) {
+            mFullVerification = fullVerification;
+            return this;
+        }
+
+        /**
+         * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
+         * {@code Builder}.
+         */
+        public V3SchemeVerifier build() {
+            int sigSchemeVersion;
+            switch (mBlockId) {
+                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
+                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+                    mMinSdkVersion = Math.max(mMinSdkVersion,
+                            V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
+                    break;
+                case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
+                    sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+                    // V3.1 supports targeting an SDK version later than that of the initial release
+                    // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
+                    // rest of the range.
+                    mMinSdkVersion = mMaxSdkVersion;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
+                                    mBlockId));
+            }
+            if (mResult == null) {
+                mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
+            }
+            if (mContentDigestsToVerify == null) {
+                mContentDigestsToVerify = new HashSet<>(1);
+            }
+
+            V3SchemeVerifier verifier = new V3SchemeVerifier(
+                    mExecutor,
+                    mApk,
+                    mZipSections,
+                    mContentDigestsToVerify,
+                    mResult,
+                    mMinSdkVersion,
+                    mMaxSdkVersion,
+                    mBlockId,
+                    mOptionalRotationMinSdkVersion,
+                    mFullVerification);
+            if (mApkSignatureSchemeV3Block != null) {
+                verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
+            }
+            return verifier;
+        }
     }
 }
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 87eae48..a8166e6 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -56,4 +56,13 @@
 
     /** Android R. */
     public static final int R = 30;
+
+    /** Android S. */
+    public static final int S = 31;
+
+    /** Android Sv2. */
+    public static final int Sv2 = 32;
+
+    /** Android T. */
+    public static final int T = 33;
 }
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index d799201..436c8dd 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -21,6 +21,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -89,9 +90,9 @@
 
     // All signers with the same prefix and an _X suffix were signed with the private key of the
     // (X-1) signer.
-    private static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
-    private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
-    private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
+    static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
+    static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
+    static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
 
     private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
 
@@ -1499,6 +1500,187 @@
                 EC_P256_SIGNER_RESOURCE_NAME);
     }
 
+    @Test
+    public void testSetMinSdkVersionForRotation_lessThanT_noV31Block() throws Exception {
+        // The V3.1 signing block is intended to allow APK signing key rotation to target T+, but
+        // a minimum SDK version can be explicitly set for rotation; if it is less than T than
+        // the rotated key will be included in the V3.0 block.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApkMinRotationP = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result resultMinRotationP = verify(signedApkMinRotationP, null);
+        // The V3.1 signature scheme was introduced in T; specifying an older SDK version as the
+        // minimum for rotation should cause the APK to still be signed with rotation in the V3.0
+        // signing block.
+        File signedApkMinRotationS = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.S)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result resultMinRotationS = verify(signedApkMinRotationP, null);
+
+        assertVerified(resultMinRotationP);
+        assertFalse(resultMinRotationP.isVerifiedUsingV31Scheme());
+        assertResultContainsSigners(resultMinRotationP, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertVerified(resultMinRotationS);
+        assertFalse(resultMinRotationS.isVerifiedUsingV31Scheme());
+        assertResultContainsSigners(resultMinRotationS, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testSetMinSdkVersionForRotation_TAndLater_v31Block() throws Exception {
+        // When T or later is specified as the minimum SDK version for rotation, then a new V3.1
+        // signing block should be created with the new rotated key, and the V3.0 signing block
+        // should still be signed with the original key.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+            Arrays.asList(
+                getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+            Resources.toSigningCertificateLineage(
+                ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApkMinRotationT = sign("original.apk",
+            new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                .setV1SigningEnabled(true)
+                .setV2SigningEnabled(true)
+                .setV3SigningEnabled(true)
+                .setV4SigningEnabled(false)
+                .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+                .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result resultMinRotationT = verify(signedApkMinRotationT, null);
+        // The API level for a release after T is not yet defined, so for now treat it as T + 1.
+        File signedApkMinRotationU = sign("original.apk",
+            new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                .setV1SigningEnabled(true)
+                .setV2SigningEnabled(true)
+                .setV3SigningEnabled(true)
+                .setV4SigningEnabled(false)
+                .setMinSdkVersionForRotation(AndroidSdkVersion.T + 1)
+                .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result resultMinRotationU = verify(signedApkMinRotationU, null);
+
+        assertVerified(resultMinRotationT);
+        assertTrue(resultMinRotationT.isVerifiedUsingV31Scheme());
+        assertResultContainsSigners(resultMinRotationT, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+            SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertV31SignerTargetsMinApiLevel(resultMinRotationT, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+            AndroidSdkVersion.T);
+        assertVerified(resultMinRotationU);
+        assertTrue(resultMinRotationU.isVerifiedUsingV31Scheme());
+        assertResultContainsSigners(resultMinRotationU, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+            SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertV31SignerTargetsMinApiLevel(resultMinRotationU, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+            AndroidSdkVersion.T + 1);
+    }
+
+    @Test
+    public void testSetMinSdkVersionForRotation_targetTNoOriginalSigner_fails() throws Exception {
+        // Similar to the V1 and V2 signatures schemes, if an app is targeting P or later with
+        // rotation targeting T, the original signer must be provided so that it can be used in the
+        // V3.0 signing block; if it is not provided the signer should throw an Exception.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+            .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+            Resources.toSigningCertificateLineage(
+                ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        assertThrows(IllegalArgumentException.class, () ->
+            sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                    .setV1SigningEnabled(false)
+                    .setV2SigningEnabled(false)
+                    .setV3SigningEnabled(true)
+                    .setV4SigningEnabled(false)
+                    .setMinSdkVersion(28)
+                    .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+                    .setSigningCertificateLineage(lineage)));
+    }
+
+    @Test
+    public void testSetMinSdkVersionForRotation_targetTAndApkMinSdkT_onlySignsV3Block()
+            throws Exception {
+        // A V3.1 signing block should only exist alongside a V3.0 signing block; if an APK's
+        // min SDK version is greater than or equal to the SDK version for rotation then the
+        // original signer should not be required, and the rotated signing key should be in
+        // a V3.0 signing block.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+            .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+            Resources.toSigningCertificateLineage(
+                ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApk = sign("original.apk",
+            new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                .setV1SigningEnabled(false)
+                .setV2SigningEnabled(false)
+                .setV3SigningEnabled(true)
+                .setV4SigningEnabled(false)
+                .setMinSdkVersion(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+                .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+                .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verifyForMinSdkVersion(signedApk,
+            V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT);
+
+        assertVerified(result);
+        assertFalse(result.isVerifiedUsingV31Scheme());
+        assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+            SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testSetMinSdkVersionForRotation_targetTWithSourceStamp_noWarnings()
+            throws Exception {
+        // Source stamp verification will report a warning if a stamp signature is not found for any
+        // of the APK Signature Schemes used to sign the APK. This test verifies an APK signed with
+        // a rotated key in the v3.1 block and a source stamp successfully verifies, including the
+        // source stamp, without any warnings.
+        ApkSigner.SignerConfig rsa2048OriginalSignerConfig = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        rsa2048OriginalSignerConfig,
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+                        .setSigningCertificateLineage(lineage)
+                        .setSourceStampSignerConfig(rsa2048OriginalSignerConfig));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertSourceStampVerified(signedApk, result);
+    }
+
     /**
      * Asserts the provided {@code signedApk} contains a signature block with the expected
      * {@code byte[]} value and block ID as specified in the {@code expectedBlock}.
@@ -1527,8 +1709,20 @@
      * Asserts the provided verification {@code result} contains the expected {@code signers} for
      * each scheme that was used to verify the APK's signature.
      */
-    private static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
+    static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
             throws Exception {
+        assertResultContainsSigners(result, false, signers);
+    }
+
+    /**
+     * Asserts the provided verification {@code result} contains the expected {@code signers} for
+     * each scheme that was used to verify the APK's signature; if {@code rotationExpected} is set
+     * to {@code true}, then the first element in {@code signers} is treated as the expected
+     * original signer for any V1, V2, and V3 (where applicable) signatures, and the last element
+     * is the rotated expected signer for V3+.
+     */
+    static void assertResultContainsSigners(ApkVerifier.Result result,
+        boolean rotationExpected, String... signers) throws Exception {
         // A result must be successfully verified before verifying any of the result's signers.
         assertTrue(result.isVerified());
 
@@ -1537,16 +1731,27 @@
             ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
             expectedSigners.addAll(signerConfig.getCertificates());
         }
+        // If rotation is expected then the V1 and V2 signature should only be signed by the
+        // original signer.
+        List<X509Certificate> expectedV1Signers =
+            rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+        List<X509Certificate> expectedV2Signers =
+            rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+        // V3 only supports a single signer; if rotation is not expected or the V3.1 block contains
+        // the rotated signing key then the expected V3.0 signer should be the original signer.
+        List<X509Certificate> expectedV3Signers =
+            !rotationExpected || result.isVerifiedUsingV31Scheme()
+                ? List.of(expectedSigners.get(0))
+                : List.of(expectedSigners.get(expectedSigners.size() - 1));
 
         if (result.isVerifiedUsingV1Scheme()) {
             Set<X509Certificate> v1Signers = new HashSet<>();
             for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
                 v1Signers.add(signer.getCertificate());
             }
-            assertEquals(expectedSigners.size(), v1Signers.size());
-            assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedSigners)
+            assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedV1Signers)
                             + ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers),
-                    v1Signers.containsAll(expectedSigners));
+                    v1Signers.containsAll(expectedV1Signers));
         }
 
         if (result.isVerifiedUsingV2Scheme()) {
@@ -1554,10 +1759,9 @@
             for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
                 v2Signers.add(signer.getCertificate());
             }
-            assertEquals(expectedSigners.size(), v2Signers.size());
-            assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedSigners)
+            assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedV2Signers)
                             + ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers),
-                    v2Signers.containsAll(expectedSigners));
+                    v2Signers.containsAll(expectedV2Signers));
         }
 
         if (result.isVerifiedUsingV3Scheme()) {
@@ -1565,11 +1769,47 @@
             for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
                 v3Signers.add(signer.getCertificate());
             }
-            assertEquals(expectedSigners.size(), v3Signers.size());
-            assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedSigners)
+            assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedV3Signers)
                             + ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers),
-                    v3Signers.containsAll(expectedSigners));
+                    v3Signers.containsAll(expectedV3Signers));
         }
+
+        if (result.isVerifiedUsingV31Scheme()) {
+            Set<X509Certificate> v31Signers = new HashSet<>();
+            for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+                v31Signers.add(signer.getCertificate());
+            }
+            // V3.1 only supports specifying signatures with a rotated signing key; if a V3.1
+            // signing block was verified then ensure it contains the expected rotated signer.
+            List<X509Certificate> expectedV31Signers = List
+                .of(expectedSigners.get(expectedSigners.size() - 1));
+            assertTrue("Expected V3.1 signers: " + getAllSubjectNamesFrom(expectedV31Signers)
+                    + ", actual V3.1 signers: " + getAllSubjectNamesFrom(v31Signers),
+                v31Signers.containsAll(expectedV31Signers));
+        }
+    }
+
+    /**
+     * Asserts the provided {@code result} contains the expected {@code signer} targeting
+     * {@code minSdkVersion} as the minimum version for rotation.
+     */
+    static void assertV31SignerTargetsMinApiLevel(ApkVerifier.Result result, String signer,
+        int minSdkVersion) throws Exception {
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        ApkSigner.SignerConfig expectedSignerConfig = getDefaultSignerConfigFromResources(signer);
+
+        for (ApkVerifier.Result.V3SchemeSignerInfo signerConfig : result.getV31SchemeSigners()) {
+            if (signerConfig.getCertificates()
+                .containsAll(expectedSignerConfig.getCertificates())) {
+                assertEquals("The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates())
+                        + ", is expected to target SDK version " + minSdkVersion
+                        + ", instead it is targeting " + signerConfig.getMinSdkVersion(),
+                    minSdkVersion, signerConfig.getMinSdkVersion());
+                return;
+            }
+        }
+        fail("Did not find the expected signer, " + getAllSubjectNamesFrom(
+            expectedSignerConfig.getCertificates()));
     }
 
     /**
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 5a7c64d..72e7b9b 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -16,6 +16,10 @@
 
 package com.android.apksig;
 
+import static com.android.apksig.ApkSignerTest.FIRST_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.assertResultContainsSigners;
+import static com.android.apksig.ApkSignerTest.assertV31SignerTargetsMinApiLevel;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -25,6 +29,7 @@
 import com.android.apksig.ApkVerifier.IssueWithParams;
 import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus;
 import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.HexEncoding;
 import com.android.apksig.internal.util.Resources;
@@ -1348,6 +1353,67 @@
         }
     }
 
+    @Test
+    public void verifyV31_rotationTarget34_containsExpectedSigners() throws Exception {
+        // This test verifies an APK targeting a specific SDK version for rotation properly reports
+        // that version for the rotated signer in the v3.1 block, and all other signing blocks
+        // use the original signing key.
+        ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+
+        assertVerified(result);
+        assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+            SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+    }
+
+    @Test
+    public void verifyV31_missingStrippingAttr_warningReported() throws Exception {
+        // The v3.1 signing block supports targeting SDK versions; to protect against these target
+        // versions being modified the v3 signer contains a stripping protection attribute with the
+        // SDK version on which rotation should be applied. This test verifies a warning is reported
+        // when this attribute is not present in the v3 signer.
+        ApkVerifier.Result result = verify("v31-tgt-33-no-v3-attr.apk");
+
+        assertVerificationWarning(result, Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING);
+    }
+
+    @Test
+    public void verifyV31_strippingAttrMismatch_errorReportedOnSupportedVersions()
+            throws Exception {
+        // This test verifies if the stripping protection attribute does not properly match the
+        // minimum SDK version on which rotation is supported then the APK should fail verification.
+        ApkVerifier.Result result = verify("v31-tgt-34-v3-attr-value-33.apk");
+        assertVerificationFailure(result, Issue.V31_ROTATION_MIN_SDK_MISMATCH);
+
+        // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+        // and the v3.1 signing block.
+        result = verifyForMaxSdkVersion("v31-tgt-34-v3-attr-value-33.apk",
+            V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+        assertVerified(result);
+    }
+
+    @Test
+    public void verifyV31_missingV31Block_errorReportedOnSupportedVersions() throws Exception {
+        // This test verifies if the stripping protection attribute contains a value for rotation
+        // but a v3.1 signing block was not found then the APK should fail verification.
+        ApkVerifier.Result result = verify("v31-block-stripped-v3-attr-value-33.apk");
+        assertVerificationFailure(result, Issue.V31_BLOCK_MISSING);
+
+        // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+        // and the v3.1 signing block.
+        result = verifyForMaxSdkVersion("v31-block-stripped-v3-attr-value-33.apk",
+            V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+        assertVerified(result);
+    }
+
+    @Test
+    public void verify31_v31BlockWithoutV3Block_reportsError() throws Exception {
+        // A v3.1 block must always exist alongside a v3.0 block; if an APK's minSdkVersion is the
+        // same as the version supporting rotation then it should be written to a v3.0 block.
+        ApkVerifier.Result result = verify("v31-tgt-33-no-v3-block.apk");
+        assertVerificationFailure(result, Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+    }
+
     private ApkVerifier.Result verify(String apkFilenameInResources)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, null);
@@ -1486,13 +1552,27 @@
     }
 
     static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
-        if (result.isVerified()) {
+        assertVerificationIssue(result, expectedIssue, true);
+    }
+
+    static void assertVerificationWarning(ApkVerifier.Result result, Issue expectedIssue) {
+        assertVerificationIssue(result, expectedIssue, false);
+    }
+
+    /**
+     * Asserts the provided {@code result} contains the {@code expectedIssue}; if {@code
+     * verifyError} is set to {@code true} then the specified {@link Issue} will be expected as an
+     * error, otherwise it will be expected as a warning.
+     */
+    private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue,
+        boolean verifyError) {
+        if (result.isVerified() && verifyError) {
             fail("APK verification succeeded instead of failing with " + expectedIssue);
             return;
         }
 
         StringBuilder msg = new StringBuilder();
-        for (IssueWithParams issue : result.getErrors()) {
+        for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) {
             if (expectedIssue.equals(issue.getIssue())) {
                 return;
             }
@@ -1503,7 +1583,8 @@
         }
         for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
             String signerName = signer.getName();
-            for (ApkVerifier.IssueWithParams issue : signer.getErrors()) {
+            for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors()
+                    : signer.getWarnings())) {
                 if (expectedIssue.equals(issue.getIssue())) {
                     return;
                 }
@@ -1520,7 +1601,8 @@
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
             String signerName = "signer #" + (signer.getIndex() + 1);
-            for (IssueWithParams issue : signer.getErrors()) {
+            for (IssueWithParams issue : (verifyError ? signer.getErrors()
+                    : signer.getWarnings())) {
                 if (expectedIssue.equals(issue.getIssue())) {
                     return;
                 }
@@ -1535,7 +1617,8 @@
         }
         for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
             String signerName = "signer #" + (signer.getIndex() + 1);
-            for (IssueWithParams issue : signer.getErrors()) {
+            for (IssueWithParams issue : (verifyError ? signer.getErrors()
+                    : signer.getWarnings())) {
                 if (expectedIssue.equals(issue.getIssue())) {
                     return;
                 }
@@ -1548,6 +1631,22 @@
                         .append(issue);
             }
         }
+        for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+            String signerName = "signer #" + (signer.getIndex() + 1);
+            for (IssueWithParams issue : (verifyError ? signer.getErrors()
+                : signer.getWarnings())) {
+                if (expectedIssue.equals(issue.getIssue())) {
+                    return;
+                }
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK Signature Scheme v3.1 signer ")
+                    .append(signerName)
+                    .append(": ")
+                    .append(issue);
+            }
+        }
 
         fail(
                 "APK failed verification for the wrong reason"
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index d5dc71d..80b9641 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -397,6 +397,21 @@
         assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
     }
 
+    @Test
+    public void testLineageFromAPKWithV31BlockContainsExpectedSigners() throws Exception {
+        SignerConfig firstSigner = getSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        SignerConfig secondSigner = getSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<SignerConfig> expectedSigners = Arrays.asList(firstSigner, secondSigner);
+        DataSource apkDataSource = Resources.toDataSource(getClass(),
+                "v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+        SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
+                apkDataSource);
+        assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
+
+    }
+
     @Test(expected = ApkFormatException.class)
     public void testLineageFromAPKWithInvalidZipCDSizeFails() throws Exception {
         // This test verifies that attempting to read the lineage from an APK where the zip
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 965f901..a273b0c 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-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
index 13a3bc9..2a0d383 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-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
index 36f4825..4a4cda9 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-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
index f6b60d6..376b0e5 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-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
index b0debf6..25ffa5b 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-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
index f8b4171..341d7ba 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-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
index 02ecee0..5636723 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-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
index 08538a0..4f21b4c 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-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
index 9bcbc7a..7514d20 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/v31-block-stripped-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
new file mode 100644
index 0000000..d091075
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
new file mode 100644
index 0000000..ccf59d4
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
new file mode 100644
index 0000000..284cd7a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
new file mode 100644
index 0000000..1c797a7
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
new file mode 100644
index 0000000..ab87fcc
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
Binary files differ