Snap for 8701376 from 8b14e4f1c30bb890082cc5e2fc8865af07136f28 to mainline-appsearch-release
Change-Id: I1f1739883e4f2a42adf5788f34a009252e848271
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 319b57f..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
@@ -35,13 +35,6 @@
* {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
*/
public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
- /**
- * 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.
- */
- public static final int DEV_RELEASE_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.Sv2;
/**
* 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 9004d80..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;
@@ -1608,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,
@@ -1703,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);
}
@@ -1790,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
@@ -1908,7 +1867,7 @@
assertTrue(result.isVerifiedUsingV31Scheme());
assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
- V3SchemeConstants.DEV_RELEASE_ROTATION_MIN_SDK_VERSION);
+ AndroidSdkVersion.T);
}
@Test
@@ -1999,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