[automerger skipped] Limit the number of supported v1 and v2 signers am: dae4126300 am: ae4b19d6db am: eb57dca6c1 am: 2d9be54d52 am: b8f17653e4 -s ours am: c792b59932 -s ours am: 6c5ea98b94 -s ours am: 27f77f53c7 -s ours am: 1ebb6035e4 -s ours

am skip reason: Merged-In I4fd8f603385dd7e59c81695a75fe6df9a116185d with SHA-1 6be64b9339 is already in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/tools/apksig/+/22389785

Change-Id: Id7d80af3ad00b97ffe0930bb2dfb9275be512b0e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index bd34ad1..ff64b1c 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -146,6 +146,7 @@
         boolean v3SigningEnabled = true;
         boolean v4SigningEnabled = true;
         boolean forceSourceStampOverwrite = false;
+        boolean sourceStampTimestampEnabled = true;
         boolean alignFileSize = false;
         boolean verityEnabled = false;
         boolean debuggableApkPermitted = true;
@@ -199,6 +200,8 @@
                 v4SigningFlagFound = true;
             } else if ("force-stamp-overwrite".equals(optionName)) {
                 forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true);
+            } else if ("stamp-timestamp-enabled".equals(optionName)) {
+                sourceStampTimestampEnabled = optionsParser.getOptionalBooleanValue(true);
             } else if ("align-file-size".equals(optionName)) {
                 alignFileSize = true;
             } else if ("verity-enabled".equals(optionName)) {
@@ -210,6 +213,13 @@
                     signers.add(signerParams);
                     signerParams = new SignerParams();
                 }
+            } else if ("signer-for-min-sdk-version".equals(optionName)) {
+                if (!signerParams.isEmpty()) {
+                    signers.add(signerParams);
+                    signerParams = new SignerParams();
+                }
+                signerParams.setMinSdkVersion(optionsParser.getRequiredIntValue(
+                        "Mininimum API Level for signing config"));
             } else if ("ks".equals(optionName)) {
                 signerParams.setKeystoreFile(optionsParser.getRequiredValue("KeyStore file"));
             } else if ("ks-key-alias".equals(optionName)) {
@@ -250,8 +260,12 @@
                 signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file"));
             } else if ("cert".equals(optionName)) {
                 signerParams.setCertFile(optionsParser.getRequiredValue("Certificate file"));
+            } else if ("signer-lineage".equals(optionName)) {
+                File lineageFile = new File(
+                        optionsParser.getRequiredValue("Lineage file for signing config"));
+                signerParams.setSigningCertificateLineage(getLineageFromInputFile(lineageFile));
             } else if ("lineage".equals(optionName)) {
-                File lineageFile = new File(optionsParser.getRequiredValue("Lineage File"));
+                File lineageFile = new File(optionsParser.getRequiredValue("Lineage file"));
                 lineage = getLineageFromInputFile(lineageFile);
             } else if ("v".equals(optionName) || "verbose".equals(optionName)) {
                 verbose = optionsParser.getOptionalBooleanValue(true);
@@ -374,6 +388,7 @@
                         .setV3SigningEnabled(v3SigningEnabled)
                         .setV4SigningEnabled(v4SigningEnabled)
                         .setForceSourceStampOverwrite(forceSourceStampOverwrite)
+                        .setSourceStampTimestampEnabled(sourceStampTimestampEnabled)
                         .setAlignFileSize(alignFileSize)
                         .setVerityEnabled(verityEnabled)
                         .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
@@ -448,11 +463,15 @@
         } else {
             throw new RuntimeException("Neither KeyStore key alias nor private key file available");
         }
-        ApkSigner.SignerConfig signerConfig =
-                new ApkSigner.SignerConfig.Builder(
-                        v1SigBasename, signer.getPrivateKey(), signer.getCerts(),
-                        deterministicDsaSigning)
-                        .build();
+        ApkSigner.SignerConfig.Builder signerConfigBuilder = new ApkSigner.SignerConfig.Builder(
+                v1SigBasename, signer.getPrivateKey(), signer.getCerts(), deterministicDsaSigning);
+        SigningCertificateLineage lineage = signer.getSigningCertificateLineage();
+        int minSdkVersion = signer.getMinSdkVersion();
+        if (minSdkVersion > 0) {
+            signerConfigBuilder.setLineageForMinSdkVersion(lineage, minSdkVersion);
+        }
+        ApkSigner.SignerConfig signerConfig = signerConfigBuilder.build();
+
         return signerConfig;
     }
 
@@ -708,6 +727,9 @@
             for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) {
                 warningsOut.println("WARNING: SourceStamp: " + warning);
             }
+            for (ApkVerifier.IssueWithParams infoMessage : sourceStampInfo.getInfoMessages()) {
+                System.out.println("INFO: SourceStamp: " + infoMessage);
+            }
         }
 
         if (!verified) {
diff --git a/src/apksigner/java/com/android/apksigner/SignerParams.java b/src/apksigner/java/com/android/apksigner/SignerParams.java
index 515cd41..a50cc1d 100644
--- a/src/apksigner/java/com/android/apksigner/SignerParams.java
+++ b/src/apksigner/java/com/android/apksigner/SignerParams.java
@@ -16,8 +16,10 @@
 
 package com.android.apksigner;
 
+import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
 import com.android.apksig.internal.util.X509CertificateUtils;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -71,6 +73,9 @@
     private final SignerCapabilities.Builder signerCapabilitiesBuilder =
             new SignerCapabilities.Builder();
 
+    private int minSdkVersion;
+    private SigningCertificateLineage signingCertificateLineage;
+
     public String getName() {
         return name;
     }
@@ -151,6 +156,22 @@
         return signerCapabilitiesBuilder;
     }
 
+    public int getMinSdkVersion() {
+        return minSdkVersion;
+    }
+
+    public void setMinSdkVersion(int minSdkVersion) {
+        this.minSdkVersion = minSdkVersion;
+    }
+
+    public SigningCertificateLineage getSigningCertificateLineage() {
+        return signingCertificateLineage;
+    }
+
+    public void setSigningCertificateLineage(SigningCertificateLineage lineage) {
+        this.signingCertificateLineage = lineage;
+    }
+
     boolean isEmpty() {
         return (name == null)
                 && (keystoreFile == null)
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index dc5f6cc..a116be6 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -139,6 +139,18 @@
 --stamp-signer        The signing information for the signer of the source stamp
                       to be included in the APK.
 
+--signer-for-min-sdk-version <SDK> Requires an int value indicating the minimum
+                      SDK version for which this signing config should be used
+                      to produce the APK's signature. The value should be >= 28
+                      (Android P), and any value <= 32 will apply to Android P
+                      through Sv2 (SDK versions 28 - 32); since the V3.0
+                      signature scheme does not support verified SDK version
+                      targeting, only a single signing config <= 32 can be
+                      specified.
+
+--signer-lineage      The lineage to be used for the current SDK targeted
+                      signing config.
+
         PER-SIGNER SIGNING KEY & CERTIFICATE OPTIONS
 There are two ways to provide the signer's private key and certificate: (1) Java
 KeyStore (see --ks), or (2) private key file in PKCS #8 format and certificate
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index f225ae9..60c18d4 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -25,6 +25,7 @@
 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.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferDataSource;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.EocdRecord;
@@ -93,6 +94,7 @@
     private final SignerConfig mSourceStampSignerConfig;
     private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
     private final boolean mForceSourceStampOverwrite;
+    private final boolean mSourceStampTimestampEnabled;
     private final Integer mMinSdkVersion;
     private final int mRotationMinSdkVersion;
     private final boolean mRotationTargetsDevRelease;
@@ -125,6 +127,7 @@
             SignerConfig sourceStampSignerConfig,
             SigningCertificateLineage sourceStampSigningCertificateLineage,
             boolean forceSourceStampOverwrite,
+            boolean sourceStampTimestampEnabled,
             Integer minSdkVersion,
             int rotationMinSdkVersion,
             boolean rotationTargetsDevRelease,
@@ -151,6 +154,7 @@
         mSourceStampSignerConfig = sourceStampSignerConfig;
         mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
         mForceSourceStampOverwrite = forceSourceStampOverwrite;
+        mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
         mMinSdkVersion = minSdkVersion;
         mRotationMinSdkVersion = rotationMinSdkVersion;
         mRotationTargetsDevRelease = rotationTargetsDevRelease;
@@ -294,13 +298,20 @@
             List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
                     new ArrayList<>(mSignerConfigs.size());
             for (SignerConfig signerConfig : mSignerConfigs) {
-                engineSignerConfigs.add(
+                DefaultApkSignerEngine.SignerConfig.Builder signerConfigBuilder =
                         new DefaultApkSignerEngine.SignerConfig.Builder(
-                                        signerConfig.getName(),
-                                        signerConfig.getPrivateKey(),
-                                        signerConfig.getCertificates(),
-                                        signerConfig.getDeterministicDsaSigning())
-                                .build());
+                                signerConfig.getName(),
+                                signerConfig.getPrivateKey(),
+                                signerConfig.getCertificates(),
+                                signerConfig.getDeterministicDsaSigning());
+                int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+                SigningCertificateLineage signerLineage =
+                        signerConfig.getSigningCertificateLineage();
+                if (signerMinSdkVersion > 0) {
+                    signerConfigBuilder.setLineageForMinSdkVersion(signerLineage,
+                            signerMinSdkVersion);
+                }
+                engineSignerConfigs.add(signerConfigBuilder.build());
             }
             DefaultApkSignerEngine.Builder signerEngineBuilder =
                     new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
@@ -324,6 +335,7 @@
                                         mSourceStampSignerConfig.getCertificates(),
                                         mSourceStampSignerConfig.getDeterministicDsaSigning())
                                 .build());
+                signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled);
             }
             if (mSourceStampSigningCertificateLineage != null) {
                 signerEngineBuilder.setSourceStampSigningCertificateLineage(
@@ -1014,18 +1026,19 @@
         private final String mName;
         private final PrivateKey mPrivateKey;
         private final List<X509Certificate> mCertificates;
-        private boolean mDeterministicDsaSigning;
+        private final boolean mDeterministicDsaSigning;
+        private final int mMinSdkVersion;
+        private final SigningCertificateLineage mSigningCertificateLineage;
 
-        private SignerConfig(
-                String name,
-                PrivateKey privateKey,
-                List<X509Certificate> certificates,
-                boolean deterministicDsaSigning) {
-            mName = name;
-            mPrivateKey = privateKey;
-            mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
-            mDeterministicDsaSigning = deterministicDsaSigning;
+        private SignerConfig(Builder builder) {
+            mName = builder.mName;
+            mPrivateKey = builder.mPrivateKey;
+            mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+            mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+            mMinSdkVersion = builder.mMinSdkVersion;
+            mSigningCertificateLineage = builder.mSigningCertificateLineage;
         }
+
         /** Returns the name of this signer. */
         public String getName() {
             return mName;
@@ -1044,7 +1057,6 @@
             return mCertificates;
         }
 
-
         /**
          * If this signer is a DSA signer, whether or not the signing is done deterministically.
          */
@@ -1052,6 +1064,16 @@
             return mDeterministicDsaSigning;
         }
 
+        /** Returns the minimum SDK version for which this signer should be used. */
+        public int getMinSdkVersion() {
+            return mMinSdkVersion;
+        }
+
+        /** Returns the {@link SigningCertificateLineage} for this signer. */
+        public SigningCertificateLineage getSigningCertificateLineage() {
+            return mSigningCertificateLineage;
+        }
+
         /** Builder of {@link SignerConfig} instances. */
         public static class Builder {
             private final String mName;
@@ -1059,6 +1081,9 @@
             private final List<X509Certificate> mCertificates;
             private final boolean mDeterministicDsaSigning;
 
+            private int mMinSdkVersion;
+            private SigningCertificateLineage mSigningCertificateLineage;
+
             /**
              * Constructs a new {@code Builder}.
              *
@@ -1100,13 +1125,71 @@
                 mDeterministicDsaSigning = deterministicDsaSigning;
             }
 
+            /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+            public Builder setMinSdkVersion(int minSdkVersion) {
+                return setLineageForMinSdkVersion(null, minSdkVersion);
+            }
+
+            /**
+             * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+             * (API level) for which the provided {@code lineage} (where applicable) should be used
+             * to produce the APK's signature. This method is useful if callers want to specify a
+             * particular rotated signer or lineage with restricted capabilities for later
+             * platform releases.
+             *
+             * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+             * signing lineages with capabilities; only an app's original signer(s) can be used for
+             * the V1 and V2 signature blocks. Because of this, only a value of {@code
+             * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+             * introduced can be specified.
+             *
+             * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+             * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+             * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+             * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+             * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+             * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+             *
+             * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+             *                minSdkVersion}
+             * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+             *                      should be used
+             * @return this {@code Builder} instance
+             *
+             * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+             * certificate provided in the constructor is not in the specified {@code lineage}.
+             */
+            public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+                    int minSdkVersion) {
+                if (minSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalArgumentException(
+                            "SDK targeted signing config is only supported with the V3 signature "
+                                    + "scheme on Android P (SDK version "
+                                    + AndroidSdkVersion.P + ") and later");
+                }
+                if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    minSdkVersion = AndroidSdkVersion.P;
+                }
+                mMinSdkVersion = minSdkVersion;
+                // If a lineage is provided, ensure the signing certificate for this signer is in
+                // the lineage; in the case of multiple signing certificates, the first is always
+                // used in the lineage.
+                if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+                    throw new IllegalArgumentException(
+                            "The provided lineage does not contain the signing certificate, "
+                                    + mCertificates.get(0).getSubjectDN()
+                                    + ", for this SignerConfig");
+                }
+                mSigningCertificateLineage = lineage;
+                return this;
+            }
+
             /**
              * Returns a new {@code SignerConfig} instance configured based on the configuration of
              * this builder.
              */
             public SignerConfig build() {
-                return new SignerConfig(mName, mPrivateKey, mCertificates,
-                        mDeterministicDsaSigning);
+                return new SignerConfig(this);
             }
         }
     }
@@ -1128,6 +1211,7 @@
         private SignerConfig mSourceStampSignerConfig;
         private SigningCertificateLineage mSourceStampSigningCertificateLineage;
         private boolean mForceSourceStampOverwrite = false;
+        private boolean mSourceStampTimestampEnabled = true;
         private boolean mV1SigningEnabled = true;
         private boolean mV2SigningEnabled = true;
         private boolean mV3SigningEnabled = true;
@@ -1229,6 +1313,15 @@
         }
 
         /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
          * Sets the APK to be signed.
          *
          * @see #setInputApk(DataSource)
@@ -1652,6 +1745,7 @@
                     mSourceStampSignerConfig,
                     mSourceStampSigningCertificateLineage,
                     mForceSourceStampOverwrite,
+                    mSourceStampTimestampEnabled,
                     mMinSdkVersion,
                     mRotationMinSdkVersion,
                     mRotationTargetsDevRelease,
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 8ae5f78..078996a 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -22,15 +22,23 @@
 import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP;
 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.ApkVerifier.Result.V2SchemeSignerInfo;
+import com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo;
+import com.android.apksig.SigningCertificateLineage.SignerConfig;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.apk.ApkSigResult;
 import com.android.apksig.internal.apk.ApkSignerInfo;
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.Result.SignerInfo.ContentDigest;
 import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
@@ -56,7 +64,9 @@
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -82,6 +92,10 @@
  */
 public class ApkVerifier {
 
+    private static final Set<Issue> LINEAGE_RELATED_ISSUES = new HashSet<>(Arrays.asList(
+        Issue.V3_SIG_MALFORMED_LINEAGE, Issue.V3_INCONSISTENT_LINEAGES,
+        Issue.V3_SIG_POR_DID_NOT_VERIFY, Issue.V3_SIG_POR_CERT_MISMATCH));
+
     private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
             loadSupportedApkSigSchemeNames();
 
@@ -215,12 +229,12 @@
                             .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
                             .build()
                             .verify();
-                    foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+                    foundApkSigSchemeIds.add(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,
+                            VERSION_APK_SIGNATURE_SCHEME_V31,
                             getApkContentDigestsFromSigningSchemeResult(v31Result));
                 } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
                     // v3.1 signature not required
@@ -229,8 +243,10 @@
                     return result;
                 }
             }
-            // Android P and newer attempts to verify APKs using APK Signature Scheme v3
-            if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) {
+            // Android P and newer attempts to verify APKs using APK Signature Scheme v3; since a
+            // V3.1 block should only be written with a V3.0 block, always perform the V3.0 check
+            // if the minSdkVersion supports V3.0.
+            if (maxSdkVersion >= AndroidSdkVersion.P) {
                 try {
                     V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
                             zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
@@ -251,7 +267,7 @@
                     // 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)) {
+                            VERSION_APK_SIGNATURE_SCHEME_V31)) {
                         result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
                     }
                 }
