diff options
author | Xin Li <delphij@google.com> | 2023-10-05 15:45:35 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2023-10-05 15:45:35 -0700 |
commit | acf0e6e4e137b143f8ea1acd01bc926022969036 (patch) | |
tree | 42d89bdbdde8814cfb0102c0d3fbb31e10cb1e45 | |
parent | 7317fe300dcc9bcce0ec2a6a10fa15703c80c747 (diff) | |
parent | 8112df60ad8f079b019fd37f57010b9ab6ca02bc (diff) | |
download | apksig-acf0e6e4e137b143f8ea1acd01bc926022969036.tar.gz |
Merge Android 14
Bug: 298295554
Merged-In: I77f4218599511ff4f9f3790e4942a329d5a18da4
Change-Id: I50dd82e55a627635b96fae0aba8caee39ee0a8dc
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 Binary files differnew file mode 100644 index 0000000..509ea3b --- /dev/null +++ b/src/test/resources/com/android/apksig/ec-p256-lineage-2-signers diff --git a/src/test/resources/com/android/apksig/ec-p256_2.pk8 b/src/test/resources/com/android/apksig/ec-p256_2.pk8 Binary files differnew file mode 100644 index 0000000..5e73f27 --- /dev/null +++ b/src/test/resources/com/android/apksig/ec-p256_2.pk8 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 Binary files differnew file mode 100644 index 0000000..34bc091 --- /dev/null +++ b/src/test/resources/com/android/apksig/incorrect-manifest-size.apk diff --git a/src/test/resources/com/android/apksig/original-minSdk33.apk b/src/test/resources/com/android/apksig/original-minSdk33.apk Binary files differnew file mode 100644 index 0000000..a2ea9eb --- /dev/null +++ b/src/test/resources/com/android/apksig/original-minSdk33.apk 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 Binary files differnew file mode 100644 index 0000000..c2a3545 --- /dev/null +++ b/src/test/resources/com/android/apksig/rsa-2048-lineage-2-signers-2-3 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 Binary files differnew file mode 100644 index 0000000..0fa3118 --- /dev/null +++ b/src/test/resources/com/android/apksig/rsa-2048-lineage-3-signers-1-no-caps 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 Binary files differnew file mode 100644 index 0000000..7ec82eb --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk diff --git a/src/test/resources/com/android/apksig/stamp-unknown-attr.apk b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk Binary files differnew file mode 100644 index 0000000..68771a5 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk diff --git a/src/test/resources/com/android/apksig/stamp-unknown-sig.apk b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk Binary files differnew file mode 100644 index 0000000..1c1557e --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk 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 Binary files differnew file mode 100644 index 0000000..517a1ef --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-2elem-incorrect-lineage.apk 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 Binary files differnew file mode 100644 index 0000000..2eba63e --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-2elem-lineage-incorrect-digest.apk 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 Binary files differnew file mode 100644 index 0000000..fbc7f76 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-empty-lineage-no-v3.apk 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 Binary files differnew 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 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 Binary files differindex 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 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 Binary files differnew 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 |