Snap for 8626064 from fc6387ff79c434dd2616d3391655ec5c12ba415c to mainline-go-adservices-release

Change-Id: I49687e54c7a3c579e9a450969cdb5dcc366a3108
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 4a303b6..742a966 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -49,6 +49,7 @@
 import java.security.interfaces.RSAKey;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.List;
 
 /**
@@ -63,6 +64,8 @@
     private static final String HELP_PAGE_VERIFY = "help_verify.txt";
     private static final String HELP_PAGE_ROTATE = "help_rotate.txt";
     private static final String HELP_PAGE_LINEAGE = "help_lineage.txt";
+    private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
+    private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
 
     private static MessageDigest sha256 = null;
     private static MessageDigest sha1 = null;
@@ -461,6 +464,7 @@
         int maxSdkVersion = Integer.MAX_VALUE;
         boolean maxSdkVersionSpecified = false;
         boolean printCerts = false;
+        boolean printCertsPem = false;
         boolean verbose = false;
         boolean warningsTreatedAsErrors = false;
         boolean verifySourceStamp = false;
@@ -479,6 +483,13 @@
                 maxSdkVersionSpecified = true;
             } else if ("print-certs".equals(optionName)) {
                 printCerts = optionsParser.getOptionalBooleanValue(true);
+            } else if ("print-certs-pem".equals(optionName)) {
+                printCertsPem = optionsParser.getOptionalBooleanValue(true);
+                // If the PEM output of the certs is requested, this implicitly implies the
+                // cert details should be printed.
+                if (printCertsPem && !printCerts) {
+                    printCerts = true;
+                }
             } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
                 verbose = optionsParser.getOptionalBooleanValue(true);
             } else if ("Werr".equals(optionName)) {
@@ -602,24 +613,25 @@
                                         + (signer.getRotationTargetsDevRelease()
                                         ? " (dev release=true)" : "")
                                         + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
-                                verbose);
+                                verbose, printCertsPem);
                     }
                     for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
                         printCertificate(signer.getCertificate(),
                                 "Signer (minSdkVersion=" + signer.getMinSdkVersion()
                                         + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
-                                verbose);
+                                verbose, printCertsPem);
                     }
                 } else {
                     int signerNumber = 0;
                     for (X509Certificate signerCert : signerCerts) {
                         signerNumber++;
-                        printCertificate(signerCert, "Signer #" + signerNumber, verbose);
+                        printCertificate(signerCert, "Signer #" + signerNumber, verbose,
+                                printCertsPem);
                     }
                 }
                 if (sourceStampInfo != null) {
                     printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
-                            verbose);
+                            verbose, printCertsPem);
                 }
             }
         } else {
@@ -844,6 +856,7 @@
 
         boolean verbose = false;
         boolean printCerts = false;
+        boolean printCertsPem = false;
         boolean lineageUpdated = false;
         File inputKeyLineage = null;
         File outputKeyLineage = null;
@@ -865,6 +878,13 @@
                 verbose = optionsParser.getOptionalBooleanValue(true);
             } else if ("print-certs".equals(optionName)) {
                 printCerts = optionsParser.getOptionalBooleanValue(true);
+            } else if ("print-certs-pem".equals(optionName)) {
+                printCertsPem = optionsParser.getOptionalBooleanValue(true);
+                // If the PEM output of the certs is requested, this implicitly implies the
+                // cert details should be printed.
+                if (printCertsPem && !printCerts) {
+                    printCerts = true;
+                }
             } else {
                 throw new ParameterException(
                         "Unsupported option: " + optionsParser.getOptionOriginalForm()
@@ -925,7 +945,8 @@
             for (int i = 0; i < signingCerts.size(); i++) {
                 X509Certificate signerCert = signingCerts.get(i);
                 SignerCapabilities signerCapabilities = lineage.getSignerCapabilities(signerCert);
-                printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose);
+                printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose,
+                        printCertsPem);
                 printCapabilities(signerCapabilities);
             }
         }
@@ -1057,19 +1078,29 @@
     }
 
     /**
+     * @see #printCertificate(X509Certificate, String, boolean, boolean)
+     */
+    public static void printCertificate(X509Certificate cert, String name, boolean verbose)
+            throws NoSuchAlgorithmException, CertificateEncodingException {
+        printCertificate(cert, name, verbose, false);
+    }
+
+    /**
      * Prints details from the provided certificate to stdout.
      *
      * @param cert    the certificate to be displayed.
      * @param name    the name to be used to identify the certificate.
      * @param verbose boolean indicating whether public key details from the certificate should be
      *                displayed.
+     * @param pemOutput boolean indicating whether the PEM encoding of the certificate should be
+     *                  displayed.
      * @throws NoSuchAlgorithmException     if an instance of MD5, SHA-1, or SHA-256 cannot be
      *                                      obtained.
      * @throws CertificateEncodingException if an error is encountered when encoding the
      *                                      certificate.
      */