@@ -707,6 +723,31 @@
     }
 
     /**
+     * Compares the digests coming from signature blocks. Returns {@code true} if at least one
+     * digest algorithm is present in both digests and actual digests for all common algorithms
+     * are the same.
+     */
+    public static boolean compareDigests(
+            Map<ContentDigestAlgorithm, byte[]> firstDigests,
+            Map<ContentDigestAlgorithm, byte[]> secondDigests) throws NoSuchAlgorithmException {
+
+        Set<ContentDigestAlgorithm> intersectKeys = new HashSet<>(firstDigests.keySet());
+        intersectKeys.retainAll(secondDigests.keySet());
+        if (intersectKeys.isEmpty()) {
+            return false;
+        }
+
+        for (ContentDigestAlgorithm algorithm : intersectKeys) {
+            if (!Arrays.equals(firstDigests.get(algorithm),
+                    secondDigests.get(algorithm))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
      * Verifies the provided {@code apk}'s source stamp signature, including verification of the
      * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
      * returns the result of the verification.
@@ -736,7 +777,7 @@
                 boolean stampSigningBlockFound;
                 try {
                     ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
-                            ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+                            VERSION_SOURCE_STAMP);
                     ApkSigningBlockUtils.findSignature(apk, zipSections,
                             SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
                     stampSigningBlockFound = true;
@@ -872,6 +913,124 @@
     }
 
     /**
+     * Gets content digests, signing lineage and certificates from the given {@code schemeId} block
+     * alongside encountered errors info and creates a new {@code Result} containing all this
+     * information.
+     */
+    public static Result getSigningBlockResult(
+        DataSource apk, ApkUtils.ZipSections zipSections, int sdkVersion, int schemeId)
+        throws IOException, NoSuchAlgorithmException{
+        Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests =
+                new HashMap<>();
+        Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(sdkVersion);
+        Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+        Result result = new Result();
+        result.mergeFrom(getApkContentDigests(apk, zipSections,
+                foundApkSigSchemeIds, supportedSchemeNames, sigSchemeApkContentDigests,
+                schemeId, sdkVersion, sdkVersion));
+        return result;
+    }
+
+    /**
+     * Gets the content digest from the {@code result}'s signers. Ignores {@code ContentDigest}s
+     * for which {@code SignatureAlgorithm} is {@code null}.
+     */
+    public static Map<ContentDigestAlgorithm, byte[]> getContentDigestsFromResult(
+        Result result, int schemeId) {
+        Map<ContentDigestAlgorithm, byte[]>  apkContentDigests = new HashMap<>();
+        if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V2
+                || schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+                || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)) {
+            return apkContentDigests;
+        }
+        switch (schemeId) {
+            case VERSION_APK_SIGNATURE_SCHEME_V2:
+                for (V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+            case VERSION_APK_SIGNATURE_SCHEME_V3:
+                for (Result.V3SchemeSignerInfo signerInfo : result.getV3SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+            case  VERSION_APK_SIGNATURE_SCHEME_V31:
+                for (Result.V3SchemeSignerInfo signerInfo : result.getV31SchemeSigners()) {
+                    getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+                }
+                break;
+        }
+        return apkContentDigests;
+    }
+
+    private static void getContentDigests(
+            List<ContentDigest> digests, Map<ContentDigestAlgorithm, byte[]> contentDigestsMap) {
+        for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+            digests) {
+            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+                    contentDigest.getSignatureAlgorithmId());
+            if (signatureAlgorithm == null) {
+                continue;
+            }
+            contentDigestsMap.put(signatureAlgorithm.getContentDigestAlgorithm(),
+                    contentDigest.getValue());
+        }
+    }
+
+    /**
+     * Checks whether a given {@code result} contains errors indicating that a signing certificate
+     * lineage is incorrect.
+     */
+    public static boolean containsLineageErrors(
+        Result result) {
+        if (!result.containsErrors()) {
+            return false;
+        }
+
+        return (result.getAllErrors().stream().map(i -> i.getIssue())
+                .anyMatch(error -> LINEAGE_RELATED_ISSUES.contains(error)));
+    }
+
+
+    /**
+     * Gets a lineage from the first signer from a given {@code result}.
+     * If the {@code result} contains errors related to the lineage incorrectness or there are no
+     * signers or certificates, it returns {@code null}.
+     * If the lineage is empty but there is a signer, it returns a 1-element lineage containing
+     * the signing key.
+     */
+    public static SigningCertificateLineage getLineageFromResult(
+        Result result, int sdkVersion, int schemeId)
+        throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+        SignatureException {
+        if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+                        || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)
+                || containsLineageErrors(result)) {
+            return null;
+        }
+        List<V3SchemeSignerInfo> signersInfo =
+                schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 ?
+                        result.getV3SchemeSigners() : result.getV31SchemeSigners();
+        if (signersInfo.isEmpty()) {
+            return null;
+        }
+        V3SchemeSignerInfo firstSignerInfo = signersInfo.get(0);
+        SigningCertificateLineage lineage = firstSignerInfo.mSigningCertificateLineage;
+        if (lineage == null && firstSignerInfo.getCertificate() != null) {
+            try {
+                lineage = new SigningCertificateLineage.Builder(
+                        new SignerConfig.Builder(
+                                /* privateKey= */ null, firstSignerInfo.getCertificate())
+                                .build()).build();
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        return lineage;
+    }
+
+    /**
      * Obtains the APK content digest(s) and adds them to the provided {@code
      * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
      * merged with a {@code Result} to notify the client of any errors.
@@ -888,16 +1047,48 @@
             Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
             int apkSigSchemeVersion, int minSdkVersion)
             throws IOException, NoSuchAlgorithmException {
+        return getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames,
+                sigSchemeApkContentDigests, apkSigSchemeVersion, minSdkVersion, mMaxSdkVersion);
+    }
+
+
+    /**
+     * Obtains the APK content digest(s) and adds them to the provided {@code
+     * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+     * merged with a {@code Result} to notify the client of any errors.
+     *
+     * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+     * content digests for V1 signatures use {@link
+     * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+     * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+     * returned.
+     */
+    private static ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+            ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+            Map<Integer, String> supportedSchemeNames,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+            int apkSigSchemeVersion, int minSdkVersion, int maxSdkVersion)
+            throws IOException, NoSuchAlgorithmException {
         if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
-                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) {
+                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
+                || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31)) {
             return null;
         }
         ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion);
         SignatureInfo signatureInfo;
         try {
-            int sigSchemeBlockId = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
-                    ? V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID
-                    : V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+            int sigSchemeBlockId;
+            switch (apkSigSchemeVersion) {
+                case VERSION_APK_SIGNATURE_SCHEME_V31:
+                    sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+                    break;
+                case VERSION_APK_SIGNATURE_SCHEME_V3:
+                    sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+                    break;
+                default:
+                    sigSchemeBlockId =
+                        V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+            }
             signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
                     sigSchemeBlockId, result);
         } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
@@ -909,7 +1100,7 @@
         if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
             V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
                     contentDigestsToVerify, supportedSchemeNames,
-                    foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result);
+                    foundApkSigSchemeIds, minSdkVersion, maxSdkVersion, result);
         } else {
             V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
                     contentDigestsToVerify, result);
@@ -1262,7 +1453,7 @@
 
         private void mergeFrom(ApkSigResult source) {
             switch (source.signatureSchemeVersion) {
-                case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                case VERSION_SOURCE_STAMP:
                     mSourceStampVerified = source.verified;
                     if (!source.mSigners.isEmpty()) {
                         mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
@@ -1276,14 +1467,23 @@
         }
 
         private void mergeFrom(ApkSigningBlockUtils.Result source) {
+            if (source == null) {
+                return;
+            }
+            if (source.containsErrors()) {
+                mErrors.addAll(source.getErrors());
+            }
+            if (source.containsWarnings()) {
+                mWarnings.addAll(source.getWarnings());
+            }
             switch (source.signatureSchemeVersion) {
-                case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+                case VERSION_APK_SIGNATURE_SCHEME_V2:
                     mVerifiedUsingV2Scheme = source.verified;
                     for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
                         mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
                     }
                     break;
-                case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+                case VERSION_APK_SIGNATURE_SCHEME_V3:
                     mVerifiedUsingV3Scheme = source.verified;
                     for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
                         mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
@@ -1293,20 +1493,20 @@
                         mSigningCertificateLineage = source.signingCertificateLineage;
                     }
                     break;
-                case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31:
+                case 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:
+                case VERSION_APK_SIGNATURE_SCHEME_V4:
                     mVerifiedUsingV4Scheme = source.verified;
                     for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
                         mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
                     }
                     break;
-                case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+                case VERSION_SOURCE_STAMP:
                     mSourceStampVerified = source.verified;
                     if (!source.signers.isEmpty()) {
                         mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
@@ -1358,6 +1558,16 @@
                     }
                 }
             }
+            if (!mV31SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+                    if (signer.containsErrors()) {
+                        return true;
+                    }
+                    if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+                        return true;
+                    }
+                }
+            }
             if (mSourceStampInfo != null) {
                 if (mSourceStampInfo.containsErrors()) {
                     return true;
@@ -1404,6 +1614,14 @@
                     }
                 }
             }
+            if (!mV31SchemeSigners.isEmpty()) {
+                for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+                    errors.addAll(signer.mErrors);
+                    if (mWarningsAsErrors) {
+                        errors.addAll(signer.getWarnings());
+                    }
+                }
+            }
             if (mSourceStampInfo != null) {
                 errors.addAll(mSourceStampInfo.getErrors());
                 if (mWarningsAsErrors) {
@@ -1588,6 +1806,7 @@
             private final int mMinSdkVersion;
             private final int mMaxSdkVersion;
             private final boolean mRotationTargetsDevRelease;
+            private final SigningCertificateLineage mSigningCertificateLineage;
 
             private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
                 mIndex = result.index;
@@ -1597,6 +1816,7 @@
                 mContentDigests = result.contentDigests;
                 mMinSdkVersion = result.minSdkVersion;
                 mMaxSdkVersion = result.maxSdkVersion;
+                mSigningCertificateLineage = result.signingCertificateLineage;
                 mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt(
                         attribute -> attribute.getId()).anyMatch(
                         attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
@@ -1672,6 +1892,16 @@
             public boolean getRotationTargetsDevRelease() {
                 return mRotationTargetsDevRelease;
             }
+
+            /**
+             * Returns the {@link SigningCertificateLineage} for this signer; when an APK has
+             * SDK targeted signing configs, the lineage of each signer could potentially contain
+             * a subset of the full signing lineage and / or different capabilities for each signer
+             * in the lineage.
+             */
+            public SigningCertificateLineage getSigningCertificateLineage() {
+                return mSigningCertificateLineage;
+            }
         }
 
         /**
@@ -1764,6 +1994,7 @@
 
             private final List<IssueWithParams> mErrors;
             private final List<IssueWithParams> mWarnings;
+            private final List<IssueWithParams> mInfoMessages;
 
             private final SourceStampVerificationStatus mSourceStampVerificationStatus;
 
@@ -1776,6 +2007,8 @@
                         result.getErrors());
                 mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
                         result.getWarnings());
+                mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+                        result.getInfoMessages());
                 if (mErrors.isEmpty() && mWarnings.isEmpty()) {
                     mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
                 } else {
@@ -1790,6 +2023,7 @@
                 mCertificateLineage = Collections.emptyList();
                 mErrors = Collections.emptyList();
                 mWarnings = Collections.emptyList();
+                mInfoMessages = Collections.emptyList();
                 mSourceStampVerificationStatus = sourceStampVerificationStatus;
                 mTimestamp = 0;
             }
@@ -1816,6 +2050,14 @@
                 return !mErrors.isEmpty();
             }
 
+            /**
+             * Returns {@code true} if any info messages were encountered during verification of
+             * this source stamp.
+             */
+            public boolean containsInfoMessages() {
+                return !mInfoMessages.isEmpty();
+            }
+
             public List<IssueWithParams> getErrors() {
                 return mErrors;
             }
@@ -1825,6 +2067,14 @@
             }
 
             /**
+             * Returns a {@code List} of {@link IssueWithParams} representing info messages
+             * that were encountered during verification of the source stamp.
+             */
+            public List<IssueWithParams> getInfoMessages() {
+                return mInfoMessages;
+            }
+
+            /**
              * Returns the reason for any source stamp verification failures, or {@code
              * STAMP_VERIFIED} if the source stamp was successfully verified.
              */
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index f25bc59..957f48a 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -104,10 +104,10 @@
     private final boolean mOtherSignersSignaturesPreserved;
     private final String mCreatedBy;
     private final List<SignerConfig> mSignerConfigs;
+    private final List<SignerConfig> mTargetedSignerConfigs;
     private final SignerConfig mSourceStampSignerConfig;
     private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
-    private final int mRotationMinSdkVersion;
-    private final boolean mRotationTargetsDevRelease;
+    private final boolean mSourceStampTimestampEnabled;
     private final int mMinSdkVersion;
     private final SigningCertificateLineage mSigningCertificateLineage;
 
@@ -187,11 +187,11 @@
 
     private DefaultApkSignerEngine(
             List<SignerConfig> signerConfigs,
+            List<SignerConfig> targetedSignerConfigs,
             SignerConfig sourceStampSignerConfig,
             SigningCertificateLineage sourceStampSigningCertificateLineage,
+            boolean sourceStampTimestampEnabled,
             int minSdkVersion,
-            int rotationMinSdkVersion,
-            boolean rotationTargetsDevRelease,
             boolean v1SigningEnabled,
             boolean v2SigningEnabled,
             boolean v3SigningEnabled,
@@ -201,7 +201,7 @@
             String createdBy,
             SigningCertificateLineage signingCertificateLineage)
             throws InvalidKeyException {
-        if (signerConfigs.isEmpty()) {
+        if (signerConfigs.isEmpty() && targetedSignerConfigs.isEmpty()) {
             throw new IllegalArgumentException("At least one signer config must be provided");
         }
 
@@ -216,11 +216,11 @@
         mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
         mCreatedBy = createdBy;
         mSignerConfigs = signerConfigs;
+        mTargetedSignerConfigs = targetedSignerConfigs;
         mSourceStampSignerConfig = sourceStampSignerConfig;
         mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+        mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
         mMinSdkVersion = minSdkVersion;
-        mRotationMinSdkVersion = rotationMinSdkVersion;
-        mRotationTargetsDevRelease = rotationTargetsDevRelease;
         mSigningCertificateLineage = signingCertificateLineage;
 
         if (v1SigningEnabled) {
@@ -228,7 +228,8 @@
 
                 // v3 signing only supports single signers, of which the oldest (first) will be the
                 // one to use for v1 and v2 signing
-                SignerConfig oldestConfig = signerConfigs.get(0);
+                SignerConfig oldestConfig = !signerConfigs.isEmpty() ? signerConfigs.get(0)
+                        : targetedSignerConfigs.get(0);
 
                 // in the event of signing certificate changes, make sure we have the oldest in the
                 // signing history to sign with v1
@@ -311,7 +312,8 @@
             // to use for v1 and v2 signing
             List<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
 
-            SignerConfig oldestConfig = mSignerConfigs.get(0);
+            SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0)
+                    : mTargetedSignerConfigs.get(0);
 
             // first make sure that if we have signing certificate history that the oldest signer
             // corresponds to the oldest ancestor
@@ -327,7 +329,7 @@
             }
             signerConfig.add(
                     createSigningBlockSignerConfig(
-                            mSignerConfigs.get(0),
+                            oldestConfig,
                             apkSigningBlockPaddingSupported,
                             ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
             return signerConfig;
@@ -338,27 +340,17 @@
         }
     }
 
-    private boolean signingLineageHas31Support() {
-        return mSigningCertificateLineage != null
-                && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
-                && mMinSdkVersion < mRotationMinSdkVersion;
-    }
-
     private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
             List<ApkSigningBlockUtils.SignerConfig> rawConfigs) 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 (signingLineageHas31Support()) {
-            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.");
-            }
+        // If the caller only specified targeted signing configs, ensure those configs cover the
+        // full range for V3 support (or the APK's minSdkVersion if > P).
+        int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion);
+        if (mSignerConfigs.isEmpty() &&
+                mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) {
+            throw new IllegalArgumentException(
+                    "The provided targeted signer configs do not cover the SDK range for V3 "
+                            + "support; either provide the original signer or ensure a signer "
+                            + "targets SDK version " + minRequiredV3SdkVersion);
         }
 
         List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
@@ -384,43 +376,40 @@
                 // this needs to change
                 config.maxSdkVersion = Integer.MAX_VALUE;
             } else {
-                if (mRotationTargetsDevRelease && currentMinSdk == mRotationMinSdkVersion) {
-                    // The currentMinSdk is both the SDK version for the active development release
-                    // as well as the most recent released platform. To ensure the v3.0 signer will
-                    // target the released platform, overlap the maxSdkVersion for the v3.0 signer
-                    // with the minSdkVersion of the rotated signer in the v3.1 block
-                    config.maxSdkVersion = currentMinSdk;
+                // If the previous signer was targeting a development release, then the current
+                // signer's maxSdkVersion should overlap with the previous signer's minSdkVersion
+                // to ensure the current signer applies to the production release.
+                ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get(
+                        processedConfigs.size() - 1);
+                if (prevSigner.signerTargetsDevRelease) {
+                    config.maxSdkVersion = prevSigner.minSdkVersion;
                 } else {
-                    // otherwise, we only want to use this signer up to the minimum platform version
-                    // on which a newer one is acceptable
                     config.maxSdkVersion = currentMinSdk - 1;
                 }
             }
-            config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
-            // 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
-                    && ((mRotationTargetsDevRelease
-                        ? config.maxSdkVersion > mRotationMinSdkVersion
-                        : config.maxSdkVersion >= mRotationMinSdkVersion))) {
-                config.mSigningCertificateLineage =
-                        mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
-                if (config.minSdkVersion < mRotationMinSdkVersion) {
-                    config.minSdkVersion = mRotationMinSdkVersion;
-                }
+            if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+                // If the current signer is targeting the current development release, then set
+                // the signer's minSdkVersion to the last production release and the flag indicating
+                // this signer is targeting a dev release.
+                config.minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+                config.signerTargetsDevRelease = true;
+            } else if (config.minSdkVersion == 0) {
+                config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(
+                        config.signatureAlgorithms);
+            }
+            // Truncate the lineage to the current signer if it is not the latest signer.
+            X509Certificate signerCert = config.certificates.get(0);
+            if (config.signingCertificateLineage != null
+                    && !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) {
+                config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage(
+                        signerCert);
             }
             // 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
+            // at this point
             processedConfigs.add(config);
             currentMinSdk = config.minSdkVersion;
-            // If the rotation is targeting a development release and this is the v3.1 signer, then
-            // the minSdkVersion of this signer should equal the maxSdkVersion of the next signer;
-            // this ensures a package with the minSdkVersion set to the mRotationMinSdkVersion has
-            // a v3.0 block with the min / max SDK version set to this same minSdkVersion from the
-            // v3.1 block.
-            if ((mRotationTargetsDevRelease && currentMinSdk < mMinSdkVersion)
-                    || (!mRotationTargetsDevRelease && currentMinSdk <= mMinSdkVersion)
-                    || currentMinSdk <= AndroidSdkVersion.P) {
+            if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion
+                    : currentMinSdk <= minRequiredV3SdkVersion) {
                 // this satisfies all we need, stop here
                 break;
             }
@@ -443,24 +432,29 @@
 
     private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
             List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
-        // 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 (!signingLineageHas31Support()) {
+        // The V3.1 signature scheme supports SDK targeted signing config, but this scheme should
+        // only be used when a separate signing config exists for the V3.0 block.
+        if (v3SignerConfigs.size() == 1) {
             return null;
         }
 
+        // When there are multiple signing configs, the signer with the minimum SDK version should
+        // be used for the V3.0 block, and all other signers should be used for the V3.1 block.
+        int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt(
+                signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P);
         List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
-        Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator =
-                v3SignerConfigs.iterator();
+        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 >= mRotationMinSdkVersion) {
+            // If the signer config's minSdkVersion supports V3.1 and is not the min signer in the
+            // list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If
+            // the signer is targeting the minSdkVersion as a development release, then it should
+            // be included in V3.1 to allow the V3.0 block to target the production release of the
+            // same SDK version.
+            if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+                    && (signerConfig.minSdkVersion > signerMinSdkVersion
+                    || (signerConfig.minSdkVersion >= signerMinSdkVersion
+                            && signerConfig.signerTargetsDevRelease))) {
                 v31SignerConfigs.add(signerConfig);
                 v3SignerIterator.remove();
             }
@@ -486,7 +480,7 @@
                 /* apkSigningBlockPaddingSupported= */ false,
                 ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
         if (mSourceStampSigningCertificateLineage != null) {
-            config.mSigningCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+            config.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
                     config.certificates.get(0));
         }
         return config;
@@ -511,13 +505,21 @@
     private List<ApkSigningBlockUtils.SignerConfig> createSigningBlockSignerConfigs(
             boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
         List<ApkSigningBlockUtils.SignerConfig> signerConfigs =
-                new ArrayList<>(mSignerConfigs.size());
+                new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size());
         for (int i = 0; i < mSignerConfigs.size(); i++) {
             SignerConfig signerConfig = mSignerConfigs.get(i);
             signerConfigs.add(
                     createSigningBlockSignerConfig(
                             signerConfig, apkSigningBlockPaddingSupported, schemeId));
         }
