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