-    public static void printCertificate(X509Certificate cert, String name, boolean verbose)
-            throws NoSuchAlgorithmException, CertificateEncodingException {
+    public static void printCertificate(X509Certificate cert, String name, boolean verbose,
+            boolean pemOutput) throws NoSuchAlgorithmException, CertificateEncodingException {
         if (cert == null) {
             throw new NullPointerException("cert == null");
         }
@@ -1114,6 +1145,18 @@
             System.out.println(
                     name + " public key MD5 digest: " + HexEncoding.encode(md5.digest(encodedKey)));
         }
+
+        if (pemOutput) {
+            System.out.println(BEGIN_CERTIFICATE);
+            final int lineWidth = 64;
+            String pemEncodedCert = Base64.getEncoder().encodeToString(cert.getEncoded());
+            for (int i = 0; i < pemEncodedCert.length(); i += lineWidth) {
+                System.out.println(pemEncodedCert.substring(i, i + lineWidth > pemEncodedCert.length()
+                        ? pemEncodedCert.length()
+                        : i + lineWidth));
+            }
+            System.out.println(END_CERTIFICATE);
+        }
     }
 
     /**
diff --git a/src/apksigner/java/com/android/apksigner/help_lineage.txt b/src/apksigner/java/com/android/apksigner/help_lineage.txt
index 3f4922d..8fe410b 100644
--- a/src/apksigner/java/com/android/apksigner/help_lineage.txt
+++ b/src/apksigner/java/com/android/apksigner/help_lineage.txt
@@ -19,6 +19,10 @@
 --print-certs         Show information about the signing certificates and their capabilities
                       in the SigningCertificateLineage.
 
+--print-certs-pem     Show information about the signing certificates and their capabilities
+                      in the SigningCertificateLineage; prints the PEM encoding of each signing
+                      certificate to stdout.
+
 -v, --verbose         Verbose output mode.
 
 -h, --help            Show help about this command and exit.
diff --git a/src/apksigner/java/com/android/apksigner/help_verify.txt b/src/apksigner/java/com/android/apksigner/help_verify.txt
index c5cf663..bc70924 100644
--- a/src/apksigner/java/com/android/apksigner/help_verify.txt
+++ b/src/apksigner/java/com/android/apksigner/help_verify.txt
@@ -11,6 +11,9 @@
 
 --print-certs         Show information about the APK's signing certificates
 
+--print-certs-pem     Show information about the APK's signing certificates and prints the PEM
+                      encoding of each signing certificate to stdout.
+
 -v, --verbose         Verbose output mode
 
 --min-sdk-version     Lowest API Level on which this APK's signatures will be
diff --git a/src/main/java/com/android/apksig/ApkVerificationIssue.java b/src/main/java/com/android/apksig/ApkVerificationIssue.java
index 2aa9d0b..fa2b7aa 100644
--- a/src/main/java/com/android/apksig/ApkVerificationIssue.java
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -116,6 +116,8 @@
     public static final int JAR_SIG_NO_SIGNATURES = 36;
     /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
     public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+    /** The source stamp timestamp attribute has an invalid value. */
+    public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
 
     private final int mIssueId;
     private final String mFormat;
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 1613c26..fc28864 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -1767,6 +1767,8 @@
 
             private final SourceStampVerificationStatus mSourceStampVerificationStatus;
 
+            private final long mTimestamp;
+
             private SourceStampInfo(ApkSignerInfo result) {
                 mCertificates = result.certs;
                 mCertificateLineage = result.certificateLineage;
@@ -1780,6 +1782,7 @@
                     mSourceStampVerificationStatus =
                             SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
                 }
+                mTimestamp = result.timestamp;
             }
 
             SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
@@ -1788,6 +1791,7 @@
                 mErrors = Collections.emptyList();
                 mWarnings = Collections.emptyList();
                 mSourceStampVerificationStatus = sourceStampVerificationStatus;