+        if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+            for (int i = 0; i < mTargetedSignerConfigs.size(); i++) {
+                SignerConfig signerConfig = mTargetedSignerConfigs.get(i);
+                signerConfigs.add(
+                        createSigningBlockSignerConfig(
+                                signerConfig, apkSigningBlockPaddingSupported, schemeId));
+            }
+        }
         return signerConfigs;
     }
 
@@ -530,6 +532,9 @@
         ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
         newSignerConfig.privateKey = signerConfig.getPrivateKey();
         newSignerConfig.certificates = certificates;
+        newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion();
+        newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease();
+        newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage();
 
         switch (schemeId) {
             case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
@@ -1081,7 +1086,6 @@
                                 v31SignerConfigs)
                                 .setRunnablesExecutor(mExecutor)
                                 .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
-                                .setRotationTargetsDevRelease(mRotationTargetsDevRelease)
                                 .build()
                                 .generateApkSignatureSchemeV3BlockAndDigests();
                 signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
@@ -1090,8 +1094,12 @@
                 zipCentralDirectory, eocd, v3SignerConfigs)
                 .setRunnablesExecutor(mExecutor)
                 .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
-            if (signingLineageHas31Support()) {
-                builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
+            if (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) {
+                // The V3.1 stripping protection writes the minimum SDK version from the targeted
+                // signers as an additional attribute in the V3.0 signing block.
+                int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt(
+                        signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT);
+                builder.setMinSdkVersionForV31(minSdkVersionForV31);
             }
             v3SigningSchemeBlockAndDigests =
                 builder.build().generateApkSignatureSchemeV3BlockAndDigests();
@@ -1136,9 +1144,12 @@
                 signatureSchemeDigestInfos.put(
                         VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
             }
-            signingSchemeBlocks.add(
-                    V2SourceStampSigner.generateSourceStampBlock(
-                            sourceStampSignerConfig, signatureSchemeDigestInfos));
+            V2SourceStampSigner v2SourceStampSigner =
+                    new V2SourceStampSigner.Builder(sourceStampSignerConfig,
+                            signatureSchemeDigestInfos)
+                            .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled)
+                            .build();
+            signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock());
         }
 
         // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
@@ -1627,14 +1638,18 @@
         private final PrivateKey mPrivateKey;
         private final List<X509Certificate> mCertificates;
         private final boolean mDeterministicDsaSigning;
+        private final int mMinSdkVersion;
+        private final boolean mSignerTargetsDevRelease;
+        private final SigningCertificateLineage mSigningCertificateLineage;
 
-        private SignerConfig(
-                String name, PrivateKey privateKey, List<X509Certificate> certificates,
-                boolean deterministicDsaSigning) {
-            mName = name;
-            mPrivateKey = privateKey;
-            mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
-            mDeterministicDsaSigning = deterministicDsaSigning;
+        private SignerConfig(Builder builder) {
+            mName = builder.mName;
+            mPrivateKey = builder.mPrivateKey;
+            mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+            mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+            mMinSdkVersion = builder.mMinSdkVersion;
+            mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease;
+            mSigningCertificateLineage = builder.mSigningCertificateLineage;
         }
 
         /** Returns the name of this signer. */
@@ -1662,12 +1677,30 @@
             return mDeterministicDsaSigning;
         }
 
+        /** Returns the minimum SDK version for which this signer should be used. */
+        public int getMinSdkVersion() {
+            return mMinSdkVersion;
+        }
+
+        /** Returns whether this signer targets a development release. */
+        public boolean getSignerTargetsDevRelease() {
+            return mSignerTargetsDevRelease;
+        }
+
+        /** Returns the {@link SigningCertificateLineage} for this signer. */
+        public SigningCertificateLineage getSigningCertificateLineage() {
+            return mSigningCertificateLineage;
+        }
+
         /** Builder of {@link SignerConfig} instances. */
         public static class Builder {
             private final String mName;
             private final PrivateKey mPrivateKey;
             private final List<X509Certificate> mCertificates;
             private final boolean mDeterministicDsaSigning;
+            private int mMinSdkVersion;
+            private boolean mSignerTargetsDevRelease;
+            private SigningCertificateLineage mSigningCertificateLineage;
 
             /**
              * Constructs a new {@code Builder}.
@@ -1704,13 +1737,92 @@
                 mDeterministicDsaSigning = deterministicDsaSigning;
             }
 
+            /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+            public Builder setMinSdkVersion(int minSdkVersion) {
+                return setLineageForMinSdkVersion(null, minSdkVersion);
+            }
+
+            /**
+             * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+             * (API level) for which the provided {@code lineage} (where applicable) should be used
+             * to produce the APK's signature. This method is useful if callers want to specify a
+             * particular rotated signer or lineage with restricted capabilities for later
+             * platform releases.
+             *
+             * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+             * signing lineages with capabilities; only an app's original signer(s) can be used for
+             * the V1 and V2 signature blocks. Because of this, only a value of {@code
+             * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+             * introduced can be specified.
+             *
+             * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+             * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+             * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+             * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+             * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+             * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+             *
+             * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+             *                minSdkVersion}
+             * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+             *                      should be used
+             * @return this {@code Builder} instance
+             *
+             * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+             * certificate provided in the constructor is not in the specified {@code lineage}.
+             */
+            public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+                    int minSdkVersion) {
+                if (minSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalArgumentException(
+                            "SDK targeted signing config is only supported with the V3 signature "
+                                    + "scheme on Android P (SDK version "
+                                    + AndroidSdkVersion.P + ") and later");
+                }
+                if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    minSdkVersion = AndroidSdkVersion.P;
+                }
+                mMinSdkVersion = minSdkVersion;
+                // If a lineage is provided, ensure the signing certificate for this signer is in
+                // the lineage; in the case of multiple signing certificates, the first is always
+                // used in the lineage.
+                if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+                    throw new IllegalArgumentException(
+                            "The provided lineage does not contain the signing certificate, "
+                                    + mCertificates.get(0).getSubjectDN()
+                                    + ", for this SignerConfig");
+                }
+                mSigningCertificateLineage = lineage;
+                return this;
+            }
+
+            /**
+             * Sets whether this signer's min SDK version is intended to target a development
+             * release.
+             *
+             * <p>This is primarily required for a signer testing on a platform's development
+             * release; however, it is recommended that signer's use the latest development SDK
+             * version instead of explicitly specifying this boolean. This class will properly
+             * handle an SDK that is currently targeting a development release and will use the
+             * finalized SDK version on release.
+             */
+            private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) {
+                if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+                    throw new IllegalArgumentException(
+                            "Rotation can only target a development release for signers targeting "
+                                    + MIN_SDK_WITH_V31_SUPPORT + " or later");
+                }
+                mSignerTargetsDevRelease = signerTargetsDevRelease;
+                return this;
+            }
+
+
             /**
              * Returns a new {@code SignerConfig} instance configured based on the configuration of
              * this builder.
              */
             public SignerConfig build() {
-                return new SignerConfig(mName, mPrivateKey, mCertificates,
-                        mDeterministicDsaSigning);
+                return new SignerConfig(this);
             }
         }
     }
@@ -1718,8 +1830,10 @@
     /** Builder of {@link DefaultApkSignerEngine} instances. */
     public static class Builder {
         private List<SignerConfig> mSignerConfigs;
+        private List<SignerConfig> mTargetedSignerConfigs;
         private SignerConfig mStampSignerConfig;
         private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+        private boolean mSourceStampTimestampEnabled = true;
         private final int mMinSdkVersion;
 
         private boolean mV1SigningEnabled = true;
@@ -1768,11 +1882,10 @@
         }
 
         /**
-         * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
-         * configuration of this builder.
+         * Sets the APK signature schemes that should be enabled based on the options provided by
+         * the caller.
          */
-        public DefaultApkSignerEngine build() throws InvalidKeyException {
-
+        private void setEnabledSignatureSchemes() {
             if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
                 throw new IllegalStateException(
                         "Builder configured to both enable and disable APK "
@@ -1783,27 +1896,159 @@
             } else if (mV3SigningExplicitlyEnabled) {
                 mV3SigningEnabled = true;
             }
+        }
 
-            // make sure our signers are appropriately setup
+        /**
+         * Sets the SDK targeted signer configs based on the signing config and rotation options
+         * provided by the caller.
+         *
+         * @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created
+         * from the provided options
+         */
+        private void setTargetedSignerConfigs() throws InvalidKeyException {
+            // If the caller specified any SDK targeted signer configs, then the min SDK version
+            // should be set for those configs, all others should have a default 0 min SDK version.
+            mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion()
+                    - signerConfig2.getMinSdkVersion()));
+            // With the signer configs sorted, find the first targeted signer config with a min
+            // SDK version > 0 to create the separate targeted signer configs.
+            mTargetedSignerConfigs = new ArrayList<>();
+            for (int i = 0; i < mSignerConfigs.size(); i++) {
+                if (mSignerConfigs.get(i).getMinSdkVersion() > 0) {
+                    mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size());
+                    mSignerConfigs = mSignerConfigs.subList(0, i);
+                    break;
+                }
+            }
+
+            // A lineage provided outside a targeted signing config is intended for the original
+            // rotation; sort the untargeted signing configs based on this lineage and create a new
+            // targeted signing config for the initial rotation.
             if (mSigningCertificateLineage != null) {
+                if (!mTargetedSignerConfigs.isEmpty()) {
+                    // Only the initial rotation can use the rotation-min-sdk-version; all
+                    // subsequent targeted rotations must use targeted signing configs.
+                    int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion();
+                    if (mRotationMinSdkVersion >= firstTargetedSdkVersion) {
+                        throw new IllegalStateException(
+                                "The rotation-min-sdk-version, " + mRotationMinSdkVersion
+                                        + ", must be less than the first targeted SDK version, "
+                                        + firstTargetedSdkVersion);
+                    }
+                }
                 try {
                     mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
-                    if (!mV3SigningEnabled && mSignerConfigs.size() > 1) {
-
-                        // this is a strange situation: we've provided a valid rotation history, but
-                        // are only signing with v1/v2.  blow up, since we don't know for sure with
-                        // which signer the user intended to sign
-                        throw new IllegalStateException(
-                                "Provided multiple signers which are part of the"
-                                        + " SigningCertificateLineage, but not signing with APK"
-                                        + " Signature Scheme v3");
-                    }
                 } catch (IllegalArgumentException e) {
                     throw new IllegalStateException(
                             "Provided signer configs do not match the "
                                     + "provided SigningCertificateLineage",
                             e);
                 }
+                // Get the last signer in the lineage, create a new targeted signer from it,
+                // and add it as a targeted signer config.
+                SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1);
+                SignerConfig.Builder rotatedConfigBuilder = new SignerConfig.Builder(
+                        rotatedSignerConfig.getName(), rotatedSignerConfig.getPrivateKey(),
+                        rotatedSignerConfig.getCertificates(),
+                        rotatedSignerConfig.getDeterministicDsaSigning());
+                rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage,
+                        mRotationMinSdkVersion);
+                rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease);
+                mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build());
+            }
+            mSigningCertificateLineage = mergeTargetedSigningConfigLineages();
+        }
+
+        /**
+         * Merges and returns the lineages from any caller provided SDK targeted {@link
+         * SignerConfig} instances with an optional {@code lineage} specified as part of the general
+         * signing config.
+         *
+         * <p>If multiple signing configs target the same SDK version, or if any of the lineages
+         * cannot be merged, then an {@code IllegalStateException} is thrown.
+         */
+        private SigningCertificateLineage mergeTargetedSigningConfigLineages()
+                throws InvalidKeyException {
+            SigningCertificateLineage mergedLineage = null;
+            int prevSdkVersion = 0;
+            for (SignerConfig signerConfig : mTargetedSignerConfigs) {
+                int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+                if (signerMinSdkVersion < AndroidSdkVersion.P) {
+                    throw new IllegalStateException(
+                            "Targeted signing config is not supported prior to SDK version "
+                                    + AndroidSdkVersion.P + "; received value "
+                                    + signerMinSdkVersion);
+                }
+                SigningCertificateLineage signerLineage =
+                        signerConfig.getSigningCertificateLineage();
+                // It is possible for a lineage to be null if the user is using one of the
+                // signers from the lineage as the only signer to target an SDK version; create
+                // a single element lineage to verify the signer is part of the merged lineage.
+                if (signerLineage == null) {
+                    try {
+                        signerLineage = new SigningCertificateLineage.Builder(
+                                new SigningCertificateLineage.SignerConfig.Builder(
+                                        signerConfig.mPrivateKey,
+                                        signerConfig.mCertificates.get(0))
+                                        .build())
+                                .build();
+                    } catch (CertificateEncodingException | NoSuchAlgorithmException
+                            | SignatureException e) {
+                        throw new IllegalStateException(
+                                "Unable to create a SignerConfig for signer from certificate "
+                                        + signerConfig.mCertificates.get(0).getSubjectDN());
+                    }
+                }
+                // The V3.0 signature scheme does not support verified targeted SDK signing
+                // configs; if a signer is targeting any SDK version < T, then it will
+                // target P with the V3.0 signature scheme.
+                if (signerMinSdkVersion < AndroidSdkVersion.T) {
+                    signerMinSdkVersion = AndroidSdkVersion.P;
+                }
+                // Ensure there are no SignerConfigs targeting the same SDK version.
+                if (signerMinSdkVersion == prevSdkVersion) {
+                    throw new IllegalStateException(
+                            "Multiple SignerConfigs were found targeting SDK version "
+                                    + signerMinSdkVersion);
+                }
+                // If multiple lineages have been provided, then verify each subsequent lineage
+                // is a valid descendant or ancestor of the previously merged lineages.
+                if (mergedLineage == null) {
+                    mergedLineage = signerLineage;
+                } else {
+                    try {
+                        mergedLineage = mergedLineage.mergeLineageWith(signerLineage);
+                    } catch (IllegalArgumentException e) {
+                        throw new IllegalStateException(
+                                "The provided lineage targeting SDK " + signerMinSdkVersion
+                                        + " is not in the signing history of the other targeted "
+                                        + "signing configs", e);
+                    }
+                }
+                prevSdkVersion = signerMinSdkVersion;
+            }
+            return mergedLineage;
+        }
+
+        /**
+         * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+         * configuration of this builder.
+         */
+        public DefaultApkSignerEngine build() throws InvalidKeyException {
+            setEnabledSignatureSchemes();
+            setTargetedSignerConfigs();
+
+            // make sure our signers are appropriately setup
+            if (mSigningCertificateLineage != null) {
+                if (!mV3SigningEnabled && mSignerConfigs.size() > 1) {
+                    // this is a strange situation: we've provided a valid rotation history, but
+                    // are only signing with v1/v2.  blow up, since we don't know for sure with
+                    // which signer the user intended to sign
+                    throw new IllegalStateException(
+                            "Provided multiple signers which are part of the"
+                                    + " SigningCertificateLineage, but not signing with APK"
+                                    + " Signature Scheme v3");
+                }
             } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
                 throw new IllegalStateException(
                         "Multiple signing certificates provided for use with APK Signature Scheme"
@@ -1812,11 +2057,11 @@
 
             return new DefaultApkSignerEngine(
                     mSignerConfigs,
+                    mTargetedSignerConfigs,
                     mStampSignerConfig,
                     mSourceStampSigningCertificateLineage,
+                    mSourceStampTimestampEnabled,
                     mMinSdkVersion,
-                    mRotationMinSdkVersion,
-                    mRotationTargetsDevRelease,
                     mV1SigningEnabled,
                     mV2SigningEnabled,
                     mV3SigningEnabled,
@@ -1844,6 +2089,15 @@
         }
 
         /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
          * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
          *
          * <p>By default, the APK will be signed using this scheme.
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 43b7f5e..0f1cc33 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -112,6 +112,16 @@
         mSigningLineage = list;
     }
 
+    /**
+     * Creates a {@code SigningCertificateLineage} with a single signer in the lineage.
+     */
+    private static SigningCertificateLineage createSigningLineage(int minSdkVersion,
+            SignerConfig signer, SignerCapabilities capabilities) {
+        SigningCertificateLineage signingCertificateLineage = new SigningCertificateLineage(
+                minSdkVersion, new ArrayList<>());
+        return signingCertificateLineage.spawnFirstDescendant(signer, capabilities);
+    }
+
     private static SigningCertificateLineage createSigningLineage(
             int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities,
             SignerConfig child, SignerCapabilities childCapabilities)
@@ -183,14 +193,37 @@
     }
 
     /**
-     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3
-     * signature block of the provided APK DataSource.
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 and
+     * V3.1 signature blocks of the provided APK DataSource.
      *
-     * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block,
-     * or if the V3 signature block does not contain a valid lineage.
+     * @throws IllegalArgumentException if the provided APK does not contain a V3 nor V3.1
+     * signature block, or if the V3 and V3.1 signature blocks do not contain a valid lineage.
      */
