Snap for 9550355 from c0314349dc9f9ccaf9300a39b592af478dc497e9 to sdk-release
Change-Id: I6d1ed7562289c39f50cd5e4d22678de9a13ea347
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index d21515c..bd34ad1 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -22,6 +22,7 @@
import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
@@ -48,6 +49,7 @@
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.List;
/**
@@ -62,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;
@@ -148,6 +152,8 @@
int minSdkVersion = 1;
boolean minSdkVersionSpecified = false;
int maxSdkVersion = Integer.MAX_VALUE;
+ int rotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ boolean rotationTargetsDevRelease = false;
List<SignerParams> signers = new ArrayList<>(1);
SignerParams signerParams = new SignerParams();
SigningCertificateLineage lineage = null;
@@ -176,7 +182,13 @@
minSdkVersionSpecified = true;
} else if ("max-sdk-version".equals(optionName)) {
maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
- } else if ("v1-signing-enabled".equals(optionName)) {
+ } else if ("rotation-min-sdk-version".equals(optionName)) {
+ rotationMinSdkVersion = optionsParser.getRequiredIntValue(
+ "Minimum API Level for Rotation");
+ } else if ("rotation-targets-dev-release".equals(optionName)) {
+ rotationTargetsDevRelease = optionsParser.getOptionalBooleanValue(true);
+ }
+ else if ("v1-signing-enabled".equals(optionName)) {
v1SigningEnabled = optionsParser.getOptionalBooleanValue(true);
} else if ("v2-signing-enabled".equals(optionName)) {
v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
@@ -366,7 +378,9 @@
.setVerityEnabled(verityEnabled)
.setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
.setDebuggableApkPermitted(debuggableApkPermitted)
- .setSigningCertificateLineage(lineage);
+ .setSigningCertificateLineage(lineage)
+ .setMinSdkVersionForRotation(rotationMinSdkVersion)
+ .setRotationTargetsDevRelease(rotationTargetsDevRelease);
if (minSdkVersionSpecified) {
apkSignerBuilder.setMinSdkVersion(minSdkVersion);
}
@@ -454,6 +468,7 @@
int maxSdkVersion = Integer.MAX_VALUE;
boolean maxSdkVersionSpecified = false;
boolean printCerts = false;
+ boolean printCertsPem = false;
boolean verbose = false;
boolean warningsTreatedAsErrors = false;
boolean verifySourceStamp = false;
@@ -472,6 +487,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)) {
@@ -572,6 +594,9 @@
"Verified using v3 scheme (APK Signature Scheme v3): "
+ result.isVerifiedUsingV3Scheme());
System.out.println(
+ "Verified using v3.1 scheme (APK Signature Scheme v3.1): "
+ + result.isVerifiedUsingV31Scheme());
+ System.out.println(
"Verified using v4 scheme (APK Signature Scheme v4): "
+ result.isVerifiedUsingV4Scheme());
System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
@@ -580,14 +605,37 @@
}
}
if (printCerts) {
- int signerNumber = 0;
- for (X509Certificate signerCert : signerCerts) {
- signerNumber++;
- printCertificate(signerCert, "Signer #" + signerNumber, verbose);
+ // The v3.1 signature scheme allows key rotation to target T+ while the original
+ // signing key can still be used with v3.0; if a v3.1 block is present then also
+ // include the target SDK versions for both rotation and the original signing key.
+ if (result.isVerifiedUsingV31Scheme()) {
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer :
+ result.getV31SchemeSigners()) {
+
+ printCertificate(signer.getCertificate(),
+ "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+ + (signer.getRotationTargetsDevRelease()
+ ? " (dev release=true)" : "")
+ + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+ verbose, printCertsPem);
+ }
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ printCertificate(signer.getCertificate(),
+ "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+ + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+ verbose, printCertsPem);
+ }
+ } else {
+ int signerNumber = 0;
+ for (X509Certificate signerCert : signerCerts) {
+ signerNumber++;
+ printCertificate(signerCert, "Signer #" + signerNumber, verbose,
+ printCertsPem);
+ }
}
if (sourceStampInfo != null) {
printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
- verbose);
+ verbose, printCertsPem);
}
}
} else {
@@ -638,6 +686,20 @@
"WARNING: APK Signature Scheme v3 " + signerName + ": " + warning);
}
}
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1) + "(minSdkVersion="
+ + signer.getMinSdkVersion() + ", maxSdkVersion=" + signer.getMaxSdkVersion()
+ + ")";
+ for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
+ System.err.println(
+ "ERROR: APK Signature Scheme v3.1 " + signerName + ": " + error);
+ }
+ for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
+ warningsEncountered = true;
+ warningsOut.println(
+ "WARNING: APK Signature Scheme v3.1 " + signerName + ": " + warning);
+ }
+ }
if (sourceStampInfo != null) {
for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
@@ -798,6 +860,7 @@
boolean verbose = false;
boolean printCerts = false;
+ boolean printCertsPem = false;
boolean lineageUpdated = false;
File inputKeyLineage = null;
File outputKeyLineage = null;
@@ -819,6 +882,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()
@@ -879,7 +949,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);
}
}
@@ -1011,19 +1082,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");
}
@@ -1068,6 +1149,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_rotate.txt b/src/apksigner/java/com/android/apksigner/help_rotate.txt
index ff58372..d19136b 100644
--- a/src/apksigner/java/com/android/apksigner/help_rotate.txt
+++ b/src/apksigner/java/com/android/apksigner/help_rotate.txt
@@ -43,10 +43,17 @@
by a newer signing certificate. By default, the new signer will have all
capabilities, but the capability options can be specified for the new signer
during rotation to act as a default level of trust when moving to a newer
-signing certificate.The capability options accept an optional boolean value of
+signing certificate. The capability options accept an optional boolean value of
true or false; if this value is not specified then the option will default to
true.
+Prior to Android 12, if multiple apps shared a common signer in their signing lineage
+with distinct capabilities assigned, a bug in the platform would cause the capabilities
+declared for this signer in one of the app's signing lineage to be assigned to this same
+common signer in the lineage of the rest of the apps. Apps that use the default capabilities,
+or that assign the same capabilities to a common signer in their lineage, are not impacted
+by this bug.
+
--ks Load private key and certificate chain from the Java
KeyStore initialized from the specified file. NONE means
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index 5266ad9..dc5f6cc 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -63,6 +63,24 @@
--max-sdk-version Highest API Level on which this APK's signatures will be
verified. By default, the highest possible value is used.
+--rotation-min-sdk-version Lowest API Level for which an APK's rotated signing
+ key should be used to produce the APK's signature. The
+ original signing key for the APK will be used for all
+ previous platform versions. Specifying a value <= 32
+ (Android Sv2) will result in the original V3 signing block
+ being used without platform targeting. By default,
+ rotated signing keys will be used with the V3.1 signing
+ block which supports Android T+.
+
+--rotation-targets-dev-release The specified rotation-min-sdk-version is intended
+ for a platform release under development. During development
+ of a new platform, the API Level of the previously released
+ platform is used as the API Level of the development
+ platform until the SDK is finalized. This flag allows
+ targeting signing key rotation to a development platform
+ with API Level X while preventing the rotated key from being
+ used on the latest release platform with API Level X.
+
--debuggable-apk-permitted Whether to permit signing android:debuggable="true"
APKs. Android disables some of its security protections
for such apps. For example, anybody with ADB shell access
diff --git a/src/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/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index d6c7799..f225ae9 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -17,11 +17,14 @@
package com.android.apksig;
import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+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;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.util.ByteBufferDataSource;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.EocdRecord;
@@ -91,6 +94,8 @@
private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
private final boolean mForceSourceStampOverwrite;
private final Integer mMinSdkVersion;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
private final boolean mV1SigningEnabled;
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
@@ -121,6 +126,8 @@
SigningCertificateLineage sourceStampSigningCertificateLineage,
boolean forceSourceStampOverwrite,
Integer minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
@@ -145,6 +152,8 @@
mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
mForceSourceStampOverwrite = forceSourceStampOverwrite;
mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
@@ -301,7 +310,9 @@
.setVerityEnabled(mVerityEnabled)
.setDebuggableApkPermitted(mDebuggableApkPermitted)
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
- .setSigningCertificateLineage(mSigningCertificateLineage);
+ .setSigningCertificateLineage(mSigningCertificateLineage)
+ .setMinSdkVersionForRotation(mRotationMinSdkVersion)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease);
if (mCreatedBy != null) {
signerEngineBuilder.setCreatedBy(mCreatedBy);
}
@@ -1128,6 +1139,8 @@
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy;
private Integer mMinSdkVersion;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
private final ApkSignerEngine mSignerEngine;
@@ -1336,6 +1349,58 @@
}
/**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop. This method is useful for overriding the
+ * default behavior where Android T is set as the minimum API level for rotation.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ checkInitializedWithoutEngine();
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
*
* <p>By default, whether APK is signed using JAR signing is determined by {@code
@@ -1588,6 +1653,8 @@
mSourceStampSigningCertificateLineage,
mForceSourceStampOverwrite,
mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
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 354dfbd..fc28864 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -24,6 +24,7 @@
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
@@ -201,23 +202,58 @@
Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
if (maxSdkVersion >= AndroidSdkVersion.N) {
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
- // Android P and newer attempts to verify APKs using APK Signature Scheme v3
- if (maxSdkVersion >= AndroidSdkVersion.P) {
+ // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+ // also includes stripping protection for the minimum SDK version on which the rotated
+ // signing key should be used.
+ int rotationMinSdkVersion = 0;
+ if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
try {
- ApkSigningBlockUtils.Result v3Result =
- V3SchemeVerifier.verify(
- executor,
- apk,
- zipSections,
- Math.max(minSdkVersion, AndroidSdkVersion.P),
- maxSdkVersion);
+ ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .build()
+ .verify();
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ rotationMinSdkVersion = v31Result.signers.stream().mapToInt(
+ signer -> signer.minSdkVersion).min().orElse(0);
+ result.mergeFrom(v31Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31,
+ getApkContentDigestsFromSigningSchemeResult(v31Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3.1 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+ // Android P and newer attempts to verify APKs using APK Signature Scheme v3
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) {
+ try {
+ V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (rotationMinSdkVersion > 0) {
+ builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+ }
+ ApkSigningBlockUtils.Result v3Result = builder.build().verify();
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
result.mergeFrom(v3Result);
signatureSchemeApkContentDigests.put(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
getApkContentDigestsFromSigningSchemeResult(v3Result));
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
- // v3 signature not required
+ // v3 signature not required unless a v3.1 signature was found as a v3.1
+ // signature is intended to support key rotation on T+ with the v3 signature
+ // containing the original signing key.
+ if (foundApkSigSchemeIds.contains(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
}
if (result.containsErrors()) {
return result;
@@ -454,9 +490,6 @@
// The apkDigest field in the v4 signature should match the selected v2/v3.
if (result.isVerifiedUsingV4Scheme()) {
List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
- if (v4Signers.size() != 1) {
- result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
- }
List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
v4Signers.get(0).getContentDigests();
@@ -466,21 +499,22 @@
final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
if (result.isVerifiedUsingV3Scheme()) {
- List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
- if (v3Signers.size() != 1) {
+ int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1;
+ if (v4Signers.size() != expectedSize) {
result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
}
- // Compare certificates.
- checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result);
-
- // Compare digests.
- final byte[] digestFromV3 = pickBestDigestForV4(
- v3Signers.get(0).getContentDigests());
- if (!Arrays.equals(digestFromV4, digestFromV3)) {
- result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
+ result);
+ if (result.isVerifiedUsingV31Scheme()) {
+ checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
+ digestFromV4, result);
}
} else if (result.isVerifiedUsingV2Scheme()) {
+ if (v4Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
if (v2Signers.size() != 1) {
result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
@@ -527,7 +561,7 @@
// Allow this case to fall through to the next as a signature satisfying a
// later scheme version will also satisfy this requirement.
case VERSION_APK_SIGNATURE_SCHEME_V3:
- if (result.isVerifiedUsingV3Scheme()) {
+ if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) {
break;
}
result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
@@ -543,7 +577,10 @@
// Verified
result.setVerified();
- if (result.isVerifiedUsingV3Scheme()) {
+ if (result.isVerifiedUsingV31Scheme()) {
+ List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners();
+ result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV3Scheme()) {
List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate());
} else if (result.isVerifiedUsingV2Scheme()) {
@@ -895,6 +932,22 @@
return result;
}
+ private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers,
+ List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) {
+ if (v3Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV3)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ }
+
private static void checkV4Certificate(List<X509Certificate> v4Certs,
List<X509Certificate> v2v3Certs, Result result) {
try {
@@ -1005,6 +1058,7 @@
private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>();
private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
private SourceStampInfo mSourceStampInfo;
@@ -1012,6 +1066,7 @@
private boolean mVerifiedUsingV1Scheme;
private boolean mVerifiedUsingV2Scheme;
private boolean mVerifiedUsingV3Scheme;
+ private boolean mVerifiedUsingV31Scheme;
private boolean mVerifiedUsingV4Scheme;
private boolean mSourceStampVerified;
private boolean mWarningsAsErrors;
@@ -1050,6 +1105,13 @@
}
/**
+ * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+ */
+ public boolean isVerifiedUsingV31Scheme() {
+ return mVerifiedUsingV31Scheme;
+ }
+
+ /**
* Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
*/
public boolean isVerifiedUsingV4Scheme() {
@@ -1115,7 +1177,23 @@
return mV3SchemeSigners;
}
- private List<V4SchemeSignerInfo> getV4SchemeSigners() {
+ /**
+ * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+ * signature.
+ *
+ * <note> Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single
+ * signer identities.</note>
+ */
+ public List<V3SchemeSignerInfo> getV31SchemeSigners() {
+ return mV31SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v4 signers associated with the APK's
+ * signature.
+ */
+ public List<V4SchemeSignerInfo> getV4SchemeSigners() {
return mV4SchemeSigners;
}
@@ -1210,6 +1288,16 @@
for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
}
+ // Do not overwrite a previously set lineage from a v3.1 signing block.
+ if (mSigningCertificateLineage == null) {
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31:
+ mVerifiedUsingV31Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
mSigningCertificateLineage = source.signingCertificateLineage;
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
@@ -1497,6 +1585,9 @@
private final List<IssueWithParams> mWarnings;
private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
mContentDigests;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
mIndex = result.index;
@@ -1504,6 +1595,11 @@
mErrors = result.getErrors();
mWarnings = result.getWarnings();
mContentDigests = result.contentDigests;
+ mMinSdkVersion = result.minSdkVersion;
+ mMaxSdkVersion = result.maxSdkVersion;
+ mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt(
+ attribute -> attribute.getId()).anyMatch(
+ attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
}
/**
@@ -1549,6 +1645,33 @@
public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
return mContentDigests;
}
+
+ /**
+ * Returns the minimum SDK version on which this signer should be verified.
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /**
+ * Returns the maximum SDK version on which this signer should be verified.
+ */
+ public int getMaxSdkVersion() {
+ return mMaxSdkVersion;
+ }
+
+ /**
+ * Returns whether rotation is targeting a development release.
+ *
+ * <p>A development release uses the SDK version of the previously released platform
+ * until the SDK of the development release is finalized. To allow rotation to target
+ * a development release after T, this attribute must be set to ensure rotation is
+ * used on the development release but ignored on the released platform with the same
+ * API level.
+ */
+ public boolean getRotationTargetsDevRelease() {
+ return mRotationTargetsDevRelease;
+ }
}
/**
@@ -1644,6 +1767,8 @@
private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+ private final long mTimestamp;
+
private SourceStampInfo(ApkSignerInfo result) {
mCertificates = result.certs;
mCertificateLineage = result.certificateLineage;
@@ -1657,6 +1782,7 @@
mSourceStampVerificationStatus =
SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
}
+ mTimestamp = result.timestamp;
}
SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
@@ -1665,6 +1791,7 @@
mErrors = Collections.emptyList();
mWarnings = Collections.emptyList();
mSourceStampVerificationStatus = sourceStampVerificationStatus;
+ mTimestamp = 0;
}
/**
@@ -1704,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;
+ }
}
}
@@ -2527,6 +2662,61 @@
+ " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
/**
+ * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+ * was not found.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * </ul>
+ */
+ V31_BLOCK_MISSING(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but a v3.1 block was not found"),
+
+ /**
+ * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+ * targeting rotation in the v3.1 signer block.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_MISMATCH(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+ /**
+ * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+ * SDK version stripping protection attribute was not written to the v3 signer.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_ATTR_MISSING(
+ "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+ + " not contain the attribute to detect if this signature is stripped"),
+
+ /**
+ * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+ * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+ * same as the SDK version for rotation then this should be written to a v3.0 block.
+ */
+ V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+ "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+ /**
+ * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in
+ * the signer; this attribute is only intended for v3.1 signers to indicate they should be
+ * targeting the next development release that is using the SDK version of the previously
+ * released platform SDK version.
+ */
+ V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER(
+ "The rotation-targets-dev-release attribute is only supported on v3.1 signers; "
+ + "this attribute will be ignored by the platform in a v3.0 signer"),
+
+ /**
* APK Signing Block contains an unknown entry.
*
* <ul>
@@ -2822,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.
@@ -3117,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/Constants.java b/src/main/java/com/android/apksig/Constants.java
index 32c7375..f64064c 100644
--- a/src/main/java/com/android/apksig/Constants.java
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -32,6 +32,7 @@
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
@@ -41,6 +42,8 @@
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
public static final int V1_SOURCE_STAMP_BLOCK_ID =
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index e2256da..f25bc59 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -22,6 +22,8 @@
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
@@ -34,6 +36,7 @@
import com.android.apksig.internal.apk.v1.V1SchemeSigner;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v4.V4SchemeSigner;
import com.android.apksig.internal.apk.v4.V4Signature;
@@ -66,6 +69,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -102,6 +106,8 @@
private final List<SignerConfig> mSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
@@ -184,6 +190,8 @@
SignerConfig sourceStampSignerConfig,
SigningCertificateLineage sourceStampSigningCertificateLineage,
int minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
@@ -211,6 +219,8 @@
mSourceStampSignerConfig = sourceStampSignerConfig;
mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
mSigningCertificateLineage = signingCertificateLineage;
if (v1SigningEnabled) {
@@ -328,8 +338,29 @@
}
}
+ private boolean signingLineageHas31Support() {
+ return mSigningCertificateLineage != null
+ && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+ && mMinSdkVersion < mRotationMinSdkVersion;
+ }
+
private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
+ // While the V3 signature scheme supports rotation, it is possible for a caller to specify
+ // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1;
+ // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block
+ // will use the original signing key.
+ if (signingLineageHas31Support()) {
+ SigningCertificateLineage subLineage = mSigningCertificateLineage
+ .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage"
+ + " for the v3.0 signing block is missing. Please provide"
+ + " the oldest signer to enable v3.1 signing.");
+ }
+ }
+
List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
// we have our configs, now touch them up to appropriately cover all SDK levels since APK
@@ -353,20 +384,43 @@
// this needs to change
config.maxSdkVersion = Integer.MAX_VALUE;
} else {
- // otherwise, we only want to use this signer up to the minimum platform version
- // on which a newer one is acceptable
- config.maxSdkVersion = currentMinSdk - 1;
+ if (mRotationTargetsDevRelease && currentMinSdk == mRotationMinSdkVersion) {
+ // The currentMinSdk is both the SDK version for the active development release
+ // as well as the most recent released platform. To ensure the v3.0 signer will
+ // target the released platform, overlap the maxSdkVersion for the v3.0 signer
+ // with the minSdkVersion of the rotated signer in the v3.1 block
+ config.maxSdkVersion = currentMinSdk;
+ } else {
+ // otherwise, we only want to use this signer up to the minimum platform version
+ // on which a newer one is acceptable
+ config.maxSdkVersion = currentMinSdk - 1;
+ }
}
config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
- if (mSigningCertificateLineage != null) {
+ // Only use a rotated key and signing lineage if the config's max SDK version is greater
+ // than that requested to support rotation.
+ if (mSigningCertificateLineage != null
+ && ((mRotationTargetsDevRelease
+ ? config.maxSdkVersion > mRotationMinSdkVersion
+ : config.maxSdkVersion >= mRotationMinSdkVersion))) {
config.mSigningCertificateLineage =
mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
+ if (config.minSdkVersion < mRotationMinSdkVersion) {
+ config.minSdkVersion = mRotationMinSdkVersion;
+ }
}
// we know that this config will be used, so add it to our result, order doesn't matter
// at this point (and likely only one will be needed
processedConfigs.add(config);
currentMinSdk = config.minSdkVersion;
- if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) {
+ // If the rotation is targeting a development release and this is the v3.1 signer, then
+ // the minSdkVersion of this signer should equal the maxSdkVersion of the next signer;
+ // this ensures a package with the minSdkVersion set to the mRotationMinSdkVersion has
+ // a v3.0 block with the min / max SDK version set to this same minSdkVersion from the
+ // v3.1 block.
+ if ((mRotationTargetsDevRelease && currentMinSdk < mMinSdkVersion)
+ || (!mRotationTargetsDevRelease && currentMinSdk <= mMinSdkVersion)
+ || currentMinSdk <= AndroidSdkVersion.P) {
// this satisfies all we need, stop here
break;
}
@@ -387,17 +441,42 @@
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
}
- private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() throws InvalidKeyException {
- List<ApkSigningBlockUtils.SignerConfig> configs = createSigningBlockSignerConfigs(true,
+ private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
+ List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
+ // If the signing key has been rotated, the caller has requested to use the rotated
+ // signing key starting from an SDK version where v3.1 is supported, and the minimum
+ // SDK version for the APK is less than the requested rotation minimum, then the APK
+ // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0
+ // scheme with the original signing key. If the APK's minSdkVersion is >= the requested
+ // SDK version for rotation then just use the v3.0 signing block for this.
+ if (!signingLineageHas31Support()) {
+ return null;
+ }
+
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
+ Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator =
+ v3SignerConfigs.iterator();
+ while (v3SignerIterator.hasNext()) {
+ ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+ // All signing configs with a min SDK version that supports v3.1 should be used
+ // in the v3.1 signing block and removed from the v3.0 block.
+ if (signerConfig.minSdkVersion >= mRotationMinSdkVersion) {
+ v31SignerConfigs.add(signerConfig);
+ v3SignerIterator.remove();
+ }
+ }
+ return v31SignerConfigs;
+ }
+
+ private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+ List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
- if (configs.size() != 1) {
- // V4 only uses signer config to connect back to v3. Use the same filtering logic.
- configs = processV3Configs(configs);
+ if (v4Configs.size() != 1) {
+ // V4 uses signer config to connect back to v3. Use the same filtering logic.
+ v4Configs = processV3Configs(v4Configs);
}
- if (configs.size() != 1) {
- throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
- }
- return configs.get(0);
+ List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs);
+ return new V4SchemeSigner.SignerConfig(v4Configs, v41configs);
}
private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
@@ -993,13 +1072,29 @@
invalidateV3Signature();
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
createV3SignerConfigs(apkSigningBlockPaddingSupported);
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs(
+ v3SignerConfigs);
+ if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) {
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ v31SigningSchemeBlockAndDigests =
+ new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+ v31SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+ zipCentralDirectory, eocd, v3SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (signingLineageHas31Support()) {
+ builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
+ }
v3SigningSchemeBlockAndDigests =
- V3SchemeSigner.generateApkSignatureSchemeV3Block(
- mExecutor,
- beforeCentralDir,
- zipCentralDirectory,
- eocd,
- v3SignerConfigs);
+ builder.build().generateApkSignatureSchemeV3BlockAndDigests();
signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (isEligibleForSourceStamp()) {
@@ -1071,7 +1166,7 @@
throw new SignatureException("Missing V4 output file.");
}
try {
- ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
} catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
if (ignoreFailures) {
@@ -1088,7 +1183,7 @@
throw new SignatureException("Missing V4 output streams.");
}
try {
- ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
Pair<V4Signature, byte[]> pair =
V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
pair.getFirst().writeTo(sigOutput);
@@ -1630,6 +1725,8 @@
private boolean mV1SigningEnabled = true;
private boolean mV2SigningEnabled = true;
private boolean mV3SigningEnabled = true;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
private boolean mVerityEnabled = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
@@ -1718,6 +1815,8 @@
mStampSignerConfig,
mSourceStampSigningCertificateLineage,
mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
@@ -1840,5 +1939,49 @@
}
return this;
}
+
+ /**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop.
+ *
+ * <p>By default, if a signing lineage is specified with {@link
+ * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+ * V3.1 will be used to only apply the rotation on devices running Android T+.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
}
}
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 6c505be..43b7f5e 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -191,41 +191,62 @@
*/
public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
throws IOException, ApkFormatException {
- SignatureInfo signatureInfo;
+ ApkUtils.ZipSections zipSections;
try {
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
- ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
- signatureInfo =
- ApkSigningBlockUtils.findSignature(apk, zipSections,
- V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+ zipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException(e.getMessage());
- } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ }
+
+ List<SignatureInfo> signatureInfoList = new ArrayList<>();
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if there's only a V3 signature block.
+ }
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if the provided APK is not signed with the v3 signature scheme
+ }
+ if (signatureInfoList.isEmpty()) {
throw new IllegalArgumentException(
"The provided APK does not contain a valid V3 signature block.");
}
- // FORMAT:
- // * length-prefixed sequence of length-prefixed signers:
- // * length-prefixed signed data
- // * minSDK
- // * maxSDK
- // * length-prefixed sequence of length-prefixed signatures
- // * length-prefixed public key
- ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
List<SigningCertificateLineage> lineages = new ArrayList<>(1);
- while (signers.hasRemaining()) {
- ByteBuffer signer = getLengthPrefixedSlice(signers);
- ByteBuffer signedData = getLengthPrefixedSlice(signer);
- try {
- SigningCertificateLineage lineage = readFromSignedData(signedData);
- lineages.add(lineage);
- } catch (IllegalArgumentException ignored) {
- // The current signer block does not contain a valid lineage, but it is possible
- // another block will.
+ for (SignatureInfo signatureInfo : signatureInfoList) {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signers:
+ // * length-prefixed signed data
+ // * minSDK
+ // * maxSDK
+ // * length-prefixed sequence of length-prefixed signatures
+ // * length-prefixed public key
+ ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ while (signers.hasRemaining()) {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ try {
+ SigningCertificateLineage lineage = readFromSignedData(signedData);
+ lineages.add(lineage);
+ } catch (IllegalArgumentException ignored) {
+ // The current signer block does not contain a valid lineage, but it is possible
+ // another block will.
+ }
}
}
+
SigningCertificateLineage result;
if (lineages.isEmpty()) {
throw new IllegalArgumentException(
diff --git a/src/main/java/com/android/apksig/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/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 5b34849..44dcc79 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -104,6 +104,7 @@
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
/**
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/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 9cd7b1f..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
@@ -21,6 +21,7 @@
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSupportedSignature;
@@ -137,6 +138,13 @@
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
signatureSchemeApkDigests.entrySet()) {
+ // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
+ // v3.0 block must always be present with a v3.1 block is it sufficient to just use the
+ // v3.0 block?
+ if (signatureSchemeApkDigest.getKey()
+ == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
+ continue;
+ }
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
return;
@@ -310,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 3b70aa0..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
@@ -16,10 +16,40 @@
package com.android.apksig.internal.apk.v3;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+
/** Constants used by the V3 Signature Scheme signing and verification. */
public class V3SchemeConstants {
private V3SchemeConstants() {}
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+ public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
+ public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
+ /**
+ * 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 DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
+
+ /**
+ * This attribute is intended to be written to the V3.0 signer block as an additional attribute
+ * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
+ * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
+ * for rotation in the v3.1 signing block is not X, then the APK should be rejected.
+ */
+ public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
+
+ /**
+ * This attribute is written to the V3.1 signer block as an additional attribute to signify that
+ * the rotation-min-sdk-version is targeting a development release. This is required to support
+ * testing rotation on new development releases as the previous platform release SDK version
+ * is used as the development release SDK version until the development release SDK is
+ * finalized.
+ */
+ public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
index 04260d5..ee5d3b4 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -24,12 +24,14 @@
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -44,6 +46,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.OptionalInt;
/**
* APK Signature Scheme v3 signer.
@@ -56,13 +59,37 @@
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
* it can prove the new siging certificate was signed by the old.
*/
-public abstract class V3SchemeSigner {
+public class V3SchemeSigner {
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
- /** Hidden constructor to prevent instantiation. */
- private V3SchemeSigner() {}
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+ private final int mBlockId;
+ private final OptionalInt mOptionalRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+
+ private V3SchemeSigner(DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ RunnablesExecutor executor,
+ int blockId,
+ OptionalInt optionalRotationMinSdkVersion,
+ boolean rotationTargetsDevRelease) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ mExecutor = executor;
+ mBlockId = blockId;
+ mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ }
/**
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
@@ -129,21 +156,18 @@
}
}
- public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
- generateApkSignatureSchemeV3Block(
- RunnablesExecutor executor,
- DataSource beforeCentralDir,
- DataSource centralDir,
- DataSource eocd,
- List<SignerConfig> signerConfigs)
- throws IOException, InvalidKeyException, NoSuchAlgorithmException,
- SignatureException {
- Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
- ApkSigningBlockUtils.computeContentDigests(
- executor, beforeCentralDir, centralDir, eocd, signerConfigs);
- return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
- generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
- digestInfo.getSecond());
+ public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
}
public static byte[] generateV3SignerAttribute(
@@ -162,14 +186,62 @@
return result.array();
}
- private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
- List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ int rotationMinSdkVersion) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - int32 representing minimum SDK version for rotation
+ int payloadSize = 4 + 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
+ result.putInt(rotationMinSdkVersion);
+ return result.array();
+ }
+
+ private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - No value is used for this attribute
+ int payloadSize = 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+ return result.array();
+ }
+
+ /**
+ * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
+ * signing scheme block and digests based on the parameters provided to the {@link Builder}.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
+ * @throws SignatureException if an error occurs when computing digests or generating
+ * signatures
+ */
+ public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+ ApkSigningBlockUtils.computeContentDigests(
+ mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
+ return new SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
+ }
+
+ private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+ Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
- List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+ List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
int signerNumber = 0;
- for (SignerConfig signerConfig : signerConfigs) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
@@ -187,10 +259,10 @@
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
- V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ mBlockId);
}
- private static byte[] generateSignerBlock(
+ private byte[] generateSignerBlock(
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
@@ -240,7 +312,7 @@
return encodeSigner(signer);
}
- private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
+ private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
byte[] signatures =
encodeAsLengthPrefixedElement(
@@ -269,7 +341,7 @@
return result.array();
}
- private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
+ private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
byte[] digests =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
@@ -305,11 +377,26 @@
return result.array();
}
- private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
- if (signerConfig.mSigningCertificateLineage == null) {
- return new byte[0];
+ private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
+ if (signerConfig.mSigningCertificateLineage != null) {
+ byte[] lineageAttr = generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
+ // If this rotation is not targeting a development release, or if this is not a v3.1
+ // signer block then just return the lineage attribute.
+ if (!mRotationTargetsDevRelease
+ || mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ return lineageAttr;
+ }
+ byte[] devReleaseRotationAttr = generateV31RotationTargetsDevReleaseAttribute();
+ byte[] attributes = new byte[lineageAttr.length + devReleaseRotationAttr.length];
+ System.arraycopy(lineageAttr, 0, attributes, 0, lineageAttr.length);
+ System.arraycopy(devReleaseRotationAttr, 0, attributes, lineageAttr.length,
+ devReleaseRotationAttr.length);
+ return attributes;
+ } else if (mOptionalRotationMinSdkVersion.isPresent()) {
+ return generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ mOptionalRotationMinSdkVersion.getAsInt());
}
- return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
+ return new byte[0];
}
private static final class V3SignatureSchemeBlock {
@@ -329,4 +416,97 @@
public byte[] additionalAttributes;
}
}
+
+ /** Builder of {@link V3SchemeSigner} instances. */
+ public static class Builder {
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+ private boolean mRotationTargetsDevRelease = false;
+
+ /**
+ * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
+ * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
+ * be used to sign the APK.
+ */
+ public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
+ List<SignerConfig> signerConfigs) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the {@code blockId} to be used for the V3 signature block.
+ *
+ * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
+ * signer's block.
+ *
+ * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
+ * is not modified or removed from the APK's signature block.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets whether the minimum SDK version of a signer is intended to target a development
+ * release; this is primarily required after the T SDK is finalized, and an APK needs to
+ * target U during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. A signer with a minimum SDK version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeSigner} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeSigner build() {
+ return new V3SchemeSigner(mBeforeCentralDir,
+ mCentralDir,
+ mEocd,
+ mSignerConfigs,
+ mExecutor,
+ mBlockId,
+ mOptionalRotationMinSdkVersion,
+ mRotationTargetsDevRelease);
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index ea93194..956027f 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -28,7 +28,6 @@
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
-import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
@@ -38,6 +37,7 @@
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -54,6 +54,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.OptionalInt;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -67,9 +68,42 @@
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
-public abstract class V3SchemeVerifier {
- /** Hidden constructor to prevent instantiation. */
- private V3SchemeVerifier() {}
+public class V3SchemeVerifier {
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mApk;
+ private final ApkUtils.ZipSections mZipSections;
+ private final ApkSigningBlockUtils.Result mResult;
+ private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final int mBlockId;
+ private final OptionalInt mOptionalRotationMinSdkVersion;
+ private final boolean mFullVerification;
+
+ private ByteBuffer mApkSignatureSchemeV3Block;
+
+ private V3SchemeVerifier(
+ RunnablesExecutor executor,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ ApkSigningBlockUtils.Result result,
+ int minSdkVersion,
+ int maxSdkVersion,
+ int blockId,
+ OptionalInt optionalRotationMinSdkVersion,
+ boolean fullVerification) {
+ mExecutor = executor;
+ mApk = apk;
+ mZipSections = zipSections;
+ mContentDigestsToVerify = contentDigestsToVerify;
+ mResult = result;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ mBlockId = blockId;
+ mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+ mFullVerification = fullVerification;
+ }
/**
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
@@ -84,7 +118,10 @@
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
- * @throws ApkFormatException if the APK is malformed
+ * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
+ * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
+ * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
+ *
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws SignatureNotFoundException if no APK Signature Scheme v3
@@ -98,34 +135,11 @@
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
- ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
- SignatureInfo signatureInfo =
- ApkSigningBlockUtils.findSignature(apk, zipSections,
- V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
-
- DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
- DataSource centralDir =
- apk.slice(
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
- ByteBuffer eocd = signatureInfo.eocd;
-
- // v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
- // platforms
- if (minSdkVersion < AndroidSdkVersion.P) {
- minSdkVersion = AndroidSdkVersion.P;
- }
-
- verify(executor,
- beforeApkSigningBlock,
- signatureInfo.signatureBlock,
- centralDir,
- eocd,
- minSdkVersion,
- maxSdkVersion,
- result);
- return result;
+ return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .verify();
}
/**
@@ -134,33 +148,40 @@
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
* int)} for more information about the contract of this method.
*
- * @param result result populated by this method with interesting information about the APK,
- * such as information about signers, and verification errors and warnings.
+ * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
+ * APK, such as information about signers, and verification errors and warnings
*/
- private static void verify(
- RunnablesExecutor executor,
- DataSource beforeApkSigningBlock,
- ByteBuffer apkSignatureSchemeV3Block,
- DataSource centralDir,
- ByteBuffer eocd,
- int minSdkVersion,
- int maxSdkVersion,
- ApkSigningBlockUtils.Result result)
- throws IOException, NoSuchAlgorithmException {
- Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
- parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
-
- if (result.containsErrors()) {
- return;
+ public ApkSigningBlockUtils.Result verify()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ if (mApk == null || mZipSections == null) {
+ throw new IllegalStateException(
+ "A non-null apk and zip sections must be specified to verify an APK's v3 "
+ + "signatures");
}
- ApkSigningBlockUtils.verifyIntegrity(
- executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+
+ DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ mApk.slice(
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+ ByteBuffer eocd = signatureInfo.eocd;
+
+ parseSigners();
+
+ if (mResult.containsErrors()) {
+ return mResult;
+ }
+ ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
+ mContentDigestsToVerify, mResult);
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
// longest SigningCertificateHistory, if present, corresponds to the newest platform
// versions
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
- for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
sortedSigners.put(signer.minSdkVersion, signer);
}
@@ -170,7 +191,7 @@
int lastLineageSize = 0;
// while we're iterating through the signers, build up the list of lineages
- List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
+ List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
int currentMin = signer.minSdkVersion;
@@ -180,7 +201,7 @@
firstMin = currentMin;
} else {
if (currentMin != lastMax + 1) {
- result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
+ mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
break;
}
}
@@ -190,7 +211,7 @@
if (signer.signingCertificateLineage != null) {
int currLineageSize = signer.signingCertificateLineage.size();
if (currLineageSize < lastLineageSize) {
- result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
break;
}
lastLineageSize = currLineageSize;
@@ -198,20 +219,24 @@
}
}
- // make sure we support our desired sdk ranges
- if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
- result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
+ // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
+ // then the max level only needs to support up to that sdk version for rotation.
+ if (firstMin > mMinSdkVersion
+ || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
+ ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
+ mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
}
try {
- result.signingCertificateLineage =
+ mResult.signingCertificateLineage =
SigningCertificateLineage.consolidateLineages(lineages);
} catch (IllegalArgumentException e) {
- result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
}
- if (!result.containsErrors()) {
- result.verified = true;
+ if (!mResult.containsErrors()) {
+ mResult.verified = true;
}
+ return mResult;
}
/**
@@ -230,16 +255,49 @@
ByteBuffer apkSignatureSchemeV3Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+ try {
+ new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
+ .setResult(result)
+ .setContentDigestsToVerify(contentDigestsToVerify)
+ .setFullVerification(false)
+ .build()
+ .parseSigners();
+ } catch (IOException | SignatureNotFoundException e) {
+ // This should never occur since the apkSignatureSchemeV3Block was already provided.
+ throw new IllegalStateException("An exception was encountered when attempting to parse"
+ + " the signers from the provided APK Signature Scheme v3 block", e);
+ }
+ }
+
+ /**
+ * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
+ * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
+ * returned {@link ApkSigningBlockUtils.Result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+ * However, this does not verify the integrity of the rest of the APK but rather simply reports
+ * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
+ *
+ * <p>This method adds one or more errors to the returned {@code Result} if a verification error
+ * is encountered when parsing the signers.
+ */
+ public ApkSigningBlockUtils.Result parseSigners()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
ByteBuffer signers;
try {
- signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
+ if (mApkSignatureSchemeV3Block == null) {
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+ }
+ signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
} catch (ApkFormatException e) {
- result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
- return;
+ mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
+ return mResult;
}
if (!signers.hasRemaining()) {
- result.addError(Issue.V3_SIG_NO_SIGNERS);
- return;
+ mResult.addError(Issue.V3_SIG_NO_SIGNERS);
+ return mResult;
}
CertificateFactory certFactory;
@@ -255,15 +313,16 @@
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
- result.signers.add(signerInfo);
+ mResult.signers.add(signerInfo);
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
- parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
+ parseSigner(signer, certFactory, signerInfo);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
- return;
+ return mResult;
}
}
+ return mResult;
}
/**
@@ -278,12 +337,9 @@
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
- private static void parseSigner(
- ByteBuffer signerBlock,
- CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
- Set<ContentDigestAlgorithm> contentDigestsToVerify)
- throws ApkFormatException, NoSuchAlgorithmException {
+ private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result)
+ throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
@@ -372,7 +428,7 @@
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
- contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+ mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
@@ -482,6 +538,7 @@
// Parse the additional attributes block.
int additionalAttributeCount = 0;
+ boolean rotationAttrFound = false;
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
@@ -509,6 +566,31 @@
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
}
+ } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
+ rotationAttrFound = true;
+ // API targeting for rotation was added with V3.1; if the maxSdkVersion
+ // does not support v3.1 then ignore this attribute.
+ if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
+ && mFullVerification) {
+ int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
+ .order(ByteOrder.LITTLE_ENDIAN).getInt();
+ if (mOptionalRotationMinSdkVersion.isPresent()) {
+ int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
+ if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
+ result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
+ attrRotationMinSdkVersion, rotationMinSdkVersion);
+ }
+ } else {
+ result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
+ }
+ }
+ } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
+ // This attribute should only be used by a v3.1 signer to indicate rotation
+ // is targeting the development release that is using the SDK version of the
+ // previously released platform version.
+ if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+ }
} else {
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
@@ -518,5 +600,168 @@
return;
}
}
+ if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
+ result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
+ mOptionalRotationMinSdkVersion.getAsInt());
+ }
+ }
+
+ /** Builder of {@link V3SchemeVerifier} instances. */
+ public static class Builder {
+ private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+ private DataSource mApk;
+ private ApkUtils.ZipSections mZipSections;
+ private ByteBuffer mApkSignatureSchemeV3Block;
+ private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private ApkSigningBlockUtils.Result mResult;
+ private int mMinSdkVersion;
+ private int mMaxSdkVersion;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private boolean mFullVerification = true;
+ private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * verify the V3 signing block of the provided {@code apk} with the specified {@code
+ * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
+ */
+ public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
+ int maxSdkVersion) {
+ mApk = apk;
+ mZipSections = zipSections;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
+ * apkSignatureSchemeV3Block}.
+ *
+ * <note>Full verification of the v3 signature is not possible when instantiating a new
+ * {@code V3SchemeVerifier} with this method.</note>
+ */
+ public Builder(ByteBuffer apkSignatureSchemeV3Block) {
+ mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the V3 {code blockId} to be verified in the provided APK.
+ *
+ * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
+ * attribute.
+ *
+ * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
+ * block of an APK; in the case of multiple signers targeting different SDK versions in the
+ * v3.1 signing block, the minimum SDK version from all the signers should be used.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets the {@code result} instance to be used when returning verification results.
+ *
+ * <p>This method can be used when the caller already has a {@link
+ * ApkSigningBlockUtils.Result} and wants to store the verification results in this
+ * instance.
+ */
+ public Builder setResult(ApkSigningBlockUtils.Result result) {
+ mResult = result;
+ return this;
+ }
+
+ /**
+ * Sets the instance to be used to store the {@code contentDigestsToVerify}.
+ *
+ * <p>This method can be used when the caller needs access to the {@code
+ * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
+ */
+ public Builder setContentDigestsToVerify(
+ Set<ContentDigestAlgorithm> contentDigestsToVerify) {
+ mContentDigestsToVerify = contentDigestsToVerify;
+ return this;
+ }
+
+ /**
+ * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
+ * from this instance.
+ *
+ * <note>{@link #verify()} will always verify the content digests for the APK, but this
+ * allows verification of the rotation minimum SDK version stripping attribute to be skipped
+ * for scenarios where this value may not have been parsed from a V3.1 signing block (such
+ * as when only {@link #parseSigners()} will be invoked.</note>
+ */
+ public Builder setFullVerification(boolean fullVerification) {
+ mFullVerification = fullVerification;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeVerifier build() {
+ int sigSchemeVersion;
+ switch (mBlockId) {
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+ mMinSdkVersion = Math.max(mMinSdkVersion,
+ V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
+ break;
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+ // V3.1 supports targeting an SDK version later than that of the initial release
+ // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
+ // rest of the range.
+ mMinSdkVersion = mMaxSdkVersion;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
+ mBlockId));
+ }
+ if (mResult == null) {
+ mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
+ }
+ if (mContentDigestsToVerify == null) {
+ mContentDigestsToVerify = new HashSet<>(1);
+ }
+
+ V3SchemeVerifier verifier = new V3SchemeVerifier(
+ mExecutor,
+ mApk,
+ mZipSections,
+ mContentDigestsToVerify,
+ mResult,
+ mMinSdkVersion,
+ mMaxSdkVersion,
+ mBlockId,
+ mOptionalRotationMinSdkVersion,
+ mFullVerification);
+ if (mApkSignatureSchemeV3Block != null) {
+ verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
+ }
+ return verifier;
+ }
}
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 74aa629..2f9ecb3 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -22,11 +22,11 @@
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
-import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.util.Pair;
@@ -43,6 +43,7 @@
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
@@ -61,7 +62,6 @@
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
* <p>
- * TODO(schfan): Add v4 unit tests
*/
public abstract class V4SchemeSigner {
/**
@@ -70,6 +70,23 @@
private V4SchemeSigner() {
}
+ public static class SignerConfig {
+ final public ApkSigningBlockUtils.SignerConfig v4Config;
+ final public ApkSigningBlockUtils.SignerConfig v41Config;
+
+ public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
+ List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
+ if (v4Configs == null || v4Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
+ }
+ if (v41Configs != null && v41Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
+ }
+ this.v4Config = v4Configs.get(0);
+ this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
+ }
+ }
+
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
@@ -149,10 +166,10 @@
return Pair.of(signature, tree);
}
- private static V4Signature generateSignature(
- SignerConfig signerConfig,
+ private static V4Signature.SigningInfo generateSigningInfo(
+ ApkSigningBlockUtils.SignerConfig signerConfig,
V4Signature.HashingInfo hashingInfo,
- byte[] apkDigest, byte[] additionaData, long fileSize)
+ byte[] apkDigest, byte[] additionalData, long fileSize)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
CertificateEncodingException {
if (signerConfig.certificates.isEmpty()) {
@@ -169,7 +186,7 @@
final byte[] encodedCertificate = encodedCertificates.get(0);
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
- encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
+ encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
signingInfoNoSignature);
@@ -184,12 +201,33 @@
final int signatureAlgorithmId = signatures.get(0).getFirst();
final byte[] signature = signatures.get(0).getSecond();
- final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
- encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+ return new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
signature);
+ }
+
+ private static V4Signature generateSignature(
+ SignerConfig signerConfig,
+ V4Signature.HashingInfo hashingInfo,
+ byte[] apkDigest, byte[] additionalData, long fileSize)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+ CertificateEncodingException {
+ final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
+ hashingInfo, apkDigest, additionalData, fileSize);
+
+ final V4Signature.SigningInfos signingInfos;
+ if (signerConfig.v41Config != null) {
+ final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
+ generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
+ additionalData, fileSize).toByteArray());
+ signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
+ } else {
+ signingInfos = new V4Signature.SigningInfos(signingInfo);
+ }
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
- signingInfo.toByteArray());
+ signingInfos.toByteArray());
}
// Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
index a6cd9db..c0a9013 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -90,20 +90,37 @@
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
signature.hashingInfo);
- V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
- signature.signingInfo);
- final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, signingInfo);
+ V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
+ signature.signingInfos);
- // First, verify the signature over signedData.
- ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
- signingInfo, signedData);
- result.signers.add(signerInfo);
- if (result.containsErrors()) {
- return result;
+ final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
+
+ // Verify the primary signature over signedData.
+ {
+ V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
+ result.signers.add(signerInfo);
+ if (result.containsErrors()) {
+ return result;
+ }
}
- // Second, check if the root hash and the tree are correct.
+ // Verify all subsequent signatures.
+ for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
+ V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+ signingInfoBlock.signingInfo);
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Check if the root hash and the tree are correct.
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
if (!result.containsErrors()) {
result.verified = true;
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
index deabe12..1eac5a2 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -22,6 +22,8 @@
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
public class V4Signature {
public static final int CURRENT_VERSION = 2;
@@ -29,6 +31,8 @@
public static final int HASHING_ALGORITHM_SHA256 = 1;
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+ public static final int MAX_SIGNING_INFOS_SIZE = 7168;
+
public static class HashingInfo {
public final int hashAlgorithm; // only 1 == SHA256 supported
public final byte log2BlockSize; // only 12 (block size 4096) supported now
@@ -82,7 +86,10 @@
}
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
- ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
+ }
+
+ static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
byte[] apkDigest = readBytes(buffer);
byte[] certificate = readBytes(buffer);
byte[] additionalData = readBytes(buffer);
@@ -108,14 +115,93 @@
}
}
- public final int version; // Always 2 for now.
- public final byte[] hashingInfo;
- public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
+ public static class SigningInfoBlock {
+ public final int blockId;
+ public final byte[] signingInfo;
- V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
+ public SigningInfoBlock(int blockId, byte[] signingInfo) {
+ this.blockId = blockId;
+ this.signingInfo = signingInfo;
+ }
+
+ static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
+ int blockId = buffer.getInt();
+ byte[] signingInfo = readBytes(buffer);
+ return new SigningInfoBlock(blockId, signingInfo);
+ }
+
+ byte[] toByteArray() {
+ final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(this.blockId);
+ writeBytes(buffer, this.signingInfo);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfos {
+ public final SigningInfo signingInfo;
+ public final SigningInfoBlock[] signingInfoBlocks;
+
+ public SigningInfos(SigningInfo signingInfo) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = new SigningInfoBlock[0];
+ }
+
+ public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = signingInfoBlocks;
+ }
+
+ public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
+ if (!buffer.hasRemaining()) {
+ return new SigningInfos(signingInfo);
+ }
+ ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
+ while (buffer.hasRemaining()) {
+ signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
+ }
+ return new SigningInfos(signingInfo,
+ signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
+ }
+
+ byte[] toByteArray() {
+ byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
+ arrays[0] = this.signingInfo.toByteArray();
+ int size = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
+ size += arrays[i + 1].length;
+ }
+ if (size > MAX_SIGNING_INFOS_SIZE) {
+ throw new IllegalArgumentException(
+ "Combined SigningInfos length exceeded limit of 7K: " + size);
+ }
+
+ // Combine all arrays into one.
+ byte[] result = Arrays.copyOf(arrays[0], size);
+ int offset = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
+ offset += arrays[i + 1].length;
+ }
+ return result;
+ }
+ }
+
+ // Always 2 for now.
+ public final int version;
+ public final byte[] hashingInfo;
+ // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
+ // Passed as-is to the kernel. Can be retrieved later.
+ public final byte[] signingInfos;
+
+ V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
this.version = version;
this.hashingInfo = hashingInfo;
- this.signingInfo = signingInfo;
+ this.signingInfos = signingInfos;
}
static V4Signature readFrom(InputStream stream) throws IOException {
@@ -131,7 +217,7 @@
public void writeTo(OutputStream stream) throws IOException {
writeIntLE(stream, this.version);
writeBytes(stream, this.hashingInfo);
- writeBytes(stream, this.signingInfo);
+ writeBytes(stream, this.signingInfos);
}
static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
diff --git a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
index 87eae48..bbead72 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -54,6 +54,18 @@
/** Android P. */
public static final int P = 28;
+ /** Android Q. */
+ public static final int Q = 29;
+
/** Android R. */
public static final int R = 30;
+
+ /** Android S. */
+ public static final int S = 31;
+
+ /** Android Sv2. */
+ public static final int Sv2 = 32;
+
+ /** Android T. */
+ public static final int T = 33;
}
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 69b8d4e..daa06a1 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -89,9 +90,9 @@
// All signers with the same prefix and an _X suffix were signed with the private key of the
// (X-1) signer.
- private static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
- private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
- private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
+ static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
+ static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
+ static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
@@ -226,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"),
@@ -234,6 +237,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -242,6 +246,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -294,6 +299,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-legacy-aligned-in.apk",
@@ -302,6 +308,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -310,6 +317,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -340,6 +348,7 @@
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-legacy-aligned-in.apk",
@@ -348,6 +357,7 @@
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -356,6 +366,7 @@
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -464,6 +475,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-unaligned-in.apk",
@@ -486,6 +498,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-unaligned-in.apk",
@@ -501,6 +514,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
@@ -540,6 +554,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-legacy-aligned-in.apk",
@@ -562,6 +577,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-legacy-aligned-in.apk",
@@ -577,6 +593,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
@@ -615,6 +632,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-aligned-in.apk",
@@ -637,6 +655,7 @@
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-aligned-in.apk",
@@ -652,6 +671,7 @@
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
}
@@ -967,6 +987,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
@@ -1516,6 +1537,469 @@
EC_P256_SIGNER_RESOURCE_NAME);
}
+ @Test
+ public void testSetMinSdkVersionForRotation_lessThanT_noV31Block() throws Exception {
+ // The V3.1 signing block is intended to allow APK signing key rotation to target T+, but
+ // a minimum SDK version can be explicitly set for rotation; if it is less than T than
+ // the rotated key will be included in the V3.0 block.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkMinRotationP = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationP = verify(signedApkMinRotationP, null);
+ // The V3.1 signature scheme was introduced in T; specifying an older SDK version as the
+ // minimum for rotation should cause the APK to still be signed with rotation in the V3.0
+ // signing block.
+ File signedApkMinRotationS = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.S)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationS = verify(signedApkMinRotationS, null);
+
+ assertVerified(resultMinRotationP);
+ assertFalse(resultMinRotationP.isVerifiedUsingV31Scheme());
+ assertEquals(1, resultMinRotationP.getV3SchemeSigners().size());
+ assertResultContainsSigners(resultMinRotationP, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertVerified(resultMinRotationS);
+ assertFalse(resultMinRotationS.isVerifiedUsingV31Scheme());
+ // While rotation is targeting S, signer blocks targeting specific SDK versions have not
+ // been tested in previous platform releases; ensure only a single signer block with the
+ // rotated key is in the V3 block.
+ assertEquals(1, resultMinRotationS.getV3SchemeSigners().size());
+ assertResultContainsSigners(resultMinRotationS, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_TAndLater_v31Block() throws Exception {
+ // When T or later is specified as the minimum SDK version for rotation, then a new V3.1
+ // signing block should be created with the new rotated key, and the V3.0 signing block
+ // should still be signed with the original key.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkMinRotationT = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationT = verify(signedApkMinRotationT, null);
+ // The API level for a release after T is not yet defined, so for now treat it as T + 1.
+ File signedApkMinRotationU = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T + 1)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationU = verify(signedApkMinRotationU, null);
+
+ assertVerified(resultMinRotationT);
+ assertTrue(resultMinRotationT.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultMinRotationT, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(resultMinRotationT, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T);
+ assertVerified(resultMinRotationU);
+ assertTrue(resultMinRotationU.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultMinRotationU, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(resultMinRotationU, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T + 1);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTNoOriginalSigner_fails() throws Exception {
+ // Similar to the V1 and V2 signatures schemes, if an app is targeting P or later with
+ // rotation targeting T, the original signer must be provided so that it can be used in the
+ // V3.0 signing block; if it is not provided the signer should throw an Exception.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+ .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(28)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage)));
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTAndApkMinSdkT_onlySignsV3Block()
+ throws Exception {
+ // A V3.1 signing block should only exist alongside a V3.0 signing block; if an APK's
+ // min SDK version is greater than or equal to the SDK version for rotation then the
+ // original signer should not be required, and the rotated signing key should be in
+ // a V3.0 signing block.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+ .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+ .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verifyForMinSdkVersion(signedApk,
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT);
+
+ assertVerified(result);
+ assertFalse(result.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTWithSourceStamp_noWarnings()
+ throws Exception {
+ // Source stamp verification will report a warning if a stamp signature is not found for any
+ // of the APK Signature Schemes used to sign the APK. This test verifies an APK signed with
+ // a rotated key in the v3.1 block and a source stamp successfully verifies, including the
+ // source stamp, without any warnings.
+ ApkSigner.SignerConfig rsa2048OriginalSignerConfig = getDefaultSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ rsa2048OriginalSignerConfig,
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage)
+ .setSourceStampSignerConfig(rsa2048OriginalSignerConfig));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T);
+ assertSourceStampVerified(signedApk, result);
+ }
+
+ @Test
+ public void testSetRotationTargetsDevRelease_target34_v30SignerTargetsAtLeast34()
+ throws Exception {
+ // During development of a new platform release the new platform will use the SDK version
+ // of the previously released platform, so in order to test rotation on a new platform
+ // release it must target the SDK version of the previous platform. However an APK signed
+ // with the v3.1 signature scheme and targeting rotation on the previous platform release X
+ // would still use rotation if that APK were installed on a device running release version
+ // X. To support targeting rotation on the main branch, the v3.1 signature scheme supports
+ // a rotation-targets-dev-release attribute; this allows the APK to use the v3.1 signer
+ // block on a development platform with SDK version X while a release platform X will
+ // skip this signer block when it sees this additional attribute. To ensure that the APK
+ // will still target the released platform X, the v3.0 signer must have a maxSdkVersion
+ // of at least X for the signer.
+ 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);
+ int rotationMinSdkVersion = V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + 1;
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(rotationMinSdkVersion)
+ .setSigningCertificateLineage(lineage)
+ .setRotationTargetsDevRelease(true));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease());
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertTrue(result.getV3SchemeSigners().get(0).getMaxSdkVersion() >= rotationMinSdkVersion);
+ }
+
+ @Test
+ public void testV3_rotationMinSdkVersionLessThanTV3Only_origSignerNotRequired()
+ throws Exception {
+ // The v3.1 signature scheme allows a rotation-min-sdk-version be specified to target T+
+ // for rotation; however if this value is less than the expected SDK version of T, then
+ // apksig should just use the rotated signing key in the v3.0 block. An APK that targets
+ // P+ that wants to use rotation in the v3.0 signing block should only need to provide
+ // the rotated signing key and lineage; this test ensures this behavior when the
+ // rotation-min-sdk-version is set to a value > P and < T.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkRotationOnQ = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(AndroidSdkVersion.P)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.Q)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultRotationOnQ = verify(signedApkRotationOnQ, AndroidSdkVersion.P);
+
+ assertVerified(resultRotationOnQ);
+ assertEquals(1, resultRotationOnQ.getV3SchemeSigners().size());
+ assertFalse(resultRotationOnQ.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultRotationOnQ, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionEqualsMinSdkVersion_v3SignerPresent()
+ throws Exception {
+ // The SDK version for Sv2 (32) is used as the minSdkVersion for the V3.1 signature
+ // scheme to allow rotation to target the T development platform; this will be updated
+ // to the real SDK version of T once its SDK is finalized. This test verifies if a
+ // package has Sv2 as its minSdkVersion, the signing can complete as expected with the
+ // v3 block signed by the original signer and targeting just Sv2, and the v3.1 block
+ // signed by the rotated signer and targeting the dev release of Sv2 and all later releases.
+ 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-minSdk32.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);
+ assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionTWithoutLineage_v30VerificationSucceeds()
+ throws Exception {
+ // apksig allows setting a rotation-min-sdk-version without providing a rotated signing
+ // key / lineage; however in the absence of rotation, the rotation-min-sdk-version should
+ // be a no-op, and the stripping protection attribute should not be written to the v3.0
+ // signer.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertFalse(result.isVerifiedUsingV31Scheme());
+ assertTrue(result.isVerifiedUsingV3Scheme());
+ }
+
+ @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
+ // rotation-min-sdk-version option to allow the caller to specify the level from which
+ // the rotated signer should be used. A value less than T should result in a single
+ // rotated signer in the V3 block (along with the corresponding lineage), and the V4
+ // signature should use this signer.
+ 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(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsV4Signers(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testV4_rotationMinSdkVersionT_signatureHasOrigAndRotatedKey() throws Exception {
+ // When an APK is signed with a rotated key and the rotation-min-sdk-version X is set to T+,
+ // a V3.1 block will be signed with the rotated signing key targeting X and later, and
+ // a V3.0 block will be signed with the original signing key targeting P - X-1. The
+ // V4 signature should contain both the original signing key and the rotated signing
+ // key; this ensures if an APK is installed on a device running an SDK version less than X,
+ // the V4 signature will be verified using the original signing key which will be the only
+ // signing key visible to the platform.
+ 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(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsV4Signers(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ 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}.
@@ -1544,8 +2028,20 @@
* Asserts the provided verification {@code result} contains the expected {@code signers} for
* each scheme that was used to verify the APK's signature.
*/
- private static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
+ static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
throws Exception {
+ assertResultContainsSigners(result, false, signers);
+ }
+
+ /**
+ * Asserts the provided verification {@code result} contains the expected {@code signers} for
+ * each scheme that was used to verify the APK's signature; if {@code rotationExpected} is set
+ * to {@code true}, then the first element in {@code signers} is treated as the expected
+ * original signer for any V1, V2, and V3 (where applicable) signatures, and the last element
+ * is the rotated expected signer for V3+.
+ */
+ static void assertResultContainsSigners(ApkVerifier.Result result,
+ boolean rotationExpected, String... signers) throws Exception {
// A result must be successfully verified before verifying any of the result's signers.
assertTrue(result.isVerified());
@@ -1554,16 +2050,27 @@
ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
expectedSigners.addAll(signerConfig.getCertificates());
}
+ // If rotation is expected then the V1 and V2 signature should only be signed by the
+ // original signer.
+ List<X509Certificate> expectedV1Signers =
+ rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+ List<X509Certificate> expectedV2Signers =
+ rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+ // V3 only supports a single signer; if rotation is not expected or the V3.1 block contains
+ // the rotated signing key then the expected V3.0 signer should be the original signer.
+ List<X509Certificate> expectedV3Signers =
+ !rotationExpected || result.isVerifiedUsingV31Scheme()
+ ? List.of(expectedSigners.get(0))
+ : List.of(expectedSigners.get(expectedSigners.size() - 1));
if (result.isVerifiedUsingV1Scheme()) {
Set<X509Certificate> v1Signers = new HashSet<>();
for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
v1Signers.add(signer.getCertificate());
}
- assertEquals(expectedSigners.size(), v1Signers.size());
- assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedV1Signers)
+ ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers),
- v1Signers.containsAll(expectedSigners));
+ v1Signers.containsAll(expectedV1Signers));
}
if (result.isVerifiedUsingV2Scheme()) {
@@ -1571,10 +2078,9 @@
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
v2Signers.add(signer.getCertificate());
}
- assertEquals(expectedSigners.size(), v2Signers.size());
- assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedV2Signers)
+ ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers),
- v2Signers.containsAll(expectedSigners));
+ v2Signers.containsAll(expectedV2Signers));
}
if (result.isVerifiedUsingV3Scheme()) {
@@ -1582,11 +2088,68 @@
for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
v3Signers.add(signer.getCertificate());
}
- assertEquals(expectedSigners.size(), v3Signers.size());
- assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedV3Signers)
+ ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers),
- v3Signers.containsAll(expectedSigners));
+ v3Signers.containsAll(expectedV3Signers));
}
+
+ if (result.isVerifiedUsingV31Scheme()) {
+ Set<X509Certificate> v31Signers = new HashSet<>();
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ v31Signers.add(signer.getCertificate());
+ }
+ // V3.1 only supports specifying signatures with a rotated signing key; if a V3.1
+ // signing block was verified then ensure it contains the expected rotated signer.
+ List<X509Certificate> expectedV31Signers = List
+ .of(expectedSigners.get(expectedSigners.size() - 1));
+ assertTrue("Expected V3.1 signers: " + getAllSubjectNamesFrom(expectedV31Signers)
+ + ", actual V3.1 signers: " + getAllSubjectNamesFrom(v31Signers),
+ v31Signers.containsAll(expectedV31Signers));
+ }
+ }
+
+ /**
+ * Asserts the provided verification {@code result} contains the expected V4 {@code signers}.
+ */
+ private static void assertResultContainsV4Signers(ApkVerifier.Result result, String... signers)
+ throws Exception {
+ assertTrue(result.isVerified());
+ assertTrue(result.isVerifiedUsingV4Scheme());
+ List<X509Certificate> expectedSigners = new ArrayList<>();
+ for (String signer : signers) {
+ ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
+ expectedSigners.addAll(signerConfig.getCertificates());
+ }
+ List<X509Certificate> v4Signers = new ArrayList<>();
+ for (ApkVerifier.Result.V4SchemeSignerInfo signer : result.getV4SchemeSigners()) {
+ v4Signers.addAll(signer.getCertificates());
+ }
+ assertTrue("Expected V4 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V4 signers: " + getAllSubjectNamesFrom(v4Signers),
+ v4Signers.containsAll(expectedSigners));
+ }
+
+ /**
+ * Asserts the provided {@code result} contains the expected {@code signer} targeting
+ * {@code minSdkVersion} as the minimum version for rotation.
+ */
+ static void assertV31SignerTargetsMinApiLevel(ApkVerifier.Result result, String signer,
+ int minSdkVersion) throws Exception {
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ ApkSigner.SignerConfig expectedSignerConfig = getDefaultSignerConfigFromResources(signer);
+
+ for (ApkVerifier.Result.V3SchemeSignerInfo signerConfig : result.getV31SchemeSigners()) {
+ if (signerConfig.getCertificates()
+ .containsAll(expectedSignerConfig.getCertificates())) {
+ assertEquals("The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates())
+ + ", is expected to target SDK version " + minSdkVersion
+ + ", instead it is targeting " + signerConfig.getMinSdkVersion(),
+ minSdkVersion, signerConfig.getMinSdkVersion());
+ return;
+ }
+ }
+ fail("Did not find the expected signer, " + getAllSubjectNamesFrom(
+ expectedSignerConfig.getCertificates()));
}
/**
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 5a7c64d..9de2b59 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -16,7 +16,12 @@
package com.android.apksig;
+import static com.android.apksig.ApkSignerTest.FIRST_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.assertResultContainsSigners;
+import static com.android.apksig.ApkSignerTest.assertV31SignerTargetsMinApiLevel;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
@@ -25,6 +30,7 @@
import com.android.apksig.ApkVerifier.IssueWithParams;
import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus;
import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.HexEncoding;
import com.android.apksig.internal.util.Resources;
@@ -1307,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();
@@ -1348,6 +1471,131 @@
}
}
+ @Test
+ public void verifyV31_rotationTarget34_containsExpectedSigners() throws Exception {
+ // This test verifies an APK targeting a specific SDK version for rotation properly reports
+ // that version for the rotated signer in the v3.1 block, and all other signing blocks
+ // use the original signing key.
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+
+ assertVerified(result);
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+ }
+
+ @Test
+ public void verifyV31_missingStrippingAttr_warningReported() throws Exception {
+ // The v3.1 signing block supports targeting SDK versions; to protect against these target
+ // versions being modified the v3 signer contains a stripping protection attribute with the
+ // SDK version on which rotation should be applied. This test verifies a warning is reported
+ // when this attribute is not present in the v3 signer.
+ ApkVerifier.Result result = verify("v31-tgt-33-no-v3-attr.apk");
+
+ assertVerificationWarning(result, Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING);
+ }
+
+ @Test
+ public void verifyV31_strippingAttrMismatch_errorReportedOnSupportedVersions()
+ throws Exception {
+ // This test verifies if the stripping protection attribute does not properly match the
+ // minimum SDK version on which rotation is supported then the APK should fail verification.
+ ApkVerifier.Result result = verify("v31-tgt-34-v3-attr-value-33.apk");
+ assertVerificationFailure(result, Issue.V31_ROTATION_MIN_SDK_MISMATCH);
+
+ // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+ // and the v3.1 signing block.
+ result = verifyForMaxSdkVersion("v31-tgt-34-v3-attr-value-33.apk",
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+ assertVerified(result);
+ }
+
+ @Test
+ public void verifyV31_missingV31Block_errorReportedOnSupportedVersions() throws Exception {
+ // This test verifies if the stripping protection attribute contains a value for rotation
+ // but a v3.1 signing block was not found then the APK should fail verification.
+ ApkVerifier.Result result = verify("v31-block-stripped-v3-attr-value-33.apk");
+ assertVerificationFailure(result, Issue.V31_BLOCK_MISSING);
+
+ // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+ // and the v3.1 signing block.
+ result = verifyForMaxSdkVersion("v31-block-stripped-v3-attr-value-33.apk",
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+ assertVerified(result);
+ }
+
+ @Test
+ public void verifyV31_v31BlockWithoutV3Block_reportsError() throws Exception {
+ // A v3.1 block must always exist alongside a v3.0 block; if an APK's minSdkVersion is the
+ // same as the version supporting rotation then it should be written to a v3.0 block.
+ ApkVerifier.Result result = verify("v31-tgt-33-no-v3-block.apk");
+ assertVerificationFailure(result, Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
+
+ @Test
+ public void verifyV31_rotationTargetsDevRelease_resultReportsDevReleaseFlag() throws Exception {
+ // Development releases use the SDK version of the previous release until the SDK is
+ // finalized. In order to only target the development release and later, the v3.1 signature
+ // scheme supports targeting development releases such that the SDK version X will install
+ // on a device running X with the system property ro.build.version.codename set to a new
+ // development codename (eg T); a release platform will have this set to "REL", and the
+ // platform will ignore the v3.1 signer if the minSdkVersion is X and the codename is "REL".
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-dev-release.apk");
+
+ assertVerified(result);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void verifyV3_v3RotatedSignerTargetsDevRelease_warningReported() throws Exception {
+ // While a v3.1 signer can target a development release, v3.0 does not support the same
+ // attribute since it is only intended for v3.1 with v3.0 using the original signer. This
+ // test verifies a warning is reported if an APK has this flag set on a v3.0 signer since it
+ // will be ignored by the platform.
+ ApkVerifier.Result result = verify("v3-rsa-2048_2-tgt-dev-release.apk");
+
+ assertVerificationWarning(result, Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+ }
+
+ @Test
+ public void verifyV31_rotationTargets34_resultContainsExpectedLineage() throws Exception {
+ // During verification of the v3.1 and v3.0 signing blocks, ApkVerifier will set the
+ // signing certificate lineage in the Result object; this test verifies a null lineage from
+ // a v3.0 signer does not overwrite a valid lineage from a v3.1 signer.
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+
+ assertNotNull(result.getSigningCertificateLineage());
+ SigningCertificateLineageTest.assertLineageContainsExpectedSigners(
+ result.getSigningCertificateLineage(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void verify31_minSdkVersionT_resultSuccessfullyVerified() throws Exception {
+ // When a min-sdk-version of 33 is explicitly specified, apksig will behave the same as a
+ // device running this API level and only verify a v3.1 signature if it exists. This test
+ // verifies this v3.1 signature is sufficient to report the APK as verified.
+ ApkVerifier.Result result = verifyForMinSdkVersion("v31-rsa-2048_2-tgt-33-1-tgt-28.apk",
+ 33);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ }
+
+ @Test
+ public void verify31_minSdkVersionTTargetSdk30_resultSuccessfullyVerified() throws Exception {
+ // This test verifies when a min-sdk-version of 33 is specified and the APK targets API
+ // level 30 or later, the v3.1 signature is sufficient to report the APK meets the
+ // requirement of a minimum v2 signature.
+ ApkVerifier.Result result = verifyForMinSdkVersion(
+ "v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk", 33);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
@@ -1486,13 +1734,27 @@
}
static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
- if (result.isVerified()) {
+ assertVerificationIssue(result, expectedIssue, true);
+ }
+
+ static void assertVerificationWarning(ApkVerifier.Result result, Issue expectedIssue) {
+ assertVerificationIssue(result, expectedIssue, false);
+ }
+
+ /**
+ * Asserts the provided {@code result} contains the {@code expectedIssue}; if {@code
+ * verifyError} is set to {@code true} then the specified {@link Issue} will be expected as an
+ * error, otherwise it will be expected as a warning.
+ */
+ private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue,
+ boolean verifyError) {
+ if (result.isVerified() && verifyError) {
fail("APK verification succeeded instead of failing with " + expectedIssue);
return;
}
StringBuilder msg = new StringBuilder();
- for (IssueWithParams issue : result.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1503,7 +1765,8 @@
}
for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
String signerName = signer.getName();
- for (ApkVerifier.IssueWithParams issue : signer.getErrors()) {
+ for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1520,7 +1783,8 @@
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
String signerName = "signer #" + (signer.getIndex() + 1);
- for (IssueWithParams issue : signer.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1535,7 +1799,8 @@
}
for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
String signerName = "signer #" + (signer.getIndex() + 1);
- for (IssueWithParams issue : signer.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1548,6 +1813,22 @@
.append(issue);
}
}
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1);
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK Signature Scheme v3.1 signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue);
+ }
+ }
fail(
"APK failed verification for the wrong reason"
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index d5dc71d..07a48f1 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -397,6 +397,21 @@
assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
}
+ @Test
+ public void testLineageFromAPKWithV31BlockContainsExpectedSigners() throws Exception {
+ SignerConfig firstSigner = getSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ SignerConfig secondSigner = getSignerConfigFromResources(
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ List<SignerConfig> expectedSigners = Arrays.asList(firstSigner, secondSigner);
+ DataSource apkDataSource = Resources.toDataSource(getClass(),
+ "v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+ SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
+ apkDataSource);
+ assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
+
+ }
+
@Test(expected = ApkFormatException.class)
public void testLineageFromAPKWithInvalidZipCDSizeFails() throws Exception {
// This test verifies that attempting to read the lineage from an APK where the zip
@@ -535,7 +550,20 @@
return lineage.spawnDescendant(oldSignerConfig, newSignerConfig);
}
- private void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
+ /**
+ * Asserts the provided {@code lineage} contains the {@code expectedSigners} from the test's
+ * resources.
+ */
+ static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
+ String... expectedSigners) throws Exception {
+ List<SignerConfig> signers = new ArrayList<>();
+ for (String expectedSigner : expectedSigners) {
+ signers.add(getSignerConfigFromResources(expectedSigner));
+ }
+ assertLineageContainsExpectedSigners(lineage, signers);
+ }
+
+ private static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
List<SignerConfig> signers) {
assertEquals("The lineage does not contain the expected number of signers",
signers.size(), lineage.size());
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/golden-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
index 965f901..a273b0c 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
index 13a3bc9..2a0d383 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
index 36f4825..4a4cda9 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
index f6b60d6..376b0e5 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
index b0debf6..25ffa5b 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
index f8b4171..341d7ba 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
index 02ecee0..5636723 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
index 08538a0..4f21b4c 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
index 9bcbc7a..7514d20 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-minSdk32.apk b/src/test/resources/com/android/apksig/original-minSdk32.apk
new file mode 100644
index 0000000..1d50f27
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-minSdk32.apk
Binary files differ
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
diff --git a/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk
new file mode 100644
index 0000000..d3b2c14
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
new file mode 100644
index 0000000..d091075
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk
new file mode 100644
index 0000000..ad14731
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
new file mode 100644
index 0000000..aeaec33
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
new file mode 100644
index 0000000..ccf59d4
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
new file mode 100644
index 0000000..784f47e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
new file mode 100644
index 0000000..284cd7a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
new file mode 100644
index 0000000..1c797a7
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
new file mode 100644
index 0000000..ab87fcc
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
Binary files differ