+                mTimestamp = 0;
             }
 
             /**
@@ -1827,6 +1831,14 @@
             public SourceStampVerificationStatus getSourceStampVerificationStatus() {
                 return mSourceStampVerificationStatus;
             }
+
+            /**
+             * Returns the epoch timestamp in seconds representing the time this source stamp block
+             * was signed, or 0 if the timestamp is not available.
+             */
+            public long getTimestampEpochSeconds() {
+                return mTimestamp;
+            }
         }
     }
 
@@ -3000,6 +3012,16 @@
                 + "contains a proof-of-rotation record with signature(s) that did not verify."),
 
         /**
+         * The source stamp timestamp attribute has an invalid value (<= 0).
+         * <ul>
+         *     <li>Parameter 1: The invalid timestamp value.
+         * </ul>
+         */
+        SOURCE_STAMP_INVALID_TIMESTAMP(
+                "The source stamp"
+                        + " timestamp attribute has an invalid value: %1$d"),
+
+        /**
          * The APK could not be properly parsed due to a ZIP or APK format exception.
          * <ul>
          *     <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK.
@@ -3295,6 +3317,8 @@
                     Issue.JAR_SIG_NO_SIGNATURES);
             sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
                     Issue.JAR_SIG_PARSE_EXCEPTION);
+            sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+                    Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
         }
 
         /**
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 62c24bc..f25bc59 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -22,7 +22,6 @@
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
-import static com.android.apksig.internal.apk.v3.V3SchemeConstants.DEV_RELEASE_ROTATION_MIN_SDK_VERSION;
 import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
 import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
 
@@ -339,17 +338,6 @@
         }
     }
 
-    private int getDevReleaseRotationMinSdkVersion() {
-        // TODO (b/199793805): Once the T SDK is finalized and T development releases are using
-        // the new SDK version, this should be removed and mRotationMinSdkVersion should be used
-        // as is for rotation SDK version targeting.
-        // To support targeting the development release use the API level of the previous
-        // platform release as this is the value returned from Build.Version.SDK_INT until
-        // the SDK is finalized.
-        return mRotationMinSdkVersion == MIN_SDK_WITH_V31_SUPPORT
-                ? DEV_RELEASE_ROTATION_MIN_SDK_VERSION : mRotationMinSdkVersion;
-    }
-
     private boolean signingLineageHas31Support() {
         return mSigningCertificateLineage != null
                 && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
@@ -375,7 +363,6 @@
 
         List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
 
-        int rotationMinSdkVersion = getDevReleaseRotationMinSdkVersion();
         // we have our configs, now touch them up to appropriately cover all SDK levels since APK
         // signature scheme v3 was introduced
         int currentMinSdk = Integer.MAX_VALUE;
@@ -397,7 +384,7 @@
                 // this needs to change
                 config.maxSdkVersion = Integer.MAX_VALUE;
             } else {
-                if (mRotationTargetsDevRelease && currentMinSdk == rotationMinSdkVersion) {
+                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
@@ -414,12 +401,12 @@
             // than that requested to support rotation.
             if (mSigningCertificateLineage != null
                     && ((mRotationTargetsDevRelease
-                        ? config.maxSdkVersion > rotationMinSdkVersion
-                        : config.maxSdkVersion >= rotationMinSdkVersion))) {
+                        ? config.maxSdkVersion > mRotationMinSdkVersion
+                        : config.maxSdkVersion >= mRotationMinSdkVersion))) {
                 config.mSigningCertificateLineage =
                         mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
-                if (config.minSdkVersion < rotationMinSdkVersion) {
-                    config.minSdkVersion = rotationMinSdkVersion;
+                if (config.minSdkVersion < mRotationMinSdkVersion) {
+                    config.minSdkVersion = mRotationMinSdkVersion;
                 }
             }
             // we know that this config will be used, so add it to our result, order doesn't matter
@@ -428,7 +415,7 @@
             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 rotationMinSdkVersion has
+            // 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)
@@ -466,7 +453,6 @@
             return null;
         }
 
-        int rotationMinSdkVersion = getDevReleaseRotationMinSdkVersion();
         List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
         Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator =
                 v3SignerConfigs.iterator();
@@ -474,7 +460,7 @@
             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 >= rotationMinSdkVersion) {
+            if (signerConfig.minSdkVersion >= mRotationMinSdkVersion) {
                 v31SignerConfigs.add(signerConfig);
                 v3SignerIterator.remove();
             }
@@ -1105,7 +1091,7 @@
                 .setRunnablesExecutor(mExecutor)
                 .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
             if (signingLineageHas31Support()) {
-                builder.setRotationMinSdkVersion(getDevReleaseRotationMinSdkVersion());
+                builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
             }
             v3SigningSchemeBlockAndDigests =
                 builder.build().generateApkSignatureSchemeV3BlockAndDigests();
@@ -1824,13 +1810,6 @@
                                 + " v3 without an accompanying SigningCertificateLineage");
             }
 
-            if (mRotationMinSdkVersion == MIN_SDK_WITH_V31_SUPPORT) {
-                // To ensure the APK will install on the currently released platform with the
-                // original signing key, also set the rotation to target a dev release to ensure
-                // the original signing key block targets up through 31.
-                mRotationTargetsDevRelease = true;
-            }
-
             return new DefaultApkSignerEngine(
                     mSignerConfigs,
                     mStampSignerConfig,
diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java
index 587cbd3..b155341 100644
--- a/src/main/java/com/android/apksig/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -730,6 +730,8 @@
             private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
             private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
 
+            private final long mTimestamp;
+
             /*
              * Since this utility is intended just to verify the source stamp, and the source stamp
              * currently only logs warnings to prevent failing the APK signature verification, treat
@@ -744,6 +746,7 @@
                 mCertificateLineage = result.certificateLineage;
                 mErrors.addAll(result.getErrors());
                 mWarnings.addAll(result.getWarnings());
+                mTimestamp = result.timestamp;
             }
 
             /**
@@ -794,6 +797,14 @@
             public List<ApkVerificationIssue> getWarnings() {
                 return mWarnings;
             }
+
+            /**
+             * Returns the epoch timestamp in seconds representing the time this source stamp block
+             * was signed, or 0 if the timestamp is not available.
+             */
+            public long getTimestampEpochSeconds() {
+                return mTimestamp;
+            }
         }
     }
 
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 e0ea365..12e54d0 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -27,6 +27,7 @@
  */
 public class ApkSignerInfo {
     public int index;
+    public long timestamp;
     public List<X509Certificate> certs = new ArrayList<>();
     public List<X509Certificate> certificateLineage = new ArrayList<>();
 
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
index 465fbb0..2a949ad 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
@@ -24,4 +24,11 @@
     public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
     public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
     public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
+    /**
+     * The source stamp timestamp attribute value is an 8-byte little-endian encoded long
+     * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
+     * of the attribute value buffer will be used to read the timestamp, and any additional buffer
+     * space will be ignored.
+     */
+    public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
 }
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 59fa791..aace413 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
@@ -318,6 +318,15 @@
                 byte[] value = ByteBufferUtils.toByteArray(attribute);
                 if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
                     readStampCertificateLineage(value, sourceStampCertificate, result);