+
     public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
             throws IOException, ApkFormatException {
+        return readFromApkDataSource(apk, /* readV31Lineage= */ true,  /* readV3Lineage= */true);
+    }
+
+    /**
+     * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3.1
+     * signature blocks of the provided APK DataSource.
+     *
+     * @throws IllegalArgumentException if the provided APK does not contain a V3.1 signature block,
+     * or if the V3.1 signature block does not contain a valid lineage.
+     */
+
+    public static SigningCertificateLineage readV31FromApkDataSource(DataSource apk)
+            throws IOException, ApkFormatException {
+            return readFromApkDataSource(apk, /* readV31Lineage= */ true,
+                        /* readV3Lineage= */ false);
+    }
+
+    private static SigningCertificateLineage readFromApkDataSource(
+            DataSource apk,
+            boolean readV31Lineage,
+            boolean readV3Lineage)
+            throws IOException, ApkFormatException {
         ApkUtils.ZipSections zipSections;
         try {
             zipSections = ApkUtils.findZipSections(apk);
@@ -199,29 +232,41 @@
         }
 
         List<SignatureInfo> signatureInfoList = new ArrayList<>();
-        try {
-            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+        if (readV31Lineage) {
+            try {
+                ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
                     ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
-            signatureInfoList.add(
+                signatureInfoList.add(
                     ApkSigningBlockUtils.findSignature(apk, zipSections,
-                            V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+                        V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+            } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+                // This could be expected if there's only a V3 signature block.
+            }
         }
-        catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
-            // This could be expected if there's only a V3 signature block.
-        }
-        try {
-            ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+        if (readV3Lineage) {
+            try {
+                ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
                     ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
-            signatureInfoList.add(
+                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
+                        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.");
+            String message;
+            if (readV31Lineage && readV3Lineage) {
+                message = "The provided APK does not contain a valid V3 nor V3.1 signature block.";
+            } else if (readV31Lineage) {
+                message = "The provided APK does not contain a valid V3.1 signature block.";
+            } else if (readV3Lineage) {
+                message = "The provided APK does not contain a valid V3 signature block.";
+            } else {
+                message = "No signature blocks were requested.";
+            }
+            throw new IllegalArgumentException(message);
         }
 
         List<SigningCertificateLineage> lineages = new ArrayList<>(1);
@@ -598,11 +643,23 @@
         if (config == null) {
             throw new NullPointerException("config == null");
         }
+        updateSignerCapabilities(config.getCertificate(), capabilities);
+    }
 
-        X509Certificate cert = config.getCertificate();
+    /**
+     * Updates the {@code capabilities} for the signer with the provided {@code certificate} in the
+     * lineage. Only those capabilities that have been modified through the setXX methods will be
+     * updated for the signer to prevent unset default values from being applied.
+     */
+    public void updateSignerCapabilities(X509Certificate certificate,
+            SignerCapabilities capabilities) {
+        if (certificate == null) {
+            throw new NullPointerException("config == null");
+        }
+
         for (int i = 0; i < mSigningLineage.size(); i++) {
             SigningCertificateNode lineageNode = mSigningLineage.get(i);
-            if (lineageNode.signingCert.equals(cert)) {
+            if (lineageNode.signingCert.equals(certificate)) {
                 int flags = lineageNode.flags;
                 SignerCapabilities newCapabilities = new SignerCapabilities.Builder(
                         flags).setCallerConfiguredCapabilities(capabilities).build();
@@ -612,7 +669,7 @@
         }
 
         // the provided signer config was not found in the lineage
-        throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN()
+        throw new IllegalArgumentException("Certificate (" + certificate.getSubjectDN()
                 + ") not found in the SigningCertificateLineage");
     }
 
@@ -656,13 +713,28 @@
         return false;
     }
 
+    /**
+     * Returns whether the provided {@code cert} is the latest signing certificate in the lineage.
+     *
+     * <p>This method will only compare the provided {@code cert} against the latest signing
+     * certificate in the lineage; if a certificate that is not in the lineage is provided, this
+     * method will return false.
+     */
+    public boolean isCertificateLatestInLineage(X509Certificate cert) {
+        if (cert == null) {
+            throw new NullPointerException("cert == null");
+        }
+
+        return mSigningLineage.get(mSigningLineage.size() - 1).signingCert.equals(cert);
+    }
+
     private static int calculateDefaultFlags() {
         return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
                 | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
     }
 
     /**
-     * Returns a new SigingCertificateLineage which terminates at the node corresponding to the
+     * Returns a new SigningCertificateLineage which terminates at the node corresponding to the
      * given certificate.  This is useful in the event of rotating to a new signing algorithm that
      * is only supported on some platform versions.  It enables a v3 signature to be generated using
      * this signing certificate and the shortened proof-of-rotation record from this sub lineage in
@@ -689,50 +761,168 @@
     }
 
     /**
-     * Consolidates all of the lineages found in an APK into one lineage, which is the longest one.
-     * In so doing, it also checks that all of the smaller lineages are contained in the largest,
-     * and that they properly cover the desired platform ranges.
+     * Consolidates all of the lineages found in an APK into one lineage. In so doing, it also
+     * checks that all of the lineages are contained in one common lineage.
      *
      * An APK may contain multiple lineages, one for each signer, which correspond to different
      * supported platform versions.  In this event, the lineage(s) from the earlier platform
-     * version(s) need to be present in the most recent (longest) one to make sure that when a
-     * platform version changes.
+     * version(s) should be present in the most recent, either directly or via a sublineage
+     * that would allow the earlier lineages to merge with the most recent.
      *
      * <note> This does not verify that the largest lineage corresponds to the most recent supported
-     * platform version.  That check requires is performed during v3 verification. </note>
+     * platform version.  That check is performed during v3 verification. </note>
      */
     public static SigningCertificateLineage consolidateLineages(
             List<SigningCertificateLineage> lineages) {
         if (lineages == null || lineages.isEmpty()) {
             return null;
         }
-        int largestIndex = 0;
-        int maxSize = 0;
+        SigningCertificateLineage consolidatedLineage = lineages.get(0);
+        for (int i = 1; i < lineages.size(); i++) {
+            consolidatedLineage = consolidatedLineage.mergeLineageWith(lineages.get(i));
+        }
+        return consolidatedLineage;
+    }
 
-        // determine the longest chain
-        for (int i = 0; i < lineages.size(); i++) {
-            int curSize = lineages.get(i).size();
-            if (curSize > maxSize) {
-                largestIndex = i;
-                maxSize = curSize;
-            }
+    /**
+     * Merges this lineage with the provided {@code otherLineage}.
+     *
+     * <p>The merged lineage does not currently handle merging capabilities of common signers and
+     * should only be used to determine the full signing history of a collection of lineages.
+     */
+    public SigningCertificateLineage mergeLineageWith(SigningCertificateLineage otherLineage) {
+        // Determine the ancestor and descendant lineages; if the original signer is in the other
+        // lineage, then it is considered a descendant.
+        SigningCertificateLineage ancestorLineage;
+        SigningCertificateLineage descendantLineage;
+        X509Certificate signerCert = mSigningLineage.get(0).signingCert;
+        if (otherLineage.isCertificateInLineage(signerCert)) {
+            descendantLineage = this;
+            ancestorLineage = otherLineage;
+        } else {
+            descendantLineage = otherLineage;
+            ancestorLineage = this;
         }
 
-        List<SigningCertificateNode> largestList = lineages.get(largestIndex).mSigningLineage;
-        // make sure all other lineages fit into this one, with the same capabilities
-        for (int i = 0; i < lineages.size(); i++) {
-            if (i == largestIndex) {
-                continue;
+        int ancestorIndex = 0;
+        int descendantIndex = 0;
+        SigningCertificateNode ancestorNode;
+        SigningCertificateNode descendantNode = descendantLineage.mSigningLineage.get(
+                descendantIndex++);
+        List<SigningCertificateNode> mergedLineage = new ArrayList<>();
+        // Iterate through the ancestor lineage and add the current node to the resulting lineage
+        // until the first node of the descendant is found.
+        while (ancestorIndex < ancestorLineage.size()) {
+            ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+            if (ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+                break;
             }
-            List<SigningCertificateNode> underTest = lineages.get(i).mSigningLineage;
-            if (!underTest.equals(largestList.subList(0, underTest.size()))) {
-                throw new IllegalArgumentException("Inconsistent SigningCertificateLineages. "
-                        + "Not all lineages are subsets of each other.");
+            mergedLineage.add(ancestorNode);
+        }
+        // If all of the nodes in the ancestor lineage have been added to the merged lineage, then
+        // there is no overlap between this and the provided lineage.
+        if (ancestorIndex == mergedLineage.size()) {
+            throw new IllegalArgumentException(
+                    "The provided lineage is not a descendant or an ancestor of this lineage");
+        }
+        // The descendant lineage's first node was in the ancestor's lineage above; add it to the
+        // merged lineage.
+        mergedLineage.add(descendantNode);
+        while (ancestorIndex < ancestorLineage.size()
+                && descendantIndex < descendantLineage.size()) {
+            ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+            descendantNode = descendantLineage.mSigningLineage.get(descendantIndex++);
+            if (!ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+                throw new IllegalArgumentException(
+                        "The provided lineage diverges from this lineage");
             }
+            mergedLineage.add(descendantNode);
+        }
+        // At this point, one or both of the lineages have been exhausted and all signers to this
+        // point were a match between the two lineages; add any remaining elements from either
+        // lineage to the merged lineage.
+        while (ancestorIndex < ancestorLineage.size()) {
+            mergedLineage.add(ancestorLineage.mSigningLineage.get(ancestorIndex++));
+        }
+        while (descendantIndex < descendantLineage.size()) {
+            mergedLineage.add(descendantLineage.mSigningLineage.get(descendantIndex++));
+        }
+        return new SigningCertificateLineage(Math.min(mMinSdkVersion, otherLineage.mMinSdkVersion),
+                mergedLineage);
+    }
+
+    /**
+     * Checks whether given lineages are compatible. Returns {@code true} if an installed APK with
+     * the oldLineage could be updated with an APK with the newLineage.
+     */
+    public static boolean checkLineagesCompatibility(
+        SigningCertificateLineage oldLineage, SigningCertificateLineage newLineage) {
+
+        final ArrayList<X509Certificate> oldCertificates = oldLineage == null ?
+                new ArrayList<X509Certificate>()
+                : new ArrayList(oldLineage.getCertificatesInLineage());
+        final ArrayList<X509Certificate> newCertificates = newLineage == null ?
+                new ArrayList<X509Certificate>()
+                : new ArrayList(newLineage.getCertificatesInLineage());
+
+        if (oldCertificates.isEmpty()) {
+            return true;
+        }
+        if (newCertificates.isEmpty()) {
+            return false;
         }
 
-        // if we've made it this far, they all check out, so just return the largest
-        return lineages.get(largestIndex);
+        // Both lineages contain exactly the same certificates or the new lineage extends
+        // the old one. The capabilities of particular certificates may have changed though but it
+        // does not matter in terms of current compatibility.
+        if (newCertificates.size() >= oldCertificates.size()
+                && newCertificates.subList(0, oldCertificates.size()).equals(oldCertificates)) {
+            return true;
+        }
+
+        ArrayList<X509Certificate> newCertificatesArray = new ArrayList(newCertificates);
+        ArrayList<X509Certificate> oldCertificatesArray = new ArrayList(oldCertificates);
+
+        int lastOldCertIndexInNew = newCertificatesArray.lastIndexOf(
+                    oldCertificatesArray.get(oldCertificatesArray.size()-1));
+
+        // The new lineage trims some nodes from the beginning of the old lineage and possibly
+        // extends it at the end. The new lineage must contain the old signing certificate and
+        // the nodes up until the node with signing certificate must be in the same order.
+        // Good example 1:
+        //    old: A -> B -> C
+        //    new: B -> C -> D
+        // Good example 2:
+        //    old: A -> B -> C
+        //    new: C
+        // Bad example 1:
+        //    old: A -> B -> C
+        //    new: A -> C
+        // Bad example 1:
+        //    old: A -> B
+        //    new: C -> B
+        if (lastOldCertIndexInNew >= 0) {
+            return newCertificatesArray.subList(0, lastOldCertIndexInNew+1).equals(
+                    oldCertificatesArray.subList(
+                            oldCertificates.size()-1-lastOldCertIndexInNew,
+                            oldCertificatesArray.size()));
+        }
+
+
+        // The new lineage can be shorter than the old one only if the last certificate of the new
+        // lineage exists in the old lineage and has a rollback capability there.
+        // Good example:
+        //    old: A -> B_withRollbackCapability -> C
+        //    new: A -> B
+        // Bad example 1:
+        //    old: A -> B -> C
+        //    new: A -> B
+        // Bad example 2:
+        //    old: A -> B_withRollbackCapability -> C
+        //    new: A -> B -> D
+        return  oldCertificates.subList(0, newCertificates.size()).equals(newCertificates)
+                && oldLineage.getSignerCapabilities(
+                        oldCertificates.get(newCertificates.size()-1)).hasRollback();
     }
 
     /**
@@ -769,8 +959,17 @@
          * Returns {@code true} if the capabilities of this object match those of the provided
          * object.
          */
-        public boolean equals(SignerCapabilities other) {
-            return this.mFlags == other.mFlags;
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) return true;
+            if (!(other instanceof SignerCapabilities)) return false;
+
+            return this.mFlags == ((SignerCapabilities) other).mFlags;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * mFlags;
         }
 
         /**
@@ -1040,6 +1239,21 @@
         }
 
         /**
+         * Constructs a new {@code Builder} that is intended to create a {@code
+         * SigningCertificateLineage} with a single signer in the signing history.
+         *
+         * @param originalSignerConfig first signer in this lineage
+         */
+        public Builder(SignerConfig originalSignerConfig) {
+            if (originalSignerConfig == null) {
+                throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+                        + "new SigningCertificateLineage");
+            }
+            mOriginalSignerConfig = originalSignerConfig;
+            mNewSignerConfig = null;
+        }
+
+        /**
          * Sets the minimum Android platform version (API Level) on which this lineage is expected
          * to validate.  It is possible that newer signers in the lineage may not be recognized on
          * the given platform, but as long as an older signer is, the lineage can still be used to
@@ -1094,6 +1308,11 @@
                 mOriginalCapabilities = new SignerCapabilities.Builder().build();
             }
 
+            if (mNewSignerConfig == null) {
+                return createSigningLineage(mMinSdkVersion, mOriginalSignerConfig,
+                        mOriginalCapabilities);
+            }
+
             if (mNewCapabilities == null) {
                 mNewCapabilities = new SignerCapabilities.Builder().build();
             }
diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java
index b155341..98da68e 100644
--- a/src/main/java/com/android/apksig/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -729,6 +729,7 @@
 
             private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
             private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+            private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
 
             private final long mTimestamp;
 
@@ -746,6 +747,7 @@
                 mCertificateLineage = result.certificateLineage;
                 mErrors.addAll(result.getErrors());
                 mWarnings.addAll(result.getWarnings());
+                mInfoMessages.addAll(result.getInfoMessages());
                 mTimestamp = result.timestamp;
             }
 
@@ -777,6 +779,14 @@
             }
 
             /**
+             * Returns {@code true} if any info messages were encountered during verification of
+             * this source stamp.
+             */
+            public boolean containsInfoMessages() {
+                return !mInfoMessages.isEmpty();
+            }
+
+            /**
              * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
              * encountered during source stamp verification.
              */
@@ -799,6 +809,14 @@
             }
 
             /**
+             * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
+             * that were encountered during source stamp verification.
+             */
+            public List<ApkVerificationIssue> getInfoMessages() {
+                return mInfoMessages;
+            }
+
+            /**
              * Returns the epoch timestamp in seconds representing the time this source stamp block
              * was signed, or 0 if the timestamp is not available.
              */
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
index 12e54d0..3e79341 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -31,6 +31,7 @@
     public List<X509Certificate> certs = new ArrayList<>();
     public List<X509Certificate> certificateLineage = new ArrayList<>();
 
+    private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
     private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
     private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
 
@@ -51,6 +52,14 @@
     }
 
     /**
+     * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
+     * provided {@code issueId} and {@code params}.
+     */
+    public void addInfoMessage(int issueId, Object... params) {
+        mInfoMessages.add(new ApkVerificationIssue(issueId, params));
+    }
+
+    /**
      * Returns {@code true} if any errors were encountered during verification for this signer.
      */
     public boolean containsErrors() {
@@ -65,6 +74,14 @@
     }
 
     /**
+     * Returns {@code true} if any info messages were encountered during verification of this
+     * signer.
+     */
+    public boolean containsInfoMessages() {
+        return !mInfoMessages.isEmpty();
+    }
+
+    /**
      * Returns the errors encountered during verification for this signer.
      */
     public List<? extends ApkVerificationIssue> getErrors() {
@@ -77,4 +94,11 @@
     public List<? extends ApkVerificationIssue> getWarnings() {
         return mWarnings;
     }
+
+    /**
+     * Returns the info messages encountered during verification of this signer.
+     */
+    public List<? extends ApkVerificationIssue> getInfoMessages() {
+        return mInfoMessages;
+    }
 }
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 44dcc79..127ac24 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -1270,7 +1270,8 @@
 
         public int minSdkVersion;
         public int maxSdkVersion;
-        public SigningCertificateLineage mSigningCertificateLineage;
+        public boolean signerTargetsDevRelease;
+        public SigningCertificateLineage signingCertificateLineage;
     }
 
     public static class Result extends ApkSigResult {
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 aace413..ef6da2f 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
@@ -237,7 +237,7 @@
                 byte[] sigBytes = readLengthPrefixedByteArray(signature);
                 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
                 if (signatureAlgorithm == null) {
-                    result.addWarning(
+                    result.addInfoMessage(
                             ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
                             sigAlgorithmId);
                     continue;
@@ -328,7 +328,7 @@
                                 timestamp);
                     }
                 } else {
-                    result.addWarning(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
+                    result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
                 }
             } catch (ApkFormatException | BufferUnderflowException e) {
                 result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
index 9c00a88..9283f02 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -55,19 +55,32 @@
  *
  * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
  */
-public abstract class V2SourceStampSigner {
+public class V2SourceStampSigner {
     public static final int V2_SOURCE_STAMP_BLOCK_ID =
             SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
 
+    private final SignerConfig mSourceStampSignerConfig;
+    private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+    private final boolean mSourceStampTimestampEnabled;
+
     /** Hidden constructor to prevent instantiation. */
-    private V2SourceStampSigner() {
+    private V2SourceStampSigner(Builder builder) {
+        mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
+        mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
+        mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
     }
 
     public static Pair<byte[], Integer> generateSourceStampBlock(
             SignerConfig sourceStampSignerConfig,
             Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
             throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
-        if (sourceStampSignerConfig.certificates.isEmpty()) {
+        return new Builder(sourceStampSignerConfig,
+                signatureSchemeDigestInfos).build().generateSourceStampBlock();
+    }
+
+    public Pair<byte[], Integer> generateSourceStampBlock()
+            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        if (mSourceStampSignerConfig.certificates.isEmpty()) {
             throw new SignatureException("No certificates configured for signer");
         }
 
@@ -75,18 +88,18 @@
         List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
         getSignedDigestsFor(
                 VERSION_APK_SIGNATURE_SCHEME_V3,
-                signatureSchemeDigestInfos,
-                sourceStampSignerConfig,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
                 signatureSchemeDigests);
         getSignedDigestsFor(
                 VERSION_APK_SIGNATURE_SCHEME_V2,
-                signatureSchemeDigestInfos,
-                sourceStampSignerConfig,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
                 signatureSchemeDigests);
         getSignedDigestsFor(
                 VERSION_JAR_SIGNATURE_SCHEME,
-                signatureSchemeDigestInfos,
-                sourceStampSignerConfig,
+                mSignatureSchemeDigestInfos,
+                mSourceStampSignerConfig,
                 signatureSchemeDigests);
         Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
 
@@ -94,7 +107,7 @@
 
         try {
             sourceStampBlock.stampCertificate =
-                    sourceStampSignerConfig.certificates.get(0).getEncoded();
+                    mSourceStampSignerConfig.certificates.get(0).getEncoded();
         } catch (CertificateEncodingException e) {
             throw new SignatureException(
                     "Retrieving the encoded form of the stamp certificate failed", e);
@@ -103,9 +116,9 @@
         sourceStampBlock.signedDigests = signatureSchemeDigests;
 
         sourceStampBlock.stampAttributes = encodeStampAttributes(
-                generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage));
+                generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
         sourceStampBlock.signedStampAttributes =
-                ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig,
+                ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
                         sourceStampBlock.stampAttributes);
 
         // FORMAT:
@@ -136,16 +149,16 @@
 
     private static void getSignedDigestsFor(
             int signatureSchemeVersion,
-            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos,
-            SignerConfig sourceStampSignerConfig,
+            Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
+            SignerConfig mSourceStampSignerConfig,
             List<Pair<Integer, byte[]>> signatureSchemeDigests)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
-        if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+        if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
             return;
         }
 
         Map<ContentDigestAlgorithm, byte[]> digestInfo =
-                signatureSchemeDigestInfos.get(signatureSchemeVersion);
+                mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
         List<Pair<Integer, byte[]>> digests = new ArrayList<>();
         for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
             digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
@@ -165,7 +178,7 @@
         //   * length-prefixed bytes: signed digest for the respective signature algorithm
         List<Pair<Integer, byte[]>> signedDigest =
                 ApkSigningBlockUtils.generateSignaturesOverData(
-                        sourceStampSignerConfig, digestBytes);
+                        mSourceStampSignerConfig, digestBytes);
 
         // FORMAT:
         // * length-prefixed sequence of length-prefixed signed signature scheme digests:
@@ -201,22 +214,25 @@
         return result.array();
     }
 
-    private static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+    private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
         HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
 
-        // Write the current epoch time as the timestamp for the source stamp.
-        long timestamp = Instant.now().getEpochSecond();
-        if (timestamp > 0) {
-            ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
-            attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
-            attributeBuffer.putLong(timestamp);
-            stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID, attributeBuffer.array());
-        } else {
-            // The epoch time should never be <= 0, and since security decisions can potentially
-            // be made based on the value in the timestamp, throw an Exception to ensure the issues
-            // with the environment are resolved before allowing the signing.
-            throw new IllegalStateException(
-                    "Received an invalid value from Instant#getTimestamp: " + timestamp);
+        if (mSourceStampTimestampEnabled) {
+            // Write the current epoch time as the timestamp for the source stamp.
+            long timestamp = Instant.now().getEpochSecond();
+            if (timestamp > 0) {
+                ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
+                attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
+                attributeBuffer.putLong(timestamp);
+                stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
+                        attributeBuffer.array());
+            } else {
+                // The epoch time should never be <= 0, and since security decisions can potentially
+                // be made based on the value in the timestamp, throw an Exception to ensure the
+                // issues with the environment are resolved before allowing the signing.
+                throw new IllegalStateException(
+                        "Received an invalid value from Instant#getTimestamp: " + timestamp);
+            }
         }
 
         if (lineage != null) {
@@ -233,4 +249,38 @@
         public byte[] stampAttributes;
         public List<Pair<Integer, byte[]>> signedStampAttributes;
     }
+
+    /** Builder of {@link V2SourceStampSigner} instances. */
+    public static class Builder {
+        private final SignerConfig mSourceStampSignerConfig;
+        private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+        private boolean mSourceStampTimestampEnabled = true;
+
+        /**
+         * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
+         * and the {@code signatureSchemeDigestInfos}.
+         */
+        public Builder(SignerConfig sourceStampSignerConfig,
+                Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
+            mSourceStampSignerConfig = sourceStampSignerConfig;
+            mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
+        }
+
+        /**
+         * Sets whether the source stamp should contain the timestamp attribute with the time
+         * at which the source stamp was signed.
+         */
+        public Builder setSourceStampTimestampEnabled(boolean value) {
+            mSourceStampTimestampEnabled = value;
+            return this;
+        }
+
+        /**
+         * Builds a new V2SourceStampSigner that can be used to generate a new source stamp
+         * block signed with the specified signing config.
+         */
+        public V2SourceStampSigner build() {
+            return new V2SourceStampSigner(this);
+        }
+    }
 }
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 6963dd3..dd92da3 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
@@ -52,4 +52,15 @@
      * finalized.
      */
     public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
+
+    /**
+     * The current development release; rotation / signing configs targeting this release should
+     * be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute.
+     */
+    public static final int DEV_RELEASE = AndroidSdkVersion.U;
+
+    /**
+     * The current production release.
+     */
+    public static final int PROD_RELEASE = AndroidSdkVersion.T;
 }
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 ee5d3b4..28f6589 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
@@ -70,7 +70,7 @@
     private final DataSource mEocd;
     private final List<SignerConfig> mSignerConfigs;
     private final int mBlockId;
-    private final OptionalInt mOptionalRotationMinSdkVersion;
+    private final OptionalInt mOptionalV31MinSdkVersion;
     private final boolean mRotationTargetsDevRelease;
 
     private V3SchemeSigner(DataSource beforeCentralDir,
@@ -79,7 +79,7 @@
             List<SignerConfig> signerConfigs,
             RunnablesExecutor executor,
             int blockId,
-            OptionalInt optionalRotationMinSdkVersion,
+            OptionalInt optionalV31MinSdkVersion,
             boolean rotationTargetsDevRelease) {
         mBeforeCentralDir = beforeCentralDir;
         mCentralDir = centralDir;
@@ -87,7 +87,7 @@
         mSignerConfigs = signerConfigs;
         mExecutor = executor;
         mBlockId = blockId;
-        mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+        mOptionalV31MinSdkVersion = optionalV31MinSdkVersion;
         mRotationTargetsDevRelease = rotationTargetsDevRelease;
     }
 
@@ -378,25 +378,30 @@
     }
 
     private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
-        if (signerConfig.mSigningCertificateLineage != null) {
-            byte[] lineageAttr = generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
-            // If this rotation is not targeting a development release, or if this is not a v3.1
-            // signer block then just return the lineage attribute.
-            if (!mRotationTargetsDevRelease
-                    || mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
-                return lineageAttr;
-            }
-            byte[] devReleaseRotationAttr = generateV31RotationTargetsDevReleaseAttribute();
-            byte[] attributes = new byte[lineageAttr.length + devReleaseRotationAttr.length];
-            System.arraycopy(lineageAttr, 0, attributes, 0, lineageAttr.length);
-            System.arraycopy(devReleaseRotationAttr, 0, attributes, lineageAttr.length,
-                    devReleaseRotationAttr.length);
-            return attributes;
-        } else if (mOptionalRotationMinSdkVersion.isPresent()) {
-            return generateV3RotationMinSdkVersionStrippingProtectionAttribute(
-                    mOptionalRotationMinSdkVersion.getAsInt());
+        List<byte[]> attributes = new ArrayList<>();
+        if (signerConfig.signingCertificateLineage != null) {
+            attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage));
         }
-        return new byte[0];
+        if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease)
+                && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+            attributes.add(generateV31RotationTargetsDevReleaseAttribute());
+        }
+        if (mOptionalV31MinSdkVersion.isPresent()
+                && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+            attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+                    mOptionalV31MinSdkVersion.getAsInt()));
+        }
+        int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum();
+        byte[] attributesBuffer = new byte[attributesSize];
+        if (attributesSize == 0) {
+            return new byte[0];
+        }
+        int index = 0;
+        for (byte[] attribute : attributes) {
+            System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length);
+            index += attribute.length;
+        }
+        return attributesBuffer;
     }
 
     private static final class V3SignatureSchemeBlock {
@@ -426,7 +431,7 @@
 
         private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
         private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
-        private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+        private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty();
         private boolean mRotationTargetsDevRelease = false;
 
         /**
@@ -470,7 +475,21 @@
          * is not modified or removed from the APK's signature block.
          */
         public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
-            mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+            return setMinSdkVersionForV31(rotationMinSdkVersion);
+        }
+
+        /**
+         * Sets the {@code minSdkVersion} to be written as an additional attribute in each
+         * signer's block.
+         *
+         * <p>This value provides the stripping protection to ensure a v3.1 signing block is not
+         * modified or removed from the APK's signature block.
+         */
+        public Builder setMinSdkVersionForV31(int minSdkVersion) {
+            if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+                minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+            }
+            mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion);
             return this;
         }
 