+                } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
+                    long timestamp = ByteBuffer.wrap(value).order(
+                            ByteOrder.LITTLE_ENDIAN).getLong();
+                    if (timestamp > 0) {
+                        result.timestamp = timestamp;
+                    } else {
+                        result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+                                timestamp);
+                    }
                 } else {
                     result.addWarning(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
                 }
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 1c1570a..9c00a88 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
@@ -35,6 +35,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -202,6 +203,22 @@
 
     private static 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 (lineage != null) {
             stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
                     lineage.encodeSigningCertificateLineage());
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 834e3cc..6963dd3 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
@@ -28,16 +28,13 @@
 
     public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
     public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
-    // TODO(b/192301300): Once the signing config has been updated to support specifying a
-    // minSdkVersion for rotation this should be updated to T.
-    public static final int DEFAULT_ROTATION_MIN_SDK_VERSION  = AndroidSdkVersion.P;
     /**
-     * The v3.1 signature scheme is initially intended for the T development release, but until
-     * the T SDK is finalized it is using the SDK version of the latest platform release. To support
-     * testing of the v3.1 signature scheme and key rotation on the T development release, the
-     * rotation-min-sdk-version should use the SDK version of Sv2 in the v3.1 signer block.
+     * By default, APK signing key rotation will target T, but packages that have previously
+     * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
+     * --rotation-min-sdk-version parameter when using apksigner or when invoking
+     * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
      */
-    public static final int DEV_RELEASE_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.Sv2;
+    public static final int DEFAULT_ROTATION_MIN_SDK_VERSION  = AndroidSdkVersion.T;
 
     /**
      * This attribute is intended to be written to the V3.0 signer block as an additional attribute
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index adc3a38..83e0499 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -51,7 +51,6 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -228,7 +227,9 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
+
         signGolden(
                 "golden-legacy-aligned-in.apk",
                 new File(outDir, "golden-legacy-aligned-v3-lineage-out.apk"),
@@ -236,6 +237,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         signGolden(
                 "golden-aligned-in.apk",
@@ -244,6 +246,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         signGolden(
@@ -296,6 +299,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         signGolden(
                 "golden-legacy-aligned-in.apk",
@@ -304,6 +308,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         signGolden(
                 "golden-aligned-in.apk",
@@ -312,6 +317,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         signGolden(
@@ -342,6 +348,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         signGolden(
                 "golden-legacy-aligned-in.apk",
@@ -350,6 +357,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         signGolden(
                 "golden-aligned-in.apk",
@@ -358,6 +366,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         signGolden(
@@ -462,6 +471,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-unaligned-in.apk",
@@ -484,6 +494,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-unaligned-in.apk",
@@ -499,6 +510,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         // Uncompressed entries in this input file are aligned by zero-padding the "extra" field, as
@@ -538,6 +550,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-legacy-aligned-in.apk",
@@ -560,6 +573,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-legacy-aligned-in.apk",
@@ -575,6 +589,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         // Uncompressed entries in this input file are aligned by padding the "extra" field, as
@@ -613,6 +628,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-aligned-in.apk",
@@ -635,6 +651,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
         assertGolden(
                 "golden-aligned-in.apk",
@@ -650,6 +667,7 @@
                         .setV1SigningEnabled(true)
                         .setV2SigningEnabled(true)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
     }
 
@@ -952,6 +970,7 @@
                         .setV1SigningEnabled(false)
                         .setV2SigningEnabled(false)
                         .setV3SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
                         .setSigningCertificateLineage(lineage));
 
         // Verifies that an intermediate signer in the lineage is not sufficient to satisfy the
@@ -1588,11 +1607,8 @@
         assertTrue(resultMinRotationT.isVerifiedUsingV31Scheme());
         assertResultContainsSigners(resultMinRotationT, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
             SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
-        // Since T is still under development, it is using the SDK version of the previous platform
-        // release, so to test v3.1 on T the rotation-min-sdk-version must target the SDK version
-        // of Sv2.
         assertV31SignerTargetsMinApiLevel(resultMinRotationT, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
-            V3SchemeConstants.DEV_RELEASE_ROTATION_MIN_SDK_VERSION);
+            AndroidSdkVersion.T);
         assertVerified(resultMinRotationU);
         assertTrue(resultMinRotationU.isVerifiedUsingV31Scheme());
         assertResultContainsSigners(resultMinRotationU, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
@@ -1683,13 +1699,10 @@
                         .setSourceStampSignerConfig(rsa2048OriginalSignerConfig));
         ApkVerifier.Result result = verify(signedApk, null);
 
-        // Since T is still under development, it is using the SDK version of the previous platform
-        // release, so to test v3.1 on T the rotation-min-sdk-version must target the SDK version
-        // of Sv2.
         assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
                 SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
         assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
-                V3SchemeConstants.DEV_RELEASE_ROTATION_MIN_SDK_VERSION);
+                AndroidSdkVersion.T);
         assertSourceStampVerified(signedApk, result);
     }
 
@@ -1770,40 +1783,6 @@
     }
 
     @Test
-    public void testV31_rotationMinSdkVersionT_v30SignerTargetsAtLeast31() throws Exception {
-        // The T development release is currently using the API level of S until its own SDK is
-        // finalized. This requires apksig to sign an APK targeting T for rotation with a V3.1
-        // block that targets API level 31. By default, apksig will decrement the SDK version for
-        // the current signer block and use that as the maxSdkVersion for the next signer; however
-        // this means the original signing key will only target through 30 which would prevent
-        // an APK signed with V3.1 targeting T from installing on a device running S. This test
-        // ensures targeting T will use the rotation-targets-dev-release option so that the APK
-        // can still install on devices with an API level of 31.
-        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.MIN_SDK_WITH_V31_SUPPORT)
-                        .setSigningCertificateLineage(lineage));
-        ApkVerifier.Result result = verify(signedApk, null);
-
-        assertVerified(result);
-        assertTrue(result.isVerifiedUsingV31Scheme());
-        assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease());
-        assertTrue(result.getV3SchemeSigners().get(0).getMaxSdkVersion() >= AndroidSdkVersion.S);
-    }
-
-    @Test
     public void testV31_rotationMinSdkVersionEqualsMinSdkVersion_v3SignerPresent()
             throws Exception {
         // The SDK version for Sv2 (32) is used as the minSdkVersion for the V3.1 signature
@@ -1860,6 +1839,66 @@
     }
 
     @Test
+    public void testV31_rotationMinSdkVersionDefault_rotationTargetsT() throws Exception {
+        // The v3.1 signature scheme was introduced in T to allow developers to target T+ for
+        // rotation due to known issues with rotation on previous platform releases. This test
+        // verifies an APK signed with a rotated signing key defaults to the original signing
+        // key used in the v3 signing block for pre-T devices, and the rotated signing key used
+        // in the v3.1 signing block for T+ devices.
+        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)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        assertTrue(result.isVerifiedUsingV3Scheme());
+        assertTrue(result.isVerifiedUsingV31Scheme());
+        assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+        assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+                AndroidSdkVersion.T);
+    }
+
+    @Test
+    public void testV31_rotationMinSdkVersionP_rotationTargetsP() throws Exception {
+        // While the V3.1 signature scheme will target T by default, a package that has
+        // previously rotated can provide a rotation-min-sdk-version less than T to continue
+        // using the rotated signing key in the v3.0 block.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                        getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(false)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertVerified(result);
+        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
@@ -1919,6 +1958,31 @@
                 SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
     }
 
+    @Test
+    public void testSourceStampTimestamp_signWithSourceStamp_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.
+        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));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertSourceStampVerified(signedApk, result);
+        long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds();
+        assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0);
+    }
+
     /**
      * Asserts the provided {@code signedApk} contains a signature block with the expected
      * {@code byte[]} value and block ID as specified in the {@code expectedBlock}.
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 31ed430..9de2b59 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -1313,6 +1313,123 @@
     }
 
     @Test
+    public void verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception {
+        // A timestamp attribute was added to the source stamp, but verification of APKs that were
+        // generated prior to the addition of the timestamp should still complete successfully,
+        // returning a default value of 0 for the timestamp.
+        ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk");
+
+        assertTrue(verificationResult.isSourceStampVerified());
+        assertEquals(
+                "A value of 0 should be returned for the timestamp when the attribute is not "
+                        + "present",
+                0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception {
+        // Once an APK is signed with a source stamp that contains a valid value for the timestamp
+        // attribute, verification of the source stamp should result in the same value for the
+        // timestamp returned to the verifier.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-valid-timestamp-value.apk");
+
+        assertTrue(verificationResult.isSourceStampVerified());
+        assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue()
+            throws Exception {
+        // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer
+        // as a little-endian long; while a larger buffer will not result in an error, any
+        // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a
+        // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read
+        // successfully.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-valid-timestamp-16-byte-buffer.apk");
+
+        assertTrue(verificationResult.isSourceStampVerified());
+        assertEquals(1645126786,
+                verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails()
+            throws Exception {
+        // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+        // reported to notify the caller to the invalid attribute value. This test verifies a
+        // a warning is reported when the timestamp attribute value is 0.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-invalid-timestamp-value-zero.apk");
+
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails()
+            throws Exception {
+        // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+        // reported to notify the caller to the invalid attribute value. This test verifies a
+        // a warning is reported when the timestamp attribute value is < 0.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-invalid-timestamp-value-less-than-zero.apk");
+
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails()
+            throws Exception {
+        // The source stamp's timestamp attribute value is expected to be written to the first 8
+        // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp
+        // value is not written as a little-endian long to the first 8 bytes of the buffer, then
+        // an error should be reported for the timestamp attribute since the rest of the buffer will
+        // be ignored.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-timestamp-in-last-8-of-16-byte-buffer.apk");
+
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+
+    @Test
+    public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception {
+        // Since the source stamp timestamp attribute value is a long, an attribute value with
+        // insufficient space to hold a long value should result in a warning reported to the user.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-int-timestamp-value.apk");
+
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+    }
+
+    @Test
+    public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception {
+        // The source stamp timestamp attribute is part of the block's signed data; this test
+        // verifies if the value of the timestamp in the stamp block is modified then verification
+        // of the source stamp should fail.
+        ApkVerifier.Result verificationResult = verifySourceStamp(
+                "stamp-valid-timestamp-value-modified.apk");
+
+        assertSourceStampVerificationStatus(verificationResult,
+                SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+        assertSourceStampVerificationFailure(verificationResult,
+                Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+    @Test
     public void apkVerificationIssueAdapter_verifyAllBaseIssuesMapped() throws Exception {
         Field[] fields = ApkVerificationIssue.class.getFields();
         StringBuilder msg = new StringBuilder();
diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
index f5020cc..2e54a8a 100644
--- a/src/test/java/com/android/apksig/SourceStampVerifierTest.java
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -21,10 +21,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.apksig.SourceStampVerifier.Result;
 import com.android.apksig.SourceStampVerifier.Result.SignerInfo;
+import com.android.apksig.internal.util.AndroidSdkVersion;
 import com.android.apksig.internal.util.Resources;
 import com.android.apksig.util.DataSources;
 
@@ -269,6 +271,110 @@
                 ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
     }
 
+    @Test
+    public void verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception {
+        // A timestamp attribute was added to the source stamp, but verification of APKs that were
+        // generated prior to the addition of the timestamp should still complete successfully,
+        // returning a default value of 0 for the timestamp.
+        Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", AndroidSdkVersion.P,
+                AndroidSdkVersion.P);
+
+        assertVerified(verificationResult);
+        assertEquals(
+                "A value of 0 should be returned for the timestamp when the attribute is not "
+                        + "present",
+                0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception {
+        // Once an APK is signed with a source stamp that contains a valid value for the timestamp
+        // attribute, verification of the source stamp should result in the same value for the
+        // timestamp returned to the verifier.
+        Result verificationResult = verifySourceStamp("stamp-valid-timestamp-value.apk");
+
+        assertVerified(verificationResult);
+        assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue()
+            throws Exception {
+        // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer
+        // as a little-endian long; while a larger buffer will not result in an error, any
+        // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a
+        // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read
+        // successfully.
+        Result verificationResult = verifySourceStamp("stamp-valid-timestamp-16-byte-buffer.apk");
+
+        assertEquals(1645126786,
+                verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails()
+            throws Exception {
+        // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+        // reported to notify the caller to the invalid attribute value. This test verifies a
+        // a warning is reported when the timestamp attribute value is 0.
+        Result verificationResult = verifySourceStamp("stamp-invalid-timestamp-value-zero.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails()
+            throws Exception {
+        // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+        // reported to notify the caller to the invalid attribute value. This test verifies a
+        // a warning is reported when the timestamp attribute value is < 0.
+        Result verificationResult = verifySourceStamp(
+                "stamp-invalid-timestamp-value-less-than-zero.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails()
+            throws Exception {
+        // The source stamp's timestamp attribute value is expected to be written to the first 8
+        // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp
+        // value is not written as a little-endian long to the first 8 bytes of the buffer, then
+        // an error should be reported for the timestamp attribute since the rest of the buffer will
+        // be ignored.
+        Result verificationResult = verifySourceStamp(
+                "stamp-timestamp-in-last-8-of-16-byte-buffer.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception {
+        // Since the source stamp timestamp attribute value is a long, an attribute value with
+        // insufficient space to hold a long value should result in a warning reported to the user.
+        Result verificationResult = verifySourceStamp(
+                "stamp-int-timestamp-value.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+    }
+
+    @Test
+    public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception {
+        // The source stamp timestamp attribute is part of the block's signed data; this test
+        // verifies if the value of the timestamp in the stamp block is modified then verification
+        // of the source stamp should fail.
+        Result verificationResult = verifySourceStamp(
+                "stamp-valid-timestamp-value-modified.apk");
+
+        assertSourceStampVerificationFailure(verificationResult,
+                ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+    }
+
+
     private Result verifySourceStamp(String apkFilenameInResources)
             throws Exception {
         return verifySourceStamp(apkFilenameInResources, null, null, null);
diff --git a/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk
new file mode 100644
index 0000000..0cd740c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk
new file mode 100644
index 0000000..f4a189e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk
new file mode 100644
index 0000000..6cfd082
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk
new file mode 100644
index 0000000..260b0ca
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk
new file mode 100644
index 0000000..da9c34d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk
new file mode 100644
index 0000000..eefc148
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk
new file mode 100644
index 0000000..3c6a501
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk
Binary files differ