@@ -505,7 +524,7 @@
                     mSignerConfigs,
                     mExecutor,
                     mBlockId,
-                    mOptionalRotationMinSdkVersion,
+                    mOptionalV31MinSdkVersion,
                     mRotationTargetsDevRelease);
         }
     }
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 956027f..bd808f0 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
@@ -19,6 +19,7 @@
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
 
+import com.android.apksig.ApkVerificationIssue;
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.apk.ApkFormatException;
@@ -182,7 +183,7 @@
         // versions
         SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
         for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
-            sortedSigners.put(signer.minSdkVersion, signer);
+            sortedSigners.put(signer.maxSdkVersion, signer);
         }
 
         // first make sure there is neither overlap nor holes
@@ -200,7 +201,10 @@
                 // first round sets up our basis
                 firstMin = currentMin;
             } else {
-                if (currentMin != lastMax + 1) {
+                // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
+                // is targeting a development release.
+                if (currentMin != (lastMax + 1)
+                        && !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
                     mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
                     break;
                 }
@@ -228,8 +232,8 @@
         }
 
         try {
-             mResult.signingCertificateLineage =
-                     SigningCertificateLineage.consolidateLineages(lineages);
+            mResult.signingCertificateLineage =
+                    SigningCertificateLineage.consolidateLineages(lineages);
         } catch (IllegalArgumentException e) {
             mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
         }
@@ -488,7 +492,8 @@
         X509Certificate mainCertificate = result.certs.get(0);
         byte[] certificatePublicKeyBytes;
         try {
-            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(mainCertificate.getPublicKey());
+            certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+                    mainCertificate.getPublicKey());
         } catch (InvalidKeyException e) {
             System.out.println("Caught an exception encoding the public key: " + e);
             e.printStackTrace();
@@ -606,6 +611,17 @@
         }
     }
 
+    /**
+     * Returns whether the specified {@code signerInfo} is targeting a development release.
+     */
+    public static boolean signerTargetsDevRelease(
+            ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
+        boolean result = signerInfo.additionalAttributes.stream()
+                .mapToInt(attribute -> attribute.getId())
+                .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+        return result;
+    }
+
     /** Builder of {@link V3SchemeVerifier} instances. */
     public static class Builder {
         private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
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 bbead72..90aee30 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -66,6 +66,9 @@
     /** Android Sv2. */
     public static final int Sv2 = 32;
 
-    /** Android T. */
+    /** Android Tiramisu. */
     public static final int T = 33;
+
+    /** Android Upside Down Cake. */
+    public static final int U = 34;
 }
diff --git a/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
index 9a266f2..ca6271d 100644
--- a/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
+++ b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
@@ -40,7 +40,7 @@
  */
 public class X509CertificateUtils {
 
-    private static CertificateFactory sCertFactory = null;
+    private static volatile CertificateFactory sCertFactory = null;
 
     // The PEM certificate header and footer as specified in RFC 7468:
     //   There is exactly one space character (SP) separating the "BEGIN" or
@@ -54,6 +54,14 @@
         if (sCertFactory != null) {
             return;
         }
+
+        buildCertFactoryHelper();
+    }
+
+    private static synchronized void buildCertFactoryHelper() {
+        if (sCertFactory != null) {
+            return;
+        }
         try {
             sCertFactory = CertificateFactory.getInstance("X.509");
         } catch (CertificateException e) {
@@ -84,9 +92,7 @@
      */
     public static X509Certificate generateCertificate(byte[] encodedForm)
             throws CertificateException {
-        if (sCertFactory == null) {
-            buildCertFactory();
-        }
+        buildCertFactory();
         return generateCertificate(encodedForm, sCertFactory);
     }
 
@@ -149,9 +155,7 @@
      */
     public static Collection<? extends java.security.cert.Certificate> generateCertificates(
             InputStream in) throws CertificateException {
-        if (sCertFactory == null) {
-            buildCertFactory();
-        }
+        buildCertFactory();
         return generateCertificates(in, sCertFactory);
     }
 
diff --git a/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java b/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java
index 0a55b1a..50ce386 100644
--- a/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java
+++ b/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java
@@ -445,7 +445,13 @@
             throw new IOException(
                     cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
         }
-        byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
+        byte[] result = null;
+        try {
+            result = new byte[(int) cdRecord.getUncompressedSize()];
+        } catch (OutOfMemoryError e) {
+            throw new IOException(
+                    cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e);
+        }
         ByteBuffer resultBuf = ByteBuffer.wrap(result);
         ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
         outputUncompressedData(
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index fc32404..ecf177b 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -18,14 +18,21 @@
 
 import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
 import static com.android.apksig.apk.ApkUtils.findZipSections;
+import static com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo;
+import static com.android.apksig.SigningCertificateLineageTest.assertLineageContainsExpectedSigners;
+import static com.android.apksig.SigningCertificateLineageTest.assertLineageContainsExpectedSignersWithCapabilities;
+import static com.android.apksig.SigningCertificateLineage.SignerCapabilities;
+import static com.android.apksig.ApkVerifierTest.assertVerificationWarning;
 
 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.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.apk.ApkFormatException;
@@ -48,6 +55,7 @@
 import com.android.apksig.util.DataSources;
 import com.android.apksig.zip.ZipFormatException;
 
+import java.security.InvalidKeyException;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -95,6 +103,7 @@
     static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
 
     private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
+    private static final String EC_P256_2_SIGNER_RESOURCE_NAME = "ec-p256_2";
 
     // This is the same cert as above with the modulus reencoded to remove the leading 0 sign bit.
     private static final String FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS =
@@ -102,6 +111,20 @@
 
     private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
             "rsa-2048-lineage-2-signers";
+    private static final String LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME =
+            "rsa-2048-lineage-3-signers";
+    private static final String LINEAGE_RSA_2048_3_SIGNERS_1_NO_CAPS_RESOURCE_NAME =
+            "rsa-2048-lineage-3-signers-1-no-caps";
+    private static final String LINEAGE_RSA_2048_2_SIGNERS_2_3_RESOURCE_NAME =
+            "rsa-2048-lineage-2-signers-2-3";
+
+    private static final String LINEAGE_EC_P256_2_SIGNERS_RESOURCE_NAME =
+            "ec-p256-lineage-2-signers";
+
+    private static final SignerCapabilities DEFAULT_CAPABILITIES =
+            new SignerCapabilities.Builder().build();
+    private static final SignerCapabilities NO_CAPABILITIES = new SignerCapabilities.Builder(
+            0).build();
 
     // These are the ID and value of an extra signature block within the APK signing block that
     // can be preserved through the setOtherSignersSignaturesPreserved API.
@@ -1844,7 +1867,7 @@
         SigningCertificateLineage lineage =
                 Resources.toSigningCertificateLineage(
                         ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
-        int rotationMinSdkVersion = V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + 1;
+        int rotationMinSdkVersion = 10000;
 
         File signedApk = sign("original.apk",
                 new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
@@ -2016,6 +2039,909 @@
     }
 
     @Test
+    public void testV31_rotationMinSdkVersionDevRelease_rotationTargetsDevRelease()
+            throws Exception {
+        // The V3.1 signature scheme can be used to target rotation for a development release;
+        // a development release uses the SDK version of the previously finalized release until
+        // its own SDK is finalized. This test verifies if the rotation-min-sdk-version is set to
+        // the current development release, then the resulting APK should target the previously
+        // finalized release and the rotation-targets-dev-release attribute should be set for
+        // the signer.
+        // If the development release is less than the first release that supports V3.1, then
+        // a development release is not currently supported.
+        assumeTrue(V3SchemeConstants.DEV_RELEASE >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT);
+        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 signedApk = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setMinSdkVersionForRotation(V3SchemeConstants.DEV_RELEASE)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(V3SchemeConstants.PROD_RELEASE,
+                result.getV31SchemeSigners().get(0).getMinSdkVersion());
+        assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease());
+        // The maxSdkVersion for the V3 signer should overlap with the minSdkVersion for the V3.1
+        // signer.
+        assertEquals(V3SchemeConstants.PROD_RELEASE,
+                result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+    }
+
+
+    @Test
+    public void testV31_oneTargetedSigningConfigT_targetsT() throws Exception {
+        // The V3.1 signature scheme supports targeting a signing config for devices running
+        // T+. This test verifies a single signing config targeting T+ is written to the v3.1
+        // block, and the original signer is used for pre-T devices in the v3.0 block. This
+        // is functionally equivalent to calling setMinSdkVersionForRotation(AndroidSdkVersion.T).
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig targetedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineage);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, targetedSigner);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertLineageContainsExpectedSigners(
+                result.getV31SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_oneTargetedSigningConfig10000_targets10000() throws Exception {
+        // When a signing config targets a later release, the V3.0 signature should be used for all
+        // platform releases prior to the targeted release. This test verifies a signing config
+        // targeting SDK 10000 has a V3.0 block that targets through SDK 9999.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig targetedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, 10000, lineage);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, targetedSigner);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(9999, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 10000);
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertLineageContainsExpectedSigners(
+                result.getV31SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+
+    @Test
+    public void test31_twoTargetedSigningConfigs_twoV31Signers() throws Exception {
+        // This test verifies multiple signing configs targeting T+ can be added to the V3.1
+        // signing block.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U, lineageTargetU);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT,
+                signerTargetU);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertEquals(2, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.U).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void test31_threeTargetedSigningConfigs_threeV31Signers() throws Exception {
+        // This test verifies multiple signing configs targeting T+ with modified capabilities
+        // can be added to the V3.1 signing block.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTarget10000 =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_1_NO_CAPS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U,
+                lineageTargetU);
+        ApkSigner.SignerConfig signerTarget10000 = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, 10000,
+                lineageTarget10000);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT,
+                signerTargetU, signerTarget10000);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertEquals(3, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME, 10000);
+        assertLineageContainsExpectedSignersWithCapabilities(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                new String[]{FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME},
+                new SignerCapabilities[]{DEFAULT_CAPABILITIES, DEFAULT_CAPABILITIES});
+        assertLineageContainsExpectedSignersWithCapabilities(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.U).getSigningCertificateLineage(),
+                new String[]{FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME},
+                new SignerCapabilities[]{DEFAULT_CAPABILITIES, DEFAULT_CAPABILITIES,
+                        DEFAULT_CAPABILITIES});
+        assertLineageContainsExpectedSignersWithCapabilities(getV31SignerTargetingSdkVersion(result,
+                        10000).getSigningCertificateLineage(),
+                new String[]{FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME},
+                new SignerCapabilities[]{NO_CAPABILITIES, DEFAULT_CAPABILITIES,
+                        DEFAULT_CAPABILITIES});
+    }
+
+    @Test
+    public void testV31_oneTargetedSigningConfigP_targetsP() throws Exception {
+        // A single signing config can be specified targeting < T; this test verifies a single
+        // config targeting P is written to the V3.0 signing block
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig targetedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.P, lineage);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, targetedSigner);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertFalse(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(AndroidSdkVersion.P, result.getV3SchemeSigners().get(0).getMinSdkVersion());
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_oneTargetedSigningConfigS_targetsP() throws Exception {
+        // A single signing config can be specified targeting < T, but the V3.0 signature scheme
+        // does not have verified SDK targeting. If a signing config is specified to target < T and
+        // > P, the targeted SDK version should be set to P to ensure it applies on all platform
+        // releases that support the V3.0 signature scheme.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig targetedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.S, lineage);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, targetedSigner);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertFalse(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(AndroidSdkVersion.P, result.getV3SchemeSigners().get(0).getMinSdkVersion());
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_twoTargetedSigningConfigsTargetT_throwsException() throws Exception {
+        // The V3.1 signature scheme does not support multiple targeted signers targeting the same
+        // SDK version; this test ensures an Exception is thrown if the caller specifies multiple
+        // signers targeting the same release.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage secondLineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig secondSignerTargetT = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                secondLineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT,
+                secondSignerTargetT);
+
+        assertThrows(IllegalStateException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_oneTargetedSignerUAndDefaultRotationMinSdkVersion_multipleV31Signers()
+            throws Exception {
+        // SDK targeted signing configs can be specified alongside the rotation-min-sdk-version
+        // for the initial rotation. This test verifies when the initial rotation is specified with
+        // the default value for rotation-min-sdk-version and a separate signing config targeting U,
+        // the two signing configs are written as separate V3.1 signatures.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig rotatedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U,
+                lineageTargetU);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, rotatedSigner,
+                signerTargetU);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertEquals(2, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.U).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_oneTargetedSignerSAndRotationMinSdkVersionP_throwsException()
+            throws Exception {
+        // Since the v3.0 does not have verified targeted signing configs, any targeted SDK < T
+        // will target P. If a signing config targets < T and the rotation-min-sdk-version targets
+        // < T, then an exception should be thrown to prevent both signers from targeting P.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetS =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig rotatedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetS = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.S,
+                lineageTargetS);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, rotatedSigner,
+                signerTargetS);
+
+        assertThrows(IllegalStateException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSigningCertificateLineage(lineage)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)));
+    }
+
+    @Test
+    public void testV31_twoTargetedSignerPAndS_throwsException()
+            throws Exception {
+        // Since the v3.0 does not have verified targeted signing configs, any targeted SDK < T
+        // will target P. If two signing configs target < T, then an exception should be thrown to
+        // prevent both signers from targeting P.
+        SigningCertificateLineage lineageTargetP =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetS =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetP = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.P, lineageTargetP);
+        ApkSigner.SignerConfig signerTargetS = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.S, lineageTargetS);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetP,
+                signerTargetS);
+
+        assertThrows(IllegalStateException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_oneTargetedSignerTAndRotationMinSdkVersionP_rotationInV3andV31()
+            throws Exception {
+        // An initial rotation could target P with a separate signing config targeting T+; this
+        // test verifies a rotation-min-sdk-version < T and a signing config targeting T results
+        // in the initial rotation being written to the V3 signing block and the targeted signing
+        // config written to the V3.1 block.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig rotatedSigner = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                lineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, rotatedSigner,
+                signerTargetT);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSigningCertificateLineage(lineage)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_oneTargetedSignerTApkMinSdkT_oneV3Signer()
+            throws Exception {
+        // The V3.1 signature scheme was introduced in SDK version 33; an APK with 33 as its
+        // minSdkVersion can only be installed on devices with v3.1 support. However the V3.1
+        // signature scheme should only be used if there's a separate signing config in the V3.0
+        // block. This test verifies a single signing config targeting an APK's minSdkVersion of
+        // 33 is written to the V3.0 block.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                lineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetT);
+
+        File signedApk = sign("original-minSdk33.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertFalse(result.isVerifiedUsingV31Scheme());
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_oneTargetedSignerTApkMinSdkSv2_throwsException()
+            throws Exception {
+        // When a signing config targeting T+ is specified for an APK with a minSdkVersion < T,
+        // the original signer (or another config targeting the minSdkVersion), must be specified
+        // to ensure the APK can be installed on all supported platform releases. If a signer is
+        // not provided for the minimum SDK version, then an Exception should be thrown.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                lineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetT);
+
+        assertThrows(IllegalArgumentException.class, () -> sign("original-minSdk32.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_twoTargetedSignersSv2AndTApkMinSdkSv2_v3AndV31Signed()
+            throws Exception {
+        // V3.0 does not support verified SDK targeting, so a signing config targeting SDK > P and
+        // < T will be applied to P in the V3.0 signing block. If an app's minSdkVersion > P, then
+        // the app should still successfully sign and verify with one of the signers targeting the
+        // APK's minSdkVersion.
+        SigningCertificateLineage lineageTargetSv2 =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetSv2 = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.Sv2,
+                lineageTargetSv2);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                lineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetSv2, signerTargetT);
+
+        File signedApk = sign("original-minSdk32.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_twoTargetedSignersTAndUApkMinSdkT_v3AndV31Signed()
+            throws Exception {
+        // A V3.0 block is always required before a V3.1 block can be written to the APK's signing
+        // block. If an APK targets T (the first release with support for V3.1), and has two
+        // targeted signers, the signer targeting T should be written to the V3.0 block and the
+        // signer targeting a later release should be written to the V3.1 block.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T,
+                lineageTargetT);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U,
+                lineageTargetU);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetT, signerTargetU);
+
+        File signedApk = sign("original-minSdk33.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.U).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_twoTargetedSignersTAndUWithTruncatedLineage_v3AndV31Signed()
+            throws Exception {
+        // The V3.1 signature scheme allows different lineages to be specified for each targeted
+        // signing config as long as all the lineages can be merged to form a common lineage. A
+        // signing lineage with signers A -> B -> C could be truncated to only signer C in a
+        // targeted signing config.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineage);
+        // Manually instantiate this signer instance to make use of the Builder's setMinSdkVersion.
+        ApkSigner.SignerConfig signerTargetU = new ApkSigner.SignerConfig.Builder(
+                signerTargetT.getName(), signerTargetT.getPrivateKey(),
+                signerTargetT.getCertificates())
+                .setMinSdkVersion(AndroidSdkVersion.U)
+                .build();
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetT, signerTargetU);
+
+        File signedApk = sign("original-minSdk33.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertLineageContainsExpectedSigners(
+                result.getV3SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertNull(getV31SignerTargetingSdkVersion(result,
+                AndroidSdkVersion.U).getSigningCertificateLineage());
+    }
+
+    @Test
+    public void testV31_twoTargetedSignersTAndUWithSignerNotInLineage_throwsException()
+            throws Exception {
+        // While the V3.1 signature scheme allows a targeted signing config to omit a lineage,
+        // this can only be used if a previous targeted signer has specified a lineage that
+        // includes the new signer without a lineage. If an independent signer is specified
+        // that is not in the common lineage, an Exception should be thrown.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineage);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U, null);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetT, signerTargetU);
+
+        assertThrows(IllegalStateException.class, () -> sign("original-minSdk33.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_twoTargetedSignersSeparateLineages_throwsException() throws Exception {
+        // When multiple SDK targeted signers are specified, the lineage for each signer must
+        // be part of a common lineage; if any of the targeted signers has a lineage that diverges
+        // from the common lineage, then an Exception should be thrown.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_EC_P256_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                EC_P256_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                EC_P256_2_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U, lineageTargetU);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT,
+                signerTargetU);
+
+        assertThrows(IllegalStateException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_targetedSignerTAndRotationMinSdkVersionPSeparateLineages_throwsException()
+            throws Exception {
+        // When one or more SDK targeted signers are specified with the initial rotation using
+        // rotation-min-sdk-version, the lineage for each signer must be part of a common lineage;
+        // if any of the targeted signers has a lineage that diverges from the common lineage,
+        // then an Exception should be thrown.
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_EC_P256_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                EC_P256_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig rotatedSigner = getDefaultSignerConfigFromResources(
+                EC_P256_2_SIGNER_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U, lineageTargetT);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, rotatedSigner,
+                signerTargetT);
+
+        assertThrows(IllegalStateException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(false)
+                        .setV2SigningEnabled(false)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSigningCertificateLineage(lineage)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)));
+    }
+
+    @Test
+    public void testV31_targetedSignerWithSignerNotInLineage_throwsException()
+            throws Exception {
+        // When a targeted signer is created with a lineage, the signer must be in the provided
+        // lineage otherwise an Exception should be thrown.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_EC_P256_2_SIGNERS_RESOURCE_NAME);
+
+        assertThrows(IllegalArgumentException.class, () ->
+                getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false,
+                        AndroidSdkVersion.T, lineageTargetT));
+    }
+
+    @Test
+    public void testV31_targetedSignerTCertNotLastInLineage_truncatesLineage() throws Exception {
+        // Previously when a rotation signing config was provided with a lineage that did not
+        // contain the signer as the last node, the lineage was truncated to the signer's position.
+        // This test verifies a targeted signing config specified with a lineage containing signers
+        // later than the current signer will be truncated to the provided signer.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertLineageContainsExpectedSigners(
+                result.getV31SchemeSigners().get(0).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_targetedSignerTAndUSubLineages_signsWithExpectedLineages()
+            throws Exception {
+        // Since the V3.1 signature scheme supports targeted signing configs with separate lineages
+        // as long as the lineages can be merged into a common lineage, this test verifies two
+        // targeted signing configs with lineages A -> B and B -> C can be used to sign an APK
+        // and that each signer from a verification has the expected lineage.
+        SigningCertificateLineage lineageTargetT =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        SigningCertificateLineage lineageTargetU =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_2_3_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetT = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.T, lineageTargetT);
+        ApkSigner.SignerConfig signerTargetU = getDefaultSignerConfigFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.U, lineageTargetU);
+        ApkSigner.SignerConfig originalSigner = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(originalSigner, signerTargetT,
+                signerTargetU);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(1, result.getV3SchemeSigners().size());
+        assertEquals(2, result.getV31SchemeSigners().size());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+        assertV31SignerTargetsMinApiLevel(result, THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.U);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.T).getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(getV31SignerTargetingSdkVersion(result,
+                        AndroidSdkVersion.U).getSigningCertificateLineage(),
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(result.getSigningCertificateLineage(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testV31_targetedSignerPNoOriginalSigner_throwsException() throws Exception {
+        // Targeted signing configs can only target Android P and later since this was the initial
+        // release that added support for V3. This test verifies if a signing config with a lineage
+        // targeting P is provided without an original signer, an Exception is thrown to indicate
+        // the original signer is required for the V1 and V2 signature schemes.
+        SigningCertificateLineage lineageTargetP =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+        ApkSigner.SignerConfig signerTargetP = getDefaultSignerConfigFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.P, lineageTargetP);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetP);
+
+        assertThrows(IllegalArgumentException.class, () -> sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)));
+    }
+
+    @Test
+    public void testV31_targetedSignerPOriginalSigner_signed() throws Exception {
+        // While SDK targeted signing configs are intended to target later platform releases for
+        // rotation, it is possible for a signer to target P with the original signing key. Without
+        // a lineage, the signer will treat this as the original signing key and can use it to sign
+        // the V1 and V2 blocks as well.
+        ApkSigner.SignerConfig signerTargetP = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, false, AndroidSdkVersion.P, null);
+        List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(signerTargetP);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(signerConfigs)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertVerificationWarning(result, null);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertFalse(result.isVerifiedUsingV31Scheme());
+    }
+
+    @Test
     public void testV4_rotationMinSdkVersionLessThanT_signatureOnlyHasRotatedSigner()
             throws Exception {
         // To support SDK version targeting in the v3.1 signature scheme, apksig added a
@@ -2076,11 +3002,12 @@
     }
 
     @Test
-    public void testSourceStampTimestamp_signWithSourceStamp_validTimestampValue()
+    public void
+    testSourceStampTimestamp_signWithSourceStampAndTimestampDefault_validTimestampValue()
             throws Exception {
         // Source stamps should include a timestamp attribute with the epoch time the stamp block
         // was signed. This test verifies a standard signing with a source stamp includes a valid
-        // value for the source stamp timestamp attribute.
+        // value for the source stamp timestamp attribute by default.
         ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources(
                 FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
         List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
@@ -2100,6 +3027,61 @@
         assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0);
     }
 
+    @Test
+    public void
+    testSourceStampTimestamp_signWithSourceStampAndTimestampEnabled_validTimestampValue()
+            throws Exception {
+        // Similar to above, this test verifies a valid timestamp value is written to the
+        // attribute when the caller explicitly requests to enable the source stamp timestamp.
+        ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+                getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(ecP256SignerConfig)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSourceStampSignerConfig(rsa2048SignerConfig)
+                        .setSourceStampTimestampEnabled(true));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertSourceStampVerified(signedApk, result);
+        long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds();
+        assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0);
+    }
+
+    @Test
+    public void
+    testSourceStampTimestamp_signWithSourceStampAndTimestampDisabled_defaultTimestampValue()
+            throws Exception {
+        // While source stamps should include a timestamp attribute indicating the time at which
+        // the stamp was signed, this can cause problems for reproducible builds. The
+        // ApkSigner.Builder#setSourceStampTimestampEnabled API allows the caller to specify
+        // whether the timestamp attribute should be written; this test verifies no timestamp is
+        // written to the source stamp if this API is used to disable the timestamp.
+        ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+        List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+                getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(ecP256SignerConfig)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setSourceStampSignerConfig(rsa2048SignerConfig)
+                        .setSourceStampTimestampEnabled(false));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertSourceStampVerified(signedApk, result);
+        long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds();
+        assertEquals(0, timestamp);
+    }
+
     /**
      * Asserts the provided {@code signedApk} contains a signature block with the expected
      * {@code byte[]} value and block ID as specified in the {@code expectedBlock}.
@@ -2185,7 +3167,7 @@
 
         if (result.isVerifiedUsingV3Scheme()) {
             Set<X509Certificate> v3Signers = new HashSet<>();
-            for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+            for (V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
                 v3Signers.add(signer.getCertificate());
             }
             assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedV3Signers)
@@ -2195,7 +3177,7 @@
 
         if (result.isVerifiedUsingV31Scheme()) {
             Set<X509Certificate> v31Signers = new HashSet<>();
-            for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+            for (V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
                 v31Signers.add(signer.getCertificate());
             }
             // V3.1 only supports specifying signatures with a rotated signing key; if a V3.1
@@ -2237,19 +3219,73 @@
         int minSdkVersion) throws Exception {
         assertTrue(result.isVerifiedUsingV31Scheme());
         ApkSigner.SignerConfig expectedSignerConfig = getDefaultSignerConfigFromResources(signer);
+        StringBuilder errorMessage = new StringBuilder();
 
-        for (ApkVerifier.Result.V3SchemeSignerInfo signerConfig : result.getV31SchemeSigners()) {
+        boolean signerTargetsDevRelease = false;
+        if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+            minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+            signerTargetsDevRelease = true;
+        }
+
+        for (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;
+                // The V3.1 signature scheme allows the same signer to target multiple SDK versions
+                // with different capabilities in the lineage, so save the current error message
+                // in case no subsequent instances of this signer target the specified SDK version.
+                if (minSdkVersion != signerConfig.getMinSdkVersion()) {
+                    if (errorMessage.length() > 0) {
+                        errorMessage.append(System.getProperty("line.separator"));
+                    }
+                    errorMessage.append(
+                            "The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates())
+                                    + ", is expected to target SDK version " + minSdkVersion
+                                    + ", instead it is targeting "
+                                    + signerConfig.getMinSdkVersion());
+                } else if (signerTargetsDevRelease
+                        && !signerConfig.getRotationTargetsDevRelease()) {
+                    if (errorMessage.length() > 0) {
+                        errorMessage.append(System.getProperty("line.separator"));
+                    }
+                    errorMessage.append(
+                            "The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates())
+                                    + ", is targeting a development release, " + minSdkVersion
+                                    + ", but the attribute to target a development release is not"
+                                    + " set");
+                } else {
+                    return;
+                }
             }
         }
         fail("Did not find the expected signer, " + getAllSubjectNamesFrom(
-            expectedSignerConfig.getCertificates()));
+            expectedSignerConfig.getCertificates()) + ": " + errorMessage);
+    }
+
+    /**
+     * Returns the V3.1 signer from the provided {@code result} targeting the specified {@code
+     * targetSdkVersion}.
+     */
+    private V3SchemeSignerInfo getV31SignerTargetingSdkVersion(ApkVerifier.Result result,
+            int targetSdkVersion) throws Exception {
+        boolean signerTargetsDevRelease = false;
+        if (targetSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+            targetSdkVersion = V3SchemeConstants.PROD_RELEASE;
+            signerTargetsDevRelease = true;
+        }
+        for (V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+            if (signer.getMinSdkVersion() == targetSdkVersion) {
+                // If a signer is targeting a development release and another signer is targeting
+                // the most recent production release, then both could be targeting the same SDK
+                // version.
+                if (signerTargetsDevRelease != signer.getRotationTargetsDevRelease()) {
+                    continue;
+                }
+                return signer;
+            }
+        }
+        fail("No V3.1 signer found targeting min SDK version " + targetSdkVersion
+                + ", dev release: " + signerTargetsDevRelease);
+        return null;
     }
 
     /**
@@ -2463,6 +3499,15 @@
                 Files.readAllBytes(Paths.get(second.getPath())));
     }
 
+    private static List<ApkSigner.SignerConfig> getSignerConfigsFromResources(
+            String... signerNames) throws Exception {
+        List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>();
+        for (String signerName : signerNames) {
+            signerConfigs.add(getDefaultSignerConfigFromResources(signerName));
+        }
+        return signerConfigs;
+    }
+
     private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
             String keyNameInResources) throws Exception {
         return getDefaultSignerConfigFromResources(keyNameInResources, false);
@@ -2470,12 +3515,29 @@
 
     private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
             String keyNameInResources, boolean deterministicDsaSigning) throws Exception {
+        return getDefaultSignerConfigFromResources(keyNameInResources, deterministicDsaSigning, 0,
+                null);
+    }
+
+    /**
+     * Returns a new {@link ApkSigner.SignerConfig} with the certificate and private key in
+     * resources with the file prefix {@code keyNameInResources} targeting {@code targetSdkVersion}
+     * with lineage {@code lineage} and using deterministic DSA signing when {@code
+     * deterministicDsaSigning} is set to true.
+     */
+    private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
+            String keyNameInResources, boolean deterministicDsaSigning, int targetSdkVersion,
+            SigningCertificateLineage lineage) throws Exception {
         PrivateKey privateKey =
                 Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8");
         List<X509Certificate> certs =
                 Resources.toCertificateChain(ApkSignerTest.class, keyNameInResources + ".x509.pem");
-        return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs,
-                deterministicDsaSigning).build();
+        ApkSigner.SignerConfig.Builder signerConfigBuilder = new ApkSigner.SignerConfig.Builder(
+                keyNameInResources, privateKey, certs, deterministicDsaSigning);
+        if (targetSdkVersion > 0) {
+            signerConfigBuilder.setLineageForMinSdkVersion(lineage, targetSdkVersion);
+        }
+        return signerConfigBuilder.build();
     }
 
     private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 35944fa..3242f5e 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -20,6 +20,9 @@
 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 com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V31;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -28,15 +31,24 @@
 
 import com.android.apksig.ApkVerifier.Issue;
 import com.android.apksig.ApkVerifier.IssueWithParams;
+import com.android.apksig.ApkVerifier.Result;
 import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus;
 import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
 import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.HexEncoding;
 import com.android.apksig.internal.util.Resources;
+import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 
+import java.nio.charset.StandardCharsets;
 import java.security.Provider;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -77,6 +89,10 @@
             "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
     private static final String EC_P256_CERT_SHA256_DIGEST =
             "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+    private static final String RSA_2048_CHUNKED_SHA256_DIGEST =
+            "0a457e6dd7cc8d4dde28a4dae843032de5fbe58123eedd0a31e7f958f23e1626";
+    private static final String RSA_2048_CHUNKED_SHA256_DIGEST_FROM_INCORRECTLY_SIGNED_APK =
+            "0a457e6dd7cc8d4dde28a4dae843032de5fbe58101eedd0a31e7f958f23e1626";
 
     @Test
     public void testOriginalAccepted() throws Exception {
@@ -304,6 +320,201 @@
     }
 
     @Test
+    public void testGetResultLineage() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-tgt-33-no-v3-attr.apk")));
+        int sdkVersion = AndroidSdkVersion.O;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(ApkVerifier.getLineageFromResult(
+                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31).size() == 2);
+        assertEquals(ApkVerifier.getLineageFromResult(
+                                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31)
+                        .getCertificatesInLineage().get(1),
+                result.getV31SchemeSigners().get(0).getCertificate());
+
+        SigningCertificateLineageTest.assertLineageContainsExpectedSigners(
+                ApkVerifier.getLineageFromResult(
+                        result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testGetResultV3Lineage() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v3-rsa-2048_2-tgt-dev-release.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3);
+
+        assertTrue(ApkVerifier.getLineageFromResult(
+                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3).size() == 2);
+        assertEquals(ApkVerifier.getLineageFromResult(
+                                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3)
+                        .getCertificatesInLineage().get(1),
+                result.getV3SchemeSigners().get(0).getCertificate());
+
+        SigningCertificateLineageTest.assertLineageContainsExpectedSigners(
+                ApkVerifier.getLineageFromResult(
+                        result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testGetResultNoLineageApk() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-empty-lineage-no-v3.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(result != null);
+        assertTrue(!ApkVerifier.containsLineageErrors(result));
+        assertTrue(ApkVerifier.getLineageFromResult(
+                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31) != null);
+        assertEquals(ApkVerifier.getLineageFromResult(
+                                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31)
+                        .getCertificatesInLineage().get(0),
+                result.getV31SchemeSigners().get(0).getCertificate());
+    }
+
+    @Test
+    public void testGetResultNoV31Apk() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v3-rsa-2048_2-tgt-dev-release.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(result.getV31SchemeSigners().isEmpty());
+    }
+
+    @Test
+    public void testGetResultFromV3BlockFromV31SignedApk() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-rsa-2048_2-tgt-33-1-tgt-28.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result =
+                ApkVerifier.getSigningBlockResult(
+                        apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3);
+
+        assertTrue(!result.getV3SchemeSigners().isEmpty());
+        assertTrue(ApkVerifier.getLineageFromResult(
+                        result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3)
+                .getCertificatesInLineage()
+                .equals(Arrays.asList(result.getV3SchemeSigners().get(0).getCertificate())));
+    }
+
+    @Test
+    public void testGetResultContainsLineageErrors() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-2elem-incorrect-lineage.apk")));
+        int sdkVersion = AndroidSdkVersion.P;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(result != null);
+        assertTrue(ApkVerifier.containsLineageErrors(result));
+        assertTrue(ApkVerifier.getLineageFromResult(
+                result, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31) == null);
+    }
+
+    @Test
+    public void testGetResultDigests() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-empty-lineage-no-v3.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        Map<ContentDigestAlgorithm, byte[]> digests =
+                ApkVerifier.getContentDigestsFromResult(
+                        result, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(digests.size() == 1);
+        assertTrue(digests.containsKey(ContentDigestAlgorithm.CHUNKED_SHA256));
+        assertTrue(RSA_2048_CHUNKED_SHA256_DIGEST.equalsIgnoreCase(
+                ApkSigningBlockUtils.toHex(digests.get(ContentDigestAlgorithm.CHUNKED_SHA256))));
+    }
+
+    @Test
+    public void testGetV3ResultDigests() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-rsa-2048_2-tgt-33-1-tgt-28.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V3);
+
+        Map<ContentDigestAlgorithm, byte[]> digests =
+                ApkVerifier.getContentDigestsFromResult(
+                        result, VERSION_APK_SIGNATURE_SCHEME_V3);
+
+        assertTrue(digests.size() == 1);
+        assertTrue(digests.containsKey(ContentDigestAlgorithm.CHUNKED_SHA256));
+        assertTrue(RSA_2048_CHUNKED_SHA256_DIGEST.equalsIgnoreCase(
+                ApkSigningBlockUtils.toHex(digests.get(ContentDigestAlgorithm.CHUNKED_SHA256))));
+    }
+
+    @Test
+    public void testGetV2ResultDigests() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-rsa-2048_2-tgt-33-1-tgt-28.apk")));
+        int sdkVersion = AndroidSdkVersion.N;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result =ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V2);
+
+        Map<ContentDigestAlgorithm, byte[]> digests =
+                ApkVerifier.getContentDigestsFromResult(
+                        result, VERSION_APK_SIGNATURE_SCHEME_V2);
+
+        assertTrue(digests.size() == 1);
+        assertTrue(digests.containsKey(ContentDigestAlgorithm.CHUNKED_SHA256));
+        assertTrue(RSA_2048_CHUNKED_SHA256_DIGEST.equalsIgnoreCase(
+                ApkSigningBlockUtils.toHex(digests.get(ContentDigestAlgorithm.CHUNKED_SHA256))));
+    }
+
+    @Test
+    public void testGetResultIncorrectDigests() throws  Exception {
+        DataSource apk = DataSources.asDataSource(ByteBuffer.wrap(Resources.toByteArray(getClass(),
+                "v31-2elem-lineage-incorrect-digest.apk")));
+        int sdkVersion = AndroidSdkVersion.S;
+        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+        Result result = ApkVerifier.getSigningBlockResult(
+                apk, zipSections, sdkVersion, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        Map<ContentDigestAlgorithm, byte[]> digests =
+                ApkVerifier.getContentDigestsFromResult(
+                        result, VERSION_APK_SIGNATURE_SCHEME_V31);
+
+        assertTrue(digests.size() == 1);
+        assertTrue(digests.containsKey(ContentDigestAlgorithm.CHUNKED_SHA256));
+        assertTrue(!RSA_2048_CHUNKED_SHA256_DIGEST.equalsIgnoreCase(
+                ApkSigningBlockUtils.toHex(digests.get(ContentDigestAlgorithm.CHUNKED_SHA256))));
+        assertTrue(RSA_2048_CHUNKED_SHA256_DIGEST_FROM_INCORRECTLY_SIGNED_APK.equalsIgnoreCase(
+                ApkSigningBlockUtils.toHex(digests.get(ContentDigestAlgorithm.CHUNKED_SHA256))));
+    }
+
+    @Test
     public void testV2OneSignerOneSignatureAccepted() throws Exception {
         // APK signed with v2 scheme only, one signer, one signature
         assertVerifiedForEachForMinSdkVersion(
@@ -1506,13 +1717,14 @@
     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");
+        // use the original signing key. The target is set to 10000 to prevent test failures when
+        // SDK version 34 is set as the development release.
+        ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-10000-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);
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 10000);
     }
 
     @Test
@@ -1537,7 +1749,7 @@
         // 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);
+                V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
         assertVerified(result);
     }
 
@@ -1551,7 +1763,7 @@
         // 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);
+                V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
         assertVerified(result);
     }
 
@@ -1571,10 +1783,14 @@
         // on a device running X with the system property ro.build.version.codename set to a new
         // development codename (eg T); a release platform will have this set to "REL", and the
         // platform will ignore the v3.1 signer if the minSdkVersion is X and the codename is "REL".
-        ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-dev-release.apk");
+        // The target is set to 10000 to prevent test failures when SDK version 34 is set as the
+        // development release.
+        ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-10000-dev-release.apk");
 
         assertVerified(result);
-        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 10000);
+        assertEquals(1, result.getV31SchemeSigners().size());
+        assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease());
         assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
                 SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
     }
@@ -1627,6 +1843,100 @@
         assertTrue(result.isVerifiedUsingV31Scheme());
     }
 
+    @Test(expected = IOException.class)
+    public void verify_largeFileSize_doesNotFailWithOOMError() throws Exception {
+        // During V1 signature verification, each file needs to be uncompressed to calculate
+        // its digest; the verifier uses the file size from the central directory record to
+        // determine the size of the byte[] to allocate. If there is not sufficient memory
+        // in the heap for the allocation, the verification should fail with an exception
+        // instead of an OutOfMemoryError. This test uses an APK where the size of the
+        // MANIFEST.MF is reported as 2016310387.
+        verify("incorrect-manifest-size.apk");
+    }
+
+    @Test
+    public void compareMatchingDigests() throws Exception {
+        Map<ContentDigestAlgorithm, byte[]> firstDigest = new HashMap<>();
+        firstDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        firstDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        Map<ContentDigestAlgorithm, byte[]> secondDigest = new HashMap<>();
+        secondDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        secondDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(ApkVerifier.compareDigests(firstDigest, secondDigest));
+    }
+
+    @Test
+    public void compareMatchingIntersectionDigests() throws Exception {
+        Map<ContentDigestAlgorithm, byte[]> firstDigest = new HashMap<>();
+        firstDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        firstDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        Map<ContentDigestAlgorithm, byte[]> secondDigest = new HashMap<>();
+        secondDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        secondDigest.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST_FROM_INCORRECTLY_SIGNED_APK
+                        .getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(ApkVerifier.compareDigests(firstDigest, secondDigest));
+    }
+
+    @Test
+    public void compareNoIntersectionDigests() throws Exception {
+        Map<ContentDigestAlgorithm, byte[]> firstDigest = new HashMap<>();
+        firstDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        Map<ContentDigestAlgorithm, byte[]> secondDigest = new HashMap<>();
+        secondDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(!ApkVerifier.compareDigests(firstDigest, secondDigest));
+    }
+
+    @Test
+    public void compareNotMatchingDigests() throws Exception {
+        Map<ContentDigestAlgorithm, byte[]> firstDigest = new HashMap<>();
+        firstDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        firstDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        Map<ContentDigestAlgorithm, byte[]> secondDigest = new HashMap<>();
+        secondDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        secondDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(!ApkVerifier.compareDigests(firstDigest, secondDigest));
+    }
+
+    @Test
+    public void comparePartiallyNotMatchingDigests() throws Exception {
+        Map<ContentDigestAlgorithm, byte[]> firstDigest = new HashMap<>();
+        firstDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        firstDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CERT_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+
+        Map<ContentDigestAlgorithm, byte[]> secondDigest = new HashMap<>();
+        secondDigest.put(ContentDigestAlgorithm.SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST.getBytes(StandardCharsets.UTF_8));
+        secondDigest.put(ContentDigestAlgorithm.CHUNKED_SHA256,
+                RSA_2048_CHUNKED_SHA256_DIGEST_FROM_INCORRECTLY_SIGNED_APK
+                        .getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(!ApkVerifier.compareDigests(firstDigest, secondDigest));
+    }
+
     private ApkVerifier.Result verify(String apkFilenameInResources)
             throws IOException, ApkFormatException, NoSuchAlgorithmException {
         return verify(apkFilenameInResources, null, null);
@@ -1750,6 +2060,20 @@
                         .append(issue);
             }
         }
+        for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+            String signerName = "signer #" + (signer.getIndex() + 1);
+            for (IssueWithParams issue : signer.getErrors()) {
+                if (msg.length() > 0) {
+                    msg.append('\n');
+                }
+                msg.append("APK Signature Scheme v3.1 signer ")
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue.getIssue())
+                        .append(": ")
+                        .append(issue);
+            }
+        }
 
         fail(apkId + " did not verify: " + msg);
     }
@@ -1778,7 +2102,7 @@
      * error, otherwise it will be expected as a warning.
      */
     private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue,
-        boolean verifyError) {
+            boolean verifyError) {
         if (result.isVerified() && verifyError) {
             fail("APK verification succeeded instead of failing with " + expectedIssue);
             return;
@@ -1786,7 +2110,7 @@
 
         StringBuilder msg = new StringBuilder();
         for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) {
-            if (expectedIssue.equals(issue.getIssue())) {
+            if (issue.getIssue().equals(expectedIssue)) {
                 return;
             }
             if (msg.length() > 0) {
@@ -1798,7 +2122,7 @@
             String signerName = signer.getName();
             for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (expectedIssue.equals(issue.getIssue())) {
+                if (issue.getIssue().equals(expectedIssue)) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -1816,7 +2140,7 @@
             String signerName = "signer #" + (signer.getIndex() + 1);
             for (IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (expectedIssue.equals(issue.getIssue())) {
+                if (issue.getIssue().equals(expectedIssue)) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -1832,7 +2156,7 @@
             String signerName = "signer #" + (signer.getIndex() + 1);
             for (IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (expectedIssue.equals(issue.getIssue())) {
+                if (issue.getIssue().equals(expectedIssue)) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -1847,19 +2171,22 @@
         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())) {
+                    : signer.getWarnings())) {
+                if (issue.getIssue().equals(expectedIssue)) {
                     return;
                 }
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v3.1 signer ")
-                    .append(signerName)
-                    .append(": ")
-                    .append(issue);
+                        .append(signerName)
+                        .append(": ")
+                        .append(issue);
             }
         }
+        if (expectedIssue == null && msg.length() == 0) {
+            return;
+        }
 
         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 07a48f1..bb617d4 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -27,26 +28,26 @@
 import com.android.apksig.internal.apk.ApkSigningBlockUtils;
 import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.ByteBufferUtils;
 import com.android.apksig.internal.util.Resources;
 import com.android.apksig.util.DataSource;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
 import java.io.File;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
+import java.security.PrivateKey;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
 public class SigningCertificateLineageTest {
@@ -70,6 +71,17 @@
     }
 
     @Test
+    public void testLineageWithSingleSignerContainsExpectedSigner() throws Exception {
+        SignerConfig signerConfig = Resources.toLineageSignerConfig(getClass(),
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        SigningCertificateLineage lineage = new SigningCertificateLineage.Builder(
+                signerConfig).build();
+
+        assertLineageContainsExpectedSigners(lineage, FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
     public void testFirstRotationContainsExpectedSigners() throws Exception {
         SigningCertificateLineage lineage = createLineageWithSignersFromResources(
                 FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
@@ -219,6 +231,34 @@
     }
 
     @Test
+    public void testUpdatedCapabilitiesInLineageByCertificate() throws Exception {
+        SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        X509Certificate oldSignerCertificate = mSigners.get(0).getCertificate();
+        List<Boolean> expectedCapabilityValues = Arrays.asList(false, false, false, false, false);
+        SignerCapabilities newCapabilities = buildSignerCapabilities(expectedCapabilityValues);
+
+        lineage.updateSignerCapabilities(oldSignerCertificate, newCapabilities);
+
+        assertExpectedCapabilityValues(lineage.getSignerCapabilities(oldSignerCertificate),
+                expectedCapabilityValues);
+    }
+
+    @Test
+    public void testUpdateSignerCapabilitiesCertificateNotInLineageThrowsException()
+            throws Exception {
+        SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        X509Certificate certificate = getSignerConfigFromResources(
+                FIRST_RSA_1024_SIGNER_RESOURCE_NAME).getCertificate();
+        List<Boolean> expectedCapabilityValues = Arrays.asList(false, false, false, false, false);
+        SignerCapabilities newCapabilities = buildSignerCapabilities(expectedCapabilityValues);
+
+        assertThrows(IllegalArgumentException.class, () ->
+                lineage.updateSignerCapabilities(certificate, newCapabilities));
+    }
+
+    @Test
     public void testFirstRotationWitNonDefaultCapabilitiesForSigners() throws Exception {
         SignerConfig oldSigner = Resources.toLineageSignerConfig(getClass(),
                 FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
@@ -326,6 +366,38 @@
     }
 
     @Test
+    public void testIsCertificateLatestInLineageWithLatestCertReturnsTrue() throws Exception {
+        SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        DefaultApkSignerEngine.SignerConfig latestSigner =
+                getApkSignerEngineSignerConfigFromResources(THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertTrue(lineage.isCertificateLatestInLineage(latestSigner.getCertificates().get(0)));
+    }
+
+    @Test
+    public void testIsCertificateLatestInLineageWithOlderCertReturnsFalse() throws Exception {
+        SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        DefaultApkSignerEngine.SignerConfig olderSigner =
+                getApkSignerEngineSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertFalse(lineage.isCertificateLatestInLineage(olderSigner.getCertificates().get(0)));
+    }
+
+    @Test
+    public void testIsCertificateLatestInLineageWithUnknownCertReturnsFalse() throws Exception {
+        SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        DefaultApkSignerEngine.SignerConfig unknownSigner =
+                getApkSignerEngineSignerConfigFromResources(THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertFalse(lineage.isCertificateLatestInLineage(unknownSigner.getCertificates().get(0)));
+    }
+
+    @Test
     public void testAllExpectedCertificatesAreInLineage() throws Exception {
         SigningCertificateLineage lineage = createLineageWithSignersFromResources(
                 FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
@@ -409,9 +481,24 @@
         SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
                 apkDataSource);
         assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
-
     }
 
+    @Test
+    public void testOnlyV31LineageFromAPKWithV31BlockContainsExpectedSigners() 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.readV31FromApkDataSource(
+                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
@@ -456,6 +543,407 @@
         } catch (IllegalArgumentException expected) {}
     }
 
+    @Test
+    public void testV31LineageFromAPKWithNoV31LineageFails() throws Exception {
+        DataSource apkDataSource = Resources.toDataSource(getClass(),
+            "golden-aligned-v1v2-out.apk");
+        try {
+            SigningCertificateLineage.readV31FromApkDataSource(apkDataSource);
+            fail("A failure should have been reported due to the APK not containing a V3 signing "
+                + "block");
+        } catch (IllegalArgumentException expected) {}
+
+        // This is a valid APK signed with the V1, V2, and V3 signature schemes, but there is no
+        // lineage in the V3 signature block.
+        apkDataSource = Resources.toDataSource(getClass(), "golden-aligned-v1v2v3-out.apk");
+        try {
+            SigningCertificateLineage.readV31FromApkDataSource(apkDataSource);
+            fail("A failure should have been reported due to the APK containing a V3 signing "
+                + "block without the lineage attribute");
+        } catch (IllegalArgumentException expected) {}
+
+        // This is a valid APK signed with the V1, V2, and V3 signature schemes, with a valid
+        // lineage in the V3 signature block, but no V3.1 lineage.
+        apkDataSource = Resources.toDataSource(getClass(),
+            "v1v2v3-with-rsa-2048-lineage-3-signers.apk");
+        try {
+            SigningCertificateLineage.readV31FromApkDataSource(apkDataSource);
+            fail("A failure should have been reported due to the APK containing a V3 signing "
+                + "block without the lineage attribute");
+        } catch (IllegalArgumentException expected) {}
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilitySameLineages() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: A -> B -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateLonger() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A
+     * new lineage: A -> B -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateExtended() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: C -> B
+     */
+    public void testCheckLineagesCompatibilityUpdateFirstMismatch() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(THIRD_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: A -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateSecondMismatch() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B -> C
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilityUpdateShorter() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A_withRollbackCapability -> B -> C
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilityUpdateShorterWithDifferentKeyRollback()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME), Arrays.asList(0));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+    @Test
+    /**
+     * old lineage: A -> B_withRollbackCapability -> C
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilityUpdateShorterWithRollback() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME), Arrays.asList(1));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A_withRollbackCapability -> B_withRollbackCapability -> C
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilityUpdateShorterWithMultipleRollbacks()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME), Arrays.asList(0, 1));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A_withRollbackCapability -> B
+     * new lineage: A -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateShorterWithRollbackAdditionalCertificate()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME), Arrays.asList(0));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: empty
+     * new lineage: A -> B
+     */
+    public void testCheckLineagesCompatibilityOldNotV31Signed() throws Exception {
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_1024_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_1024_SIGNER_RESOURCE_NAME));
+
+            assertTrue(SigningCertificateLineage.checkLineagesCompatibility(
+                    /* oldLineage= */ null, newLineage));
+        }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: empty
+     */
+    public void testCheckLineagesCompatibilityNewNotV31Signed() throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_1024_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_1024_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(
+                oldLineage, /* newLineage= */ null));
+    }
+
+    @Test
+    /**
+     * old lineage: empty
+     * new lineage: empty
+     */
+    public void testCheckLineagesCompatibilityBothNotV31Signed() throws Exception {
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(
+                /* oldLineage= */ null, /* newLineage= */ null));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B -> C
+     * new lineage: B -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateTrimmed()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B
+     * new lineage: B -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateTrimmedAndExtended()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B -> C
+     * new lineage: C
+     */
+    public void testCheckLineagesCompatibilityUpdateTrimmedToOne()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertTrue(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    @Test
+    /**
+     * old lineage: A -> B -> C
+     * new lineage: A -> C
+     */
+    public void testCheckLineagesCompatibilityUpdateWronglyTrimmed()
+        throws Exception {
+        SigningCertificateLineage oldLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage newLineage = createLineageWithSignersFromResources(
+                Arrays.asList(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                        THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        assertFalse(SigningCertificateLineage.checkLineagesCompatibility(oldLineage, newLineage));
+    }
+
+    public void testMergeLineageWithTwoEqualLineagesReturnsMergedLineage() throws Exception {
+        // The mergeLineageWith method is intended to merge two separate lineages into a superset
+        // that spans both lineages. This method verifies if both lineages have the same signers,
+        // the merged lineage will have the same signers as well.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        SigningCertificateLineage mergedLineage = lineage1.mergeLineageWith(lineage2);
+
+        assertLineageContainsExpectedSigners(mergedLineage, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testMergeLineageWithOverlappingLineageReturnsMergedLineage() throws Exception {
+        // When A -> B and B -> C are passed to mergeLineageWith, the merged lineage should be
+        // A -> B -> C.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        SigningCertificateLineage mergedLineage1 = lineage1.mergeLineageWith(lineage2);
+        SigningCertificateLineage mergedLineage2 = lineage2.mergeLineageWith(lineage1);
+
+        assertLineageContainsExpectedSigners(mergedLineage1, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(mergedLineage2, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testMergeLineageWithNoOverlappingLineageThrowsException() throws Exception {
+        // When two lineages do not have any overlap, an exception should be thrown since the two
+        // lineages cannot be merged.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertThrows(IllegalArgumentException.class, () -> lineage1.mergeLineageWith(lineage2));
+        assertThrows(IllegalArgumentException.class, () -> lineage2.mergeLineageWith(lineage1));
+    }
+
+    @Test
+    public void testMergeLineageWithDivergedLineageThrowsException() throws Exception {
+        // When two lineages share a common ancestor but diverge at later signers, an exception
+        // should be thrown since the two lineages cannot be merged.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        assertThrows(IllegalArgumentException.class, () -> lineage1.mergeLineageWith(lineage2));
+        assertThrows(IllegalArgumentException.class, () -> lineage2.mergeLineageWith(lineage1));
+    }
+
+    @Test
+    public void testMergeLineageWithSingleSublineageInLineageReturnsMergedLineage()
+            throws Exception {
+        // If A -> B -> C and B are passed to mergeLineageWith, then the merged lineage should be
+        // A -> B -> C.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        SigningCertificateLineage mergedLineage1 = lineage1.mergeLineageWith(lineage2);
+        SigningCertificateLineage mergedLineage2 = lineage2.mergeLineageWith(lineage1);
+
+        assertLineageContainsExpectedSigners(mergedLineage1, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(mergedLineage2, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
+    public void testMergeLineageWithAncestorSublineageInLineageReturnsMergedLineage()
+            throws Exception {
+        // If A -> B -> C and A -> B are passed to mergeLineageWith, then the merged lineage should
+        // be A -> B -> C.
+        SigningCertificateLineage lineage1 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        SigningCertificateLineage lineage2 = createLineageWithSignersFromResources(
+                FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+        SigningCertificateLineage mergedLineage1 = lineage1.mergeLineageWith(lineage2);
+        SigningCertificateLineage mergedLineage2 = lineage2.mergeLineageWith(lineage1);
+
+        assertLineageContainsExpectedSigners(mergedLineage1, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+        assertLineageContainsExpectedSigners(mergedLineage2, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
     /**
      * Builds a new {@code SigningCertificateLinage.SignerCapabilities} object using the values in
      * the provided {@code List}. The {@code List} should contain {@code boolean} values to be
@@ -531,6 +1019,61 @@
         return new SigningCertificateLineage.Builder(oldSignerConfig, newSignerConfig).build();
     }
 
+    private SigningCertificateLineage createLineageWithSignersFromResources(
+            String signerResourceName) throws Exception {
+        SignerConfig signerConfig = Resources.toLineageSignerConfig(getClass(),
+            signerResourceName);
+        mSigners.add(signerConfig);
+        return new SigningCertificateLineage.Builder(signerConfig).build();
+    }
+
+    private SigningCertificateLineage createLineageWithSignersFromResources(
+            List<String> signerResourcesNames)
+            throws Exception {
+        if (signerResourcesNames.isEmpty()) {
+            throw new Exception();
+        }
+        SigningCertificateLineage lineage =
+                createLineageWithSignersFromResources(signerResourcesNames.get(0));
+        for (String resourceName : signerResourcesNames.subList(1, signerResourcesNames.size())) {
+            lineage = updateLineageWithSignerFromResources(lineage, resourceName);
+        }
+        return lineage;
+    }
+
+    private SigningCertificateLineage createLineageWithSignersFromResources(
+            List<String> signerResourcesNames,
+            List<Integer> rollbackCapabilityNodes)
+        throws Exception {
+        SigningCertificateLineage lineage =
+                createLineageWithSignersFromResources(signerResourcesNames);
+        for (Integer i : rollbackCapabilityNodes) {
+            if (i < mSigners.size()) {
+                SignerCapabilities newCapabilities = new SignerCapabilities.Builder()
+                    .setRollback(true).build();
+                lineage.updateSignerCapabilities(mSigners.get(i), newCapabilities);
+            }
+        }
+        return lineage;
+    }
+    /**
+     * Creates a new {@code SigningCertificateLineage} with the specified signers from the
+     * resources.
+     */
+    private SigningCertificateLineage createLineageWithSignersFromResources(String... signers)
+            throws Exception {
+        SignerConfig ancestorSignerConfig = Resources.toLineageSignerConfig(getClass(), signers[0]);
+        SigningCertificateLineage lineage = new SigningCertificateLineage.Builder(
+                ancestorSignerConfig).build();
+        for (int i = 1; i < signers.length; i++) {
+            SignerConfig descendantSignerConfig = Resources.toLineageSignerConfig(getClass(),
+                    signers[i]);
+            lineage = lineage.spawnDescendant(ancestorSignerConfig, descendantSignerConfig);
+            ancestorSignerConfig = descendantSignerConfig;
+        }
+        return lineage;
+    }
+
     /**
      * Updates the specified {@code SigningCertificateLineage} with the signer from the resources.
      * Requires that the {@code mSigners} list contains the previous signers in the lineage since
@@ -542,7 +1085,7 @@
         // specified. If this class was used to create the lineage then the last signer should
         // be in the mSigners list.
         assertTrue("The mSigners list did not contain the expected signers to update the lineage",
-                mSigners.size() >= 2);
+                mSigners.size() >= 1);
         SignerConfig oldSignerConfig = mSigners.get(mSigners.size() - 1);
         SignerConfig newSignerConfig = Resources.toLineageSignerConfig(getClass(),
                 newSignerResourceName);
@@ -554,13 +1097,19 @@
      * Asserts the provided {@code lineage} contains the {@code expectedSigners} from the test's
      * resources.
      */
-    static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
+    protected static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
             String... expectedSigners) throws Exception {
-        List<SignerConfig> signers = new ArrayList<>();
-        for (String expectedSigner : expectedSigners) {
-            signers.add(getSignerConfigFromResources(expectedSigner));
+        assertLineageContainsExpectedSigners(lineage,
+                getSignerConfigsFromResources(expectedSigners));
+    }
+
+    private static List<SignerConfig> getSignerConfigsFromResources(String... signers)
+            throws Exception {
+        List<SignerConfig> signerConfigs = new ArrayList<>();
+        for (String signer : signers) {
+            signerConfigs.add(getSignerConfigFromResources(signer));
         }
-        assertLineageContainsExpectedSigners(lineage, signers);
+        return signerConfigs;
     }
 
     private static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
@@ -573,6 +1122,23 @@
         }
     }
 
+    protected static void assertLineageContainsExpectedSignersWithCapabilities(
+            SigningCertificateLineage lineage, String[] signers,
+            SignerCapabilities[] capabilities) throws Exception {
+        List<SignerConfig> signerConfigs = getSignerConfigsFromResources(signers);
+        assertEquals("The lineage does not contain the expected number of signers",
+                signerConfigs.size(), lineage.size());
+        assertEquals(
+                "The capabilities does not contain the expected number for the provided signers",
+                signerConfigs.size(), capabilities.length);
+        for (int i = 0; i < signerConfigs.size(); i++) {
+            SignerConfig signerConfig = signerConfigs.get(i);
+            assertTrue("The signer " + signerConfig.getCertificate().getSubjectDN()
+                    + " is expected to be in the lineage", lineage.isSignerInLineage(signerConfig));
+            assertEquals(lineage.getSignerCapabilities(signerConfig), capabilities[i]);
+        }
+    }
+
     private static SignerConfig getSignerConfigFromResources(
             String resourcePrefix) throws Exception {
         PrivateKey privateKey =
@@ -585,12 +1151,23 @@
 
     private static DefaultApkSignerEngine.SignerConfig getApkSignerEngineSignerConfigFromResources(
             String resourcePrefix) throws Exception {
+        return getApkSignerEngineSignerConfigFromResources(resourcePrefix, 0, null);
+    }
+
+    private static DefaultApkSignerEngine.SignerConfig getApkSignerEngineSignerConfigFromResources(
+            String resourcePrefix, int minSdkVersion, SigningCertificateLineage lineage)
+            throws Exception {
         PrivateKey privateKey =
                 Resources.toPrivateKey(SigningCertificateLineageTest.class,
                         resourcePrefix + ".pk8");
         X509Certificate cert = Resources.toCertificate(SigningCertificateLineageTest.class,
                 resourcePrefix + ".x509.pem");
-        return new DefaultApkSignerEngine.SignerConfig.Builder(resourcePrefix, privateKey,
-                Collections.singletonList(cert)).build();
+        DefaultApkSignerEngine.SignerConfig.Builder configBuilder =
+                new DefaultApkSignerEngine.SignerConfig.Builder(resourcePrefix, privateKey,
+                        Collections.singletonList(cert));
+        if (minSdkVersion > 0) {
+            configBuilder.setLineageForMinSdkVersion(lineage, minSdkVersion);
+        }
+        return configBuilder.build();
     }
 }
diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
index 2e54a8a..2186744 100644
--- a/src/test/java/com/android/apksig/SourceStampVerifierTest.java
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -374,6 +374,47 @@
                 ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
     }
 
+    @Test
+    public void verifySourceStamp_unknownAttribute_verificationSucceeds() throws Exception {
+        // When a new attribute is added to the source stamp, verifiers previously released to
+        // prod will not recognize this new attribute. This test verifies an unknown attribute
+        // will not cause the verification to fail by using an attribute with ID 0xe43c5945.
+        Result verificationResult = verifySourceStamp("stamp-unknown-attr.apk");
+
+        assertVerified(verificationResult);
+        assertTrue(verificationResult.getSourceStampInfo().containsInfoMessages());
+        assertTrue(verificationResult.getSourceStampInfo().getInfoMessages().stream().anyMatch(
+                info -> info.getIssueId() == ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE));
+    }
+
+    @Test
+    public void verifySourceStamp_unknownSigAlgorithm_verificationSucceeds() throws Exception {
+        // When a new signature algorithm is added to the source stamp, verifiers previously
+        // released to prod will not recognize the new algorithm. This test verifies an unknown
+        // signature algorithm will not cause the verification to fail as long as there is a
+        // known signature that can be verified; this test uses a signature algorithm with ID
+        // 0x1ee.
+        Result verificationResult = verifySourceStamp("stamp-unknown-sig.apk");
+
+        assertVerified(verificationResult);
+        assertTrue(verificationResult.getSourceStampInfo().containsInfoMessages());
+        assertTrue(verificationResult.getSourceStampInfo().getInfoMessages().stream().anyMatch(
+                info -> info.getIssueId()
+                        == ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM));
+    }
+
+    @Test
+    public void verifySourceStamp_onlyUnknownSigAlgorithms_verificationFails() throws Exception {
+        // When a new signature algorithm is added to the source stamp, previously supported
+        // signature algorithms should still be written to the stamp to ensure existing verifiers
+        // can continue verifying the stamp. This test verifies if a stamp only contains signature
+        // algorithms unknown to the verifier then the verification fails as it is not able to
+        // verify any signatures; this test uses signature algorithms with IDs 0x1ee and 0x1ef.
+        Result verificationResult = verifySourceStamp("stamp-only-unknown-sigs.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+    }
 
     private Result verifySourceStamp(String apkFilenameInResources)
             throws Exception {
diff --git a/src/test/resources/com/android/apksig/ec-p256-lineage-2-signers b/src/test/resources/com/android/apksig/ec-p256-lineage-2-signers
new file mode 100644
index 0000000..509ea3b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/ec-p256-lineage-2-signers
Binary files differ
diff --git a/src/test/resources/com/android/apksig/ec-p256_2.pk8 b/src/test/resources/com/android/apksig/ec-p256_2.pk8
new file mode 100644
index 0000000..5e73f27
--- /dev/null
+++ b/src/test/resources/com/android/apksig/ec-p256_2.pk8
Binary files differ
diff --git a/src/test/resources/com/android/apksig/ec-p256_2.x509.pem b/src/test/resources/com/android/apksig/ec-p256_2.x509.pem
new file mode 100644
index 0000000..f8e5e65
--- /dev/null
+++ b/src/test/resources/com/android/apksig/ec-p256_2.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBbTCCAROgAwIBAgIJAIhVvR3SsrIlMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMM
+B2VjLXAyNTYwHhcNMTgwNzEzMTc0MTUxWhcNMjgwNzEwMTc0MTUxWjAUMRIwEAYD
+VQQDDAllYy1wMjU2XzIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQdTMoEcq2X
+7jzs7w2pPWK0UMZ4gzOzbnVTzen3SrXfALu6a6lQ5oRh1wu8JxtiFR2tLeK/YgPN
+IHaAHHqdRCLho1AwTjAdBgNVHQ4EFgQUeZHZKwII/ESL9QbU78n/9CjLXl8wHwYD
+VR0jBBgwFoAU1BM1aLlbMBWLMiBx6oxD/1sFzMgwDAYDVR0TBAUwAwEB/zAKBggq
+hkjOPQQDAgNIADBFAiAnaauxtJ/C9TR5xK6SpmMdq/1SLJrLC7orQ+vrmcYwEQIh
+ANJg+x0fF2z5t/pgCYv9JDGfSQWj5f2hAKb+Giqxn/Ce
+-----END CERTIFICATE-----
diff --git a/src/test/resources/com/android/apksig/incorrect-manifest-size.apk b/src/test/resources/com/android/apksig/incorrect-manifest-size.apk
new file mode 100644
index 0000000..34bc091
--- /dev/null
+++ b/src/test/resources/com/android/apksig/incorrect-manifest-size.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-minSdk33.apk b/src/test/resources/com/android/apksig/original-minSdk33.apk
new file mode 100644
index 0000000..a2ea9eb
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-minSdk33.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/rsa-2048-lineage-2-signers-2-3 b/src/test/resources/com/android/apksig/rsa-2048-lineage-2-signers-2-3
new file mode 100644
index 0000000..c2a3545
--- /dev/null
+++ b/src/test/resources/com/android/apksig/rsa-2048-lineage-2-signers-2-3
Binary files differ
diff --git a/src/test/resources/com/android/apksig/rsa-2048-lineage-3-signers-1-no-caps b/src/test/resources/com/android/apksig/rsa-2048-lineage-3-signers-1-no-caps
new file mode 100644
index 0000000..0fa3118
--- /dev/null
+++ b/src/test/resources/com/android/apksig/rsa-2048-lineage-3-signers-1-no-caps
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk
new file mode 100644
index 0000000..7ec82eb
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-unknown-attr.apk b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk
new file mode 100644
index 0000000..68771a5
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-unknown-sig.apk b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk
new file mode 100644
index 0000000..1c1557e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-2elem-incorrect-lineage.apk
similarity index 79%
copy from src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
copy to src/test/resources/com/android/apksig/v31-2elem-incorrect-lineage.apk
index 784f47e..517a1ef 100644
--- a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
+++ b/src/test/resources/com/android/apksig/v31-2elem-incorrect-lineage.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-2elem-lineage-incorrect-digest.apk
similarity index 80%
copy from src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
copy to src/test/resources/com/android/apksig/v31-2elem-lineage-incorrect-digest.apk
index 784f47e..2eba63e 100644
--- a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
+++ b/src/test/resources/com/android/apksig/v31-2elem-lineage-incorrect-digest.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-empty-lineage-no-v3.apk
similarity index 71%
copy from src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
copy to src/test/resources/com/android/apksig/v31-empty-lineage-no-v3.apk
index 784f47e..fbc7f76 100644
--- a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
+++ b/src/test/resources/com/android/apksig/v31-empty-lineage-no-v3.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-1-tgt-28.apk
similarity index 93%
copy from src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
copy to src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-1-tgt-28.apk
index 784f47e..dde89cb 100644
--- a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk
similarity index 94%
rename from src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
rename to src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk
index 784f47e..0257ce6 100644
--- a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk.idsig b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk.idsig
new file mode 100644
index 0000000..373e01d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-10000-dev-release.apk.idsig
Binary files differ