aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-09-22 14:15:34 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-09-22 14:15:34 +0000
commit82cb095a14d225b3dfc0bdf40c1981ecf865d931 (patch)
tree9e31880da55d7e9cc5b405cab5c2112d8cc452d1
parent86cbfc1ec45ef3490990e3b3a86bc3f568adaebc (diff)
parent3a417211e8f11480be69b714309c1576d3082489 (diff)
downloadapksig-82cb095a14d225b3dfc0bdf40c1981ecf865d931.tar.gz
Change-Id: Ic8dadb5a996f16b5cbb7f53c307b6308215c1f5a
-rw-r--r--BUILD4
-rw-r--r--src/apksigner/java/com/android/apksigner/ApkSignerTool.java214
-rw-r--r--src/apksigner/java/com/android/apksigner/SignerParams.java17
-rw-r--r--src/apksigner/java/com/android/apksigner/help_lineage.txt4
-rw-r--r--src/apksigner/java/com/android/apksigner/help_rotate.txt9
-rw-r--r--src/apksigner/java/com/android/apksigner/help_sign.txt39
-rw-r--r--src/apksigner/java/com/android/apksigner/help_verify.txt3
-rw-r--r--src/main/java/com/android/apksig/ApkSigner.java367
-rw-r--r--src/main/java/com/android/apksig/ApkVerificationIssue.java173
-rw-r--r--src/main/java/com/android/apksig/ApkVerifier.java1320
-rw-r--r--src/main/java/com/android/apksig/Constants.java55
-rw-r--r--src/main/java/com/android/apksig/DefaultApkSignerEngine.java539
-rw-r--r--src/main/java/com/android/apksig/SigningCertificateLineage.java100
-rw-r--r--src/main/java/com/android/apksig/SourceStampVerifier.java911
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtils.java393
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtilsLite.java199
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigResult.java104
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java104
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java464
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java393
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java35
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java5
-rw-r--r--src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java27
-rw-r--r--src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java54
-rw-r--r--src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java30
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java235
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java34
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java342
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java (renamed from src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java)15
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java139
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java286
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java159
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java26
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java21
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java76
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java25
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java50
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java10
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java55
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java272
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java408
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java73
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java37
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java100
-rw-r--r--src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java17
-rw-r--r--src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java10
-rw-r--r--src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java18
-rw-r--r--src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java2
-rw-r--r--src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java10
-rw-r--r--src/main/java/com/android/apksig/internal/zip/EocdRecord.java9
-rw-r--r--src/main/java/com/android/apksig/internal/zip/ZipUtils.java60
-rw-r--r--src/main/java/com/android/apksig/util/RunnablesExecutor.java9
-rw-r--r--src/main/java/com/android/apksig/zip/ZipSections.java85
-rw-r--r--src/test/java/com/android/apksig/AllTests.java1
-rw-r--r--src/test/java/com/android/apksig/ApkSignerTest.java1424
-rw-r--r--src/test/java/com/android/apksig/ApkVerifierTest.java624
-rw-r--r--src/test/java/com/android/apksig/SigningCertificateLineageTest.java75
-rw-r--r--src/test/java/com/android/apksig/SourceStampVerifierTest.java567
-rw-r--r--src/test/java/com/android/apksig/apk/ApkUtilsTest.java43
-rw-r--r--src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java2
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apkbin16961 -> 16961 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v2-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apkbin16762 -> 16762 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-aligned-v3-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-file-size-aligned.apkbin0 -> 16384 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apkbin16961 -> 16961 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apkbin12865 -> 12865 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apkbin16762 -> 16762 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apkbin12666 -> 12666 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-pinsapp-signed.apkbin0 -> 8732 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apkbin12695 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apkbin12695 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apkbin12695 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apkbin0 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-out.apkbin12695 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-rsa-verity-out.apkbin0 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-out.apkbin12926 -> 12926 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apkbin12926 -> 12926 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apkbin17022 -> 17022 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apkbin12926 -> 12926 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v2-out.apkbin8631 -> 8631 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apkbin12727 -> 12727 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apkbin8631 -> 8631 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apkbin8631 -> 8631 bytes
-rw-r--r--src/test/resources/com/android/apksig/golden-unaligned-v3-out.apkbin8631 -> 8631 bytes
-rw-r--r--src/test/resources/com/android/apksig/original-minSdk32.apkbin0 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/original-with-stamp-file.apkbin0 -> 12758 bytes
-rw-r--r--src/test/resources/com/android/apksig/original-with-versionCodeMajor.apkbin0 -> 12703 bytes
-rwxr-xr-xsrc/test/resources/com/android/apksig/pinsapp-unsigned.apkbin0 -> 2205 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apkbin0 -> 16854 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apkbin0 -> 16854 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apkbin0 -> 16854 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-certificate-mismatch.apkbin16854 -> 12758 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-int-timestamp-value.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-lineage-invalid.apkbin0 -> 16854 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-lineage-valid.apkbin0 -> 16854 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apkbin0 -> 16859 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-malformed-signature.apkbin16854 -> 12758 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-unknown-attr.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-unknown-sig.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apkbin0 -> 8659 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-without-apk-signature.apkbin0 -> 12633 bytes
-rw-r--r--src/test/resources/com/android/apksig/stamp-without-block.apkbin12758 -> 12758 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apkbin0 -> 7619 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apkbin0 -> 13348 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apkbin0 -> 4623 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-stamp.apkbin0 -> 12763 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apkbin0 -> 16859 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apkbin0 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apkbin0 -> 12504 bytes
-rw-r--r--src/test/resources/com/android/apksig/v2-only-with-stamp.apkbin0 -> 12567 bytes
-rw-r--r--src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apkbin0 -> 12703 bytes
-rw-r--r--src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apkbin0 -> 12504 bytes
-rw-r--r--src/test/resources/com/android/apksig/v3-only-with-stamp.apkbin0 -> 12567 bytes
-rw-r--r--src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apkbin0 -> 12695 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apkbin0 -> 12700 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apkbin0 -> 16791 bytes
-rw-r--r--src/test/resources/com/android/apksig/valid-stamp.apkbin16854 -> 16854 bytes
140 files changed, 9468 insertions, 1414 deletions
diff --git a/BUILD b/BUILD
index e5f0b80..7be1088 100644
--- a/BUILD
+++ b/BUILD
@@ -2,7 +2,7 @@
licenses(["notice"]) # Apache License 2.0
-load("//tools/base/bazel:coverage.bzl", "coverage_java_test", "coverage_java_library")
+load("//tools/base/bazel:coverage.bzl", "coverage_java_library", "coverage_java_test")
# Public API of the apksig library
coverage_java_library(
@@ -69,5 +69,7 @@ coverage_java_test(
deps = [
":apksig-all",
"@maven//:junit.junit",
+ "@maven//:org.bouncycastle.bcprov-jdk15on",
+ "@maven//:org.conscrypt.conscrypt-openjdk-uber",
],
)
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index e1d976e..4e5fa85 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -22,6 +22,7 @@ import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
@@ -48,6 +49,7 @@ import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.List;
/**
@@ -62,6 +64,8 @@ public class ApkSignerTool {
private static final String HELP_PAGE_VERIFY = "help_verify.txt";
private static final String HELP_PAGE_ROTATE = "help_rotate.txt";
private static final String HELP_PAGE_LINEAGE = "help_lineage.txt";
+ private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
+ private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
private static MessageDigest sha256 = null;
private static MessageDigest sha1 = null;
@@ -78,6 +82,7 @@ public class ApkSignerTool {
return;
}
+
String cmd = params[0];
try {
if ("sign".equals(cmd)) {
@@ -109,6 +114,7 @@ public class ApkSignerTool {
}
}
+
private static void sign(String[] params) throws Exception {
if (params.length == 0) {
printUsage(HELP_PAGE_SIGN);
@@ -122,14 +128,21 @@ public class ApkSignerTool {
boolean v2SigningEnabled = true;
boolean v3SigningEnabled = true;
boolean v4SigningEnabled = false;
+ boolean forceSourceStampOverwrite = false;
+ boolean sourceStampTimestampEnabled = true;
+ boolean alignFileSize = false;
+ boolean verityEnabled = false;
boolean debuggableApkPermitted = true;
int minSdkVersion = 1;
boolean minSdkVersionSpecified = false;
int maxSdkVersion = Integer.MAX_VALUE;
+ int rotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ boolean rotationTargetsDevRelease = false;
List<SignerParams> signers = new ArrayList<>(1);
SignerParams signerParams = new SignerParams();
- SignerParams sourceStampSignerParams = new SignerParams();
SigningCertificateLineage lineage = null;
+ SignerParams sourceStampSignerParams = new SignerParams();
+ SigningCertificateLineage sourceStampLineage = null;
List<ProviderInstallSpec> providers = new ArrayList<>();
ProviderInstallSpec providerParams = new ProviderInstallSpec();
OptionsParser optionsParser = new OptionsParser(params);
@@ -137,6 +150,8 @@ public class ApkSignerTool {
String optionOriginalForm = null;
boolean v4SigningFlagFound = false;
boolean sourceStampFlagFound = false;
+ boolean deterministicDsaSigning = false;
+ boolean otherSignersSignaturesPreserved = false;
while ((optionName = optionsParser.nextOption()) != null) {
optionOriginalForm = optionsParser.getOptionOriginalForm();
if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -151,7 +166,13 @@ public class ApkSignerTool {
minSdkVersionSpecified = true;
} else if ("max-sdk-version".equals(optionName)) {
maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level");
- } else if ("v1-signing-enabled".equals(optionName)) {
+ } else if ("rotation-min-sdk-version".equals(optionName)) {
+ rotationMinSdkVersion = optionsParser.getRequiredIntValue(
+ "Minimum API Level for Rotation");
+ } else if ("rotation-targets-dev-release".equals(optionName)) {
+ rotationTargetsDevRelease = optionsParser.getOptionalBooleanValue(true);
+ }
+ else if ("v1-signing-enabled".equals(optionName)) {
v1SigningEnabled = optionsParser.getOptionalBooleanValue(true);
} else if ("v2-signing-enabled".equals(optionName)) {
v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
@@ -160,6 +181,14 @@ public class ApkSignerTool {
} else if ("v4-signing-enabled".equals(optionName)) {
v4SigningEnabled = optionsParser.getOptionalBooleanValue(true);
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)) {
+ verityEnabled = optionsParser.getOptionalBooleanValue(true);
} else if ("debuggable-apk-permitted".equals(optionName)) {
debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true);
} else if ("next-signer".equals(optionName)) {
@@ -229,6 +258,14 @@ public class ApkSignerTool {
} else if ("stamp-signer".equals(optionName)) {
sourceStampFlagFound = true;
sourceStampSignerParams = processSignerParams(optionsParser);
+ } else if ("stamp-lineage".equals(optionName)) {
+ File stampLineageFile = new File(
+ optionsParser.getRequiredValue("Stamp Lineage File"));
+ sourceStampLineage = getLineageFromInputFile(stampLineageFile);
+ } else if ("deterministic-dsa-signing".equals(optionName)) {
+ deterministicDsaSigning = optionsParser.getOptionalBooleanValue(false);
+ } else if ("append-signature".equals(optionName)) {
+ otherSignersSignaturesPreserved = optionsParser.getOptionalBooleanValue(true);
} else {
throw new ParameterException(
"Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -285,7 +322,8 @@ public class ApkSignerTool {
for (SignerParams signer : signers) {
signerNumber++;
signer.setName("signer #" + signerNumber);
- ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever);
+ ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever,
+ deterministicDsaSigning);
if (signerConfig == null) {
return;
}
@@ -294,7 +332,8 @@ public class ApkSignerTool {
if (sourceStampFlagFound) {
sourceStampSignerParams.setName("stamp signer");
sourceStampSignerConfig =
- getSignerConfig(sourceStampSignerParams, passwordRetriever);
+ getSignerConfig(sourceStampSignerParams, passwordRetriever,
+ deterministicDsaSigning);
if (sourceStampSignerConfig == null) {
return;
}
@@ -315,14 +354,20 @@ public class ApkSignerTool {
new ApkSigner.Builder(signerConfigs)
.setInputApk(inputApk)
.setOutputApk(tmpOutputApk)
- .setOtherSignersSignaturesPreserved(false)
+ .setOtherSignersSignaturesPreserved(otherSignersSignaturesPreserved)
.setV1SigningEnabled(v1SigningEnabled)
.setV2SigningEnabled(v2SigningEnabled)
.setV3SigningEnabled(v3SigningEnabled)
.setV4SigningEnabled(v4SigningEnabled)
+ .setForceSourceStampOverwrite(forceSourceStampOverwrite)
+ .setSourceStampTimestampEnabled(sourceStampTimestampEnabled)
+ .setAlignFileSize(alignFileSize)
+ .setVerityEnabled(verityEnabled)
.setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
.setDebuggableApkPermitted(debuggableApkPermitted)
- .setSigningCertificateLineage(lineage);
+ .setSigningCertificateLineage(lineage)
+ .setMinSdkVersionForRotation(rotationMinSdkVersion)
+ .setRotationTargetsDevRelease(rotationTargetsDevRelease);
if (minSdkVersionSpecified) {
apkSignerBuilder.setMinSdkVersion(minSdkVersion);
}
@@ -333,7 +378,8 @@ public class ApkSignerTool {
apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
}
if (sourceStampSignerConfig != null) {
- apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
+ apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig)
+ .setSourceStampSigningCertificateLineage(sourceStampLineage);
}
ApkSigner apkSigner = apkSignerBuilder.build();
try {
@@ -358,8 +404,8 @@ public class ApkSignerTool {
}
}
- private static ApkSigner.SignerConfig getSignerConfig(
- SignerParams signer, PasswordRetriever passwordRetriever) {
+ private static ApkSigner.SignerConfig getSignerConfig(SignerParams signer,
+ PasswordRetriever passwordRetriever, boolean deterministicDsaSigning) {
try {
signer.loadPrivateKeyAndCerts(passwordRetriever);
} catch (ParameterException e) {
@@ -391,7 +437,8 @@ public class ApkSignerTool {
}
ApkSigner.SignerConfig signerConfig =
new ApkSigner.SignerConfig.Builder(
- v1SigBasename, signer.getPrivateKey(), signer.getCerts())
+ v1SigBasename, signer.getPrivateKey(), signer.getCerts(),
+ deterministicDsaSigning)
.build();
return signerConfig;
}
@@ -408,12 +455,15 @@ public class ApkSignerTool {
int maxSdkVersion = Integer.MAX_VALUE;
boolean maxSdkVersionSpecified = false;
boolean printCerts = false;
+ boolean printCertsPem = false;
boolean verbose = false;
boolean warningsTreatedAsErrors = false;
+ boolean verifySourceStamp = false;
File v4SignatureFile = null;
OptionsParser optionsParser = new OptionsParser(params);
String optionName;
String optionOriginalForm = null;
+ String sourceCertDigest = null;
while ((optionName = optionsParser.nextOption()) != null) {
optionOriginalForm = optionsParser.getOptionOriginalForm();
if ("min-sdk-version".equals(optionName)) {
@@ -424,6 +474,13 @@ public class ApkSignerTool {
maxSdkVersionSpecified = true;
} else if ("print-certs".equals(optionName)) {
printCerts = optionsParser.getOptionalBooleanValue(true);
+ } else if ("print-certs-pem".equals(optionName)) {
+ printCertsPem = optionsParser.getOptionalBooleanValue(true);
+ // If the PEM output of the certs is requested, this implicitly implies the
+ // cert details should be printed.
+ if (printCertsPem && !printCerts) {
+ printCerts = true;
+ }
} else if (("v".equals(optionName)) || ("verbose".equals(optionName))) {
verbose = optionsParser.getOptionalBooleanValue(true);
} else if ("Werr".equals(optionName)) {
@@ -436,6 +493,11 @@ public class ApkSignerTool {
"Input V4 Signature File"));
} else if ("in".equals(optionName)) {
inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
+ } else if ("verify-source-stamp".equals(optionName)) {
+ verifySourceStamp = optionsParser.getOptionalBooleanValue(true);
+ } else if ("stamp-cert-digest".equals(optionName)) {
+ sourceCertDigest = optionsParser.getRequiredValue(
+ "Expected source stamp certificate digest");
} else {
throw new ParameterException(
"Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -488,7 +550,9 @@ public class ApkSignerTool {
ApkVerifier apkVerifier = apkVerifierBuilder.build();
ApkVerifier.Result result;
try {
- result = apkVerifier.verify();
+ result = verifySourceStamp
+ ? apkVerifier.verifySourceStamp(sourceCertDigest)
+ : apkVerifier.verify();
} catch (MinSdkVersionException e) {
String msg = e.getMessage();
if (!msg.endsWith(".")) {
@@ -499,8 +563,9 @@ public class ApkSignerTool {
+ ". Use --min-sdk-version to override",
e);
}
- boolean verified = result.isVerified();
+ boolean verified = result.isVerified();
+ ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
boolean warningsEncountered = false;
if (verified) {
List<X509Certificate> signerCerts = result.getSignerCertificates();
@@ -516,16 +581,48 @@ public class ApkSignerTool {
"Verified using v3 scheme (APK Signature Scheme v3): "
+ result.isVerifiedUsingV3Scheme());
System.out.println(
+ "Verified using v3.1 scheme (APK Signature Scheme v3.1): "
+ + result.isVerifiedUsingV31Scheme());
+ System.out.println(
"Verified using v4 scheme (APK Signature Scheme v4): "
+ result.isVerifiedUsingV4Scheme());
System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
- System.out.println("Number of signers: " + signerCerts.size());
+ if (!verifySourceStamp) {
+ System.out.println("Number of signers: " + signerCerts.size());
+ }
}
if (printCerts) {
- int signerNumber = 0;
- for (X509Certificate signerCert : signerCerts) {
- signerNumber++;
- printCertificate(signerCert, "Signer #" + signerNumber, verbose);
+ // The v3.1 signature scheme allows key rotation to target T+ while the original
+ // signing key can still be used with v3.0; if a v3.1 block is present then also
+ // include the target SDK versions for both rotation and the original signing key.
+ if (result.isVerifiedUsingV31Scheme()) {
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer :
+ result.getV31SchemeSigners()) {
+
+ printCertificate(signer.getCertificate(),
+ "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+ + (signer.getRotationTargetsDevRelease()
+ ? " (dev release=true)" : "")
+ + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+ verbose, printCertsPem);
+ }
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ printCertificate(signer.getCertificate(),
+ "Signer (minSdkVersion=" + signer.getMinSdkVersion()
+ + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")",
+ verbose, printCertsPem);
+ }
+ } else {
+ int signerNumber = 0;
+ for (X509Certificate signerCert : signerCerts) {
+ signerNumber++;
+ printCertificate(signerCert, "Signer #" + signerNumber, verbose,
+ printCertsPem);
+ }
+ }
+ if (sourceStampInfo != null) {
+ printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
+ verbose, printCertsPem);
}
}
} else {
@@ -537,7 +634,7 @@ public class ApkSignerTool {
}
@SuppressWarnings("resource") // false positive -- this resource is not opened here
- PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out;
+ PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out;
for (ApkVerifier.IssueWithParams warning : result.getWarnings()) {
warningsEncountered = true;
warningsOut.println("WARNING: " + warning);
@@ -576,8 +673,21 @@ public class ApkSignerTool {
"WARNING: APK Signature Scheme v3 " + signerName + ": " + warning);
}
}
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1) + "(minSdkVersion="
+ + signer.getMinSdkVersion() + ", maxSdkVersion=" + signer.getMaxSdkVersion()
+ + ")";
+ for (ApkVerifier.IssueWithParams error : signer.getErrors()) {
+ System.err.println(
+ "ERROR: APK Signature Scheme v3.1 " + signerName + ": " + error);
+ }
+ for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) {
+ warningsEncountered = true;
+ warningsOut.println(
+ "WARNING: APK Signature Scheme v3.1 " + signerName + ": " + warning);
+ }
+ }
- ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
if (sourceStampInfo != null) {
for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
System.err.println("ERROR: SourceStamp: " + error);
@@ -585,6 +695,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) {
@@ -737,6 +850,7 @@ public class ApkSignerTool {
boolean verbose = false;
boolean printCerts = false;
+ boolean printCertsPem = false;
boolean lineageUpdated = false;
File inputKeyLineage = null;
File outputKeyLineage = null;
@@ -758,6 +872,13 @@ public class ApkSignerTool {
verbose = optionsParser.getOptionalBooleanValue(true);
} else if ("print-certs".equals(optionName)) {
printCerts = optionsParser.getOptionalBooleanValue(true);
+ } else if ("print-certs-pem".equals(optionName)) {
+ printCertsPem = optionsParser.getOptionalBooleanValue(true);
+ // If the PEM output of the certs is requested, this implicitly implies the
+ // cert details should be printed.
+ if (printCertsPem && !printCerts) {
+ printCerts = true;
+ }
} else {
throw new ParameterException(
"Unsupported option: " + optionsParser.getOptionOriginalForm()
@@ -818,7 +939,8 @@ public class ApkSignerTool {
for (int i = 0; i < signingCerts.size(); i++) {
X509Certificate signerCert = signingCerts.get(i);
SignerCapabilities signerCapabilities = lineage.getSignerCapabilities(signerCert);
- printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose);
+ printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose,
+ printCertsPem);
printCapabilities(signerCapabilities);
}
}
@@ -936,10 +1058,10 @@ public class ApkSignerTool {
private static void printUsage(String page) {
try (BufferedReader in =
- new BufferedReader(
- new InputStreamReader(
- ApkSignerTool.class.getResourceAsStream(page),
- StandardCharsets.UTF_8))) {
+ new BufferedReader(
+ new InputStreamReader(
+ ApkSignerTool.class.getResourceAsStream(page),
+ StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
@@ -950,20 +1072,29 @@ public class ApkSignerTool {
}
/**
+ * @see #printCertificate(X509Certificate, String, boolean, boolean)
+ */
+ public static void printCertificate(X509Certificate cert, String name, boolean verbose)
+ throws NoSuchAlgorithmException, CertificateEncodingException {
+ printCertificate(cert, name, verbose, false);
+ }
+
+ /**
* Prints details from the provided certificate to stdout.
*
* @param cert the certificate to be displayed.
* @param name the name to be used to identify the certificate.
* @param verbose boolean indicating whether public key details from the certificate should be
* displayed.
- *
+ * @param pemOutput boolean indicating whether the PEM encoding of the certificate should be
+ * displayed.
* @throws NoSuchAlgorithmException if an instance of MD5, SHA-1, or SHA-256 cannot be
* obtained.
* @throws CertificateEncodingException if an error is encountered when encoding the
* certificate.
*/
- public static void printCertificate(X509Certificate cert, String name, boolean verbose)
- throws NoSuchAlgorithmException, CertificateEncodingException {
+ public static void printCertificate(X509Certificate cert, String name, boolean verbose,
+ boolean pemOutput) throws NoSuchAlgorithmException, CertificateEncodingException {
if (cert == null) {
throw new NullPointerException("cert == null");
}
@@ -1008,6 +1139,18 @@ public class ApkSignerTool {
System.out.println(
name + " public key MD5 digest: " + HexEncoding.encode(md5.digest(encodedKey)));
}
+
+ if (pemOutput) {
+ System.out.println(BEGIN_CERTIFICATE);
+ final int lineWidth = 64;
+ String pemEncodedCert = Base64.getEncoder().encodeToString(cert.getEncoded());
+ for (int i = 0; i < pemEncodedCert.length(); i += lineWidth) {
+ System.out.println(pemEncodedCert.substring(i, i + lineWidth > pemEncodedCert.length()
+ ? pemEncodedCert.length()
+ : i + lineWidth));
+ }
+ System.out.println(END_CERTIFICATE);
+ }
}
/**
@@ -1046,10 +1189,19 @@ public class ApkSignerTool {
}
Provider provider;
if (constructorParam != null) {
- // Single-arg Provider constructor
- provider =
- (Provider) providerClass.getConstructor(String.class)
- .newInstance(constructorParam);
+ try {
+ // Single-arg Provider constructor
+ provider =
+ (Provider) providerClass.getConstructor(String.class)
+ .newInstance(constructorParam);
+ } catch (NoSuchMethodException e) {
+ // Starting from JDK 9 the single-arg constructor accepting the configuration
+ // has been replaced by a configure(String) method to be invoked after
+ // instantiating the Provider with the no-arg constructor.
+ provider = (Provider) providerClass.getConstructor().newInstance();
+ provider = (Provider) providerClass.getMethod("configure", String.class)
+ .invoke(provider, constructorParam);
+ }
} else {
// No-arg Provider constructor
provider = (Provider) providerClass.getConstructor().newInstance();
diff --git a/src/apksigner/java/com/android/apksigner/SignerParams.java b/src/apksigner/java/com/android/apksigner/SignerParams.java
index 8c8b550..515cd41 100644
--- a/src/apksigner/java/com/android/apksigner/SignerParams.java
+++ b/src/apksigner/java/com/android/apksigner/SignerParams.java
@@ -209,10 +209,19 @@ public class SignerParams {
}
Provider ksProvider;
if (keystoreProviderArg != null) {
- // Single-arg Provider constructor
- ksProvider =
- (Provider) ksProviderClass.getConstructor(String.class)
- .newInstance(keystoreProviderArg);
+ try {
+ // Single-arg Provider constructor
+ ksProvider =
+ (Provider) ksProviderClass.getConstructor(String.class)
+ .newInstance(keystoreProviderArg);
+ } catch (NoSuchMethodException e) {
+ // Starting from JDK 9 the single-arg constructor accepting the configuration
+ // has been replaced by a configure(String) method to be invoked after
+ // instantiating the Provider with the no-arg constructor.
+ ksProvider = (Provider) ksProviderClass.getConstructor().newInstance();
+ ksProvider = (Provider) ksProviderClass.getMethod("configure",
+ String.class).invoke(ksProvider, keystoreProviderArg);
+ }
} else {
// No-arg Provider constructor
ksProvider = (Provider) ksProviderClass.getConstructor().newInstance();
diff --git a/src/apksigner/java/com/android/apksigner/help_lineage.txt b/src/apksigner/java/com/android/apksigner/help_lineage.txt
index 3f4922d..8fe410b 100644
--- a/src/apksigner/java/com/android/apksigner/help_lineage.txt
+++ b/src/apksigner/java/com/android/apksigner/help_lineage.txt
@@ -19,6 +19,10 @@ has been migrated to the new signing certificate.
--print-certs Show information about the signing certificates and their capabilities
in the SigningCertificateLineage.
+--print-certs-pem Show information about the signing certificates and their capabilities
+ in the SigningCertificateLineage; prints the PEM encoding of each signing
+ certificate to stdout.
+
-v, --verbose Verbose output mode.
-h, --help Show help about this command and exit.
diff --git a/src/apksigner/java/com/android/apksigner/help_rotate.txt b/src/apksigner/java/com/android/apksigner/help_rotate.txt
index ff58372..d19136b 100644
--- a/src/apksigner/java/com/android/apksigner/help_rotate.txt
+++ b/src/apksigner/java/com/android/apksigner/help_rotate.txt
@@ -43,10 +43,17 @@ used in some situations on the platform even though the APK is now being signed
by a newer signing certificate. By default, the new signer will have all
capabilities, but the capability options can be specified for the new signer
during rotation to act as a default level of trust when moving to a newer
-signing certificate.The capability options accept an optional boolean value of
+signing certificate. The capability options accept an optional boolean value of
true or false; if this value is not specified then the option will default to
true.
+Prior to Android 12, if multiple apps shared a common signer in their signing lineage
+with distinct capabilities assigned, a bug in the platform would cause the capabilities
+declared for this signer in one of the app's signing lineage to be assigned to this same
+common signer in the lineage of the rest of the apps. Apps that use the default capabilities,
+or that assign the same capabilities to a common signer in their lineage, are not impacted
+by this bug.
+
--ks Load private key and certificate chain from the Java
KeyStore initialized from the specified file. NONE means
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index 92ab99c..dc5f6cc 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -46,6 +46,15 @@ certificate.
enabled based on min and max SDK version (see
--min-sdk-version and --max-sdk-version).
+--force-stamp-overwrite Whether to overwrite existing source stamp in the
+ APK, if found. By default, it is set to false. It has no
+ effect if no source stamp signer config is provided.
+
+--align-file-size Produces APK file sized as multiples of 4K bytes.
+
+--verity-enabled Whether to enable the verity signature algorithm for the
+ v2 and v3 signature schemes.
+
--min-sdk-version Lowest API Level on which this APK's signatures will be
verified. By default, the value from AndroidManifest.xml
is used. The higher the value, the stronger security
@@ -54,6 +63,24 @@ certificate.
--max-sdk-version Highest API Level on which this APK's signatures will be
verified. By default, the highest possible value is used.
+--rotation-min-sdk-version Lowest API Level for which an APK's rotated signing
+ key should be used to produce the APK's signature. The
+ original signing key for the APK will be used for all
+ previous platform versions. Specifying a value <= 32
+ (Android Sv2) will result in the original V3 signing block
+ being used without platform targeting. By default,
+ rotated signing keys will be used with the V3.1 signing
+ block which supports Android T+.
+
+--rotation-targets-dev-release The specified rotation-min-sdk-version is intended
+ for a platform release under development. During development
+ of a new platform, the API Level of the previously released
+ platform is used as the API Level of the development
+ platform until the SDK is finalized. This flag allows
+ targeting signing key rotation to a development platform
+ with API Level X while preventing the rotated key from being
+ used on the latest release platform with API Level X.
+
--debuggable-apk-permitted Whether to permit signing android:debuggable="true"
APKs. Android disables some of its security protections
for such apps. For example, anybody with ADB shell access
@@ -83,6 +110,18 @@ certificate.
can also be specified; the lineage will then be read from
the signed data in the APK.
+--deterministic-dsa-signing When signing with the DSA signature algorithm,
+ whether to use the deterministic version as specified in
+ RFC 6979.
+
+--append-signature Appends the current signature to any signatures that
+ already exist within the APK. This option can be used
+ when an APK is signed by multiple independent signers to
+ allow each to add their own signature without needing to
+ share their private key. This option can also be used to
+ preserve existing key / value blocks that exist within the
+ APK signing block.
+
-h, --help Show help about this command and exit
diff --git a/src/apksigner/java/com/android/apksigner/help_verify.txt b/src/apksigner/java/com/android/apksigner/help_verify.txt
index c5cf663..bc70924 100644
--- a/src/apksigner/java/com/android/apksigner/help_verify.txt
+++ b/src/apksigner/java/com/android/apksigner/help_verify.txt
@@ -11,6 +11,9 @@ range of API Levels.
--print-certs Show information about the APK's signing certificates
+--print-certs-pem Show information about the APK's signing certificates and prints the PEM
+ encoding of each signing certificate to stdout.
+
-v, --verbose Verbose output mode
--min-sdk-version Lowest API Level on which this APK's signatures will be
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index e60fd69..46e03e8 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -17,11 +17,14 @@
package com.android.apksig;
import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.util.ByteBufferDataSource;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.EocdRecord;
@@ -46,6 +49,7 @@ import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -80,16 +84,25 @@ public class ApkSigner {
private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
+ private static final short ANDROID_FILE_ALIGNMENT_BYTES = 4096;
+
/** Name of the Android manifest ZIP entry in APKs. */
private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
private final List<SignerConfig> mSignerConfigs;
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;
private final boolean mV1SigningEnabled;
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
private final boolean mV4SigningEnabled;
+ private final boolean mAlignFileSize;
+ private final boolean mVerityEnabled;
private final boolean mV4ErrorReportingEnabled;
private final boolean mDebuggableApkPermitted;
private final boolean mOtherSignersSignaturesPreserved;
@@ -111,11 +124,18 @@ public class ApkSigner {
private ApkSigner(
List<SignerConfig> signerConfigs,
SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ boolean forceSourceStampOverwrite,
+ boolean sourceStampTimestampEnabled,
Integer minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
boolean v4SigningEnabled,
+ boolean alignFileSize,
+ boolean verityEnabled,
boolean v4ErrorReportingEnabled,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
@@ -131,11 +151,18 @@ public class ApkSigner {
mSignerConfigs = signerConfigs;
mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ mForceSourceStampOverwrite = forceSourceStampOverwrite;
+ mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
mV4SigningEnabled = v4SigningEnabled;
+ mAlignFileSize = alignFileSize;
+ mVerityEnabled = verityEnabled;
mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
mDebuggableApkPermitted = debuggableApkPermitted;
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
@@ -170,7 +197,7 @@ public class ApkSigner {
*/
public void sign()
throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
- SignatureException, IllegalStateException {
+ SignatureException, IllegalStateException {
Closeable in = null;
DataSource inputApk;
try {
@@ -216,7 +243,7 @@ public class ApkSigner {
private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn)
throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
- SignatureException {
+ SignatureException {
// Step 1. Find input APK's main ZIP sections
ApkUtils.ZipSections inputZipSections;
try {
@@ -272,9 +299,10 @@ public class ApkSigner {
for (SignerConfig signerConfig : mSignerConfigs) {
engineSignerConfigs.add(
new DefaultApkSignerEngine.SignerConfig.Builder(
- signerConfig.getName(),
- signerConfig.getPrivateKey(),
- signerConfig.getCertificates())
+ signerConfig.getName(),
+ signerConfig.getPrivateKey(),
+ signerConfig.getCertificates(),
+ signerConfig.getDeterministicDsaSigning())
.build());
}
DefaultApkSignerEngine.Builder signerEngineBuilder =
@@ -282,19 +310,28 @@ public class ApkSigner {
.setV1SigningEnabled(mV1SigningEnabled)
.setV2SigningEnabled(mV2SigningEnabled)
.setV3SigningEnabled(mV3SigningEnabled)
+ .setVerityEnabled(mVerityEnabled)
.setDebuggableApkPermitted(mDebuggableApkPermitted)
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
- .setSigningCertificateLineage(mSigningCertificateLineage);
+ .setSigningCertificateLineage(mSigningCertificateLineage)
+ .setMinSdkVersionForRotation(mRotationMinSdkVersion)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease);
if (mCreatedBy != null) {
signerEngineBuilder.setCreatedBy(mCreatedBy);
}
if (mSourceStampSignerConfig != null) {
signerEngineBuilder.setStampSignerConfig(
new DefaultApkSignerEngine.SignerConfig.Builder(
- mSourceStampSignerConfig.getName(),
- mSourceStampSignerConfig.getPrivateKey(),
- mSourceStampSignerConfig.getCertificates())
+ mSourceStampSignerConfig.getName(),
+ mSourceStampSignerConfig.getPrivateKey(),
+ mSourceStampSignerConfig.getCertificates(),
+ mSourceStampSignerConfig.getDeterministicDsaSigning())
.build());
+ signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled);
+ }
+ if (mSourceStampSigningCertificateLineage != null) {
+ signerEngineBuilder.setSourceStampSigningCertificateLineage(
+ mSourceStampSigningCertificateLineage);
}
signerEngine = signerEngineBuilder.build();
}
@@ -317,6 +354,7 @@ public class ApkSigner {
int lastModifiedTimeForNewEntries = -1;
long inputOffset = 0;
long outputOffset = 0;
+ byte[] sourceStampCertificateDigest = null;
Map<String, CentralDirectoryRecord> outputCdRecordsByName =
new HashMap<>(inputCdRecords.size());
for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
@@ -324,6 +362,16 @@ public class ApkSigner {
if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
continue; // We'll re-add below if needed.
}
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
+ try {
+ sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+ } catch (ZipFormatException ex) {
+ throw new ApkFormatException("Bad source stamp entry");
+ }
+ continue; // Existing source stamp is handled below as needed.
+ }
ApkSignerEngine.InputJarEntryInstructions entryInstructions =
signerEngine.inputJarEntry(entryName);
boolean shouldOutput;
@@ -375,7 +423,7 @@ public class ApkSigner {
if ((lastModifiedDateForNewEntries == -1)
|| (lastModifiedDate > lastModifiedDateForNewEntries)
|| ((lastModifiedDate == lastModifiedDateForNewEntries)
- && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
+ && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
lastModifiedDateForNewEntries = lastModifiedDate;
lastModifiedTimeForNewEntries = lastModifiedTime;
}
@@ -462,15 +510,48 @@ public class ApkSigner {
// records.
if (signerEngine.isEligibleForSourceStamp()) {
byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
+ if (mForceSourceStampOverwrite
+ || sourceStampCertificateDigest == null
+ || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
+ outputOffset +=
+ outputDataToOutputApk(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ } else {
+ throw new ApkFormatException(
+ String.format(
+ "Cannot generate SourceStamp. APK contains an existing entry with"
+ + " the name: %s, and it is different than the provided source"
+ + " stamp certificate",
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
+ }
+ }
+
+ // Step 7.5. Generate pinlist.meta file if necessary.
+ // This has to be before the step 8 so that the file is signed.
+ if (pinByteRanges != null) {
+ // Covers JAR signature and zip central dir entry.
+ // The signature files don't have to be pinned, but pinning them isn't that wasteful
+ // since the total size is small.
+ pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));
+ String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
+ byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
+
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
outputOffset +=
- outputDataToOutputApk(
- SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
- uncompressedData,
- outputOffset,
- outputCdRecords,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- outputApkOut);
+ outputDataToOutputApk(
+ entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
}
// Step 8. Generate and output JAR signatures, if necessary. This may output more Local File
@@ -483,15 +564,7 @@ public class ApkSigner {
String entryName = entry.getName();
byte[] uncompressedData = entry.getData();
- ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
- signerEngine.outputJarEntry(entryName);
- if (inspectEntryRequest != null) {
- inspectEntryRequest
- .getDataSink()
- .consume(uncompressedData, 0, uncompressedData.length);
- inspectEntryRequest.done();
- }
-
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
outputOffset +=
outputDataToOutputApk(
entryName,
@@ -505,21 +578,6 @@ public class ApkSigner {
outputJarSignatureRequest.done();
}
- if (pinByteRanges != null) {
- pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir
- String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
- byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
- outputOffset +=
- outputDataToOutputApk(
- entryName,
- uncompressedData,
- outputOffset,
- outputCdRecords,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- outputApkOut);
- }
-
// Step 9. Construct output ZIP Central Directory in an in-memory buffer
long outputCentralDirSizeBytes = 0;
for (CentralDirectoryRecord record : outputCdRecords) {
@@ -541,6 +599,9 @@ public class ApkSigner {
int outputCentralDirRecordCount = outputCdRecords.size();
// Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer
+ // because it can be adjusted in Step 11 due to signing block.
+ // - CD offset (it's shifted by signing block)
+ // - Comments (when the output file needs to be sized 4k-aligned)
ByteBuffer outputEocd =
EocdRecord.createWithModifiedCentralDirectoryInfo(
inputZipSections.getZipEndOfCentralDirectory(),
@@ -559,13 +620,39 @@ public class ApkSigner {
if (outputApkSigningBlockRequest != null) {
int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock();
- outputApkOut.consume(ByteBuffer.allocate(padding));
byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+
+ long fileSize =
+ outputCentralDirStartOffset
+ + outputCentralDirDataSource.size()
+ + padding
+ + outputApkSigningBlock.length
+ + outputEocd.remaining();
+ if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) {
+ int eocdPadding =
+ (int)
+ (ANDROID_FILE_ALIGNMENT_BYTES
+ - fileSize % ANDROID_FILE_ALIGNMENT_BYTES);
+ // Replace EOCD with padding one so that output file size can be the multiples of
+ // alignment.
+ outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding);
+
+ // Since EoCD has changed, we need to regenerate signing block as well.
+ outputApkSigningBlockRequest =
+ signerEngine.outputZipSections2(
+ outputApkIn,
+ new ByteBufferDataSource(outputCentralDir),
+ DataSources.asDataSource(outputEocd));
+ outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+ }
+
+ outputApkOut.consume(ByteBuffer.allocate(padding));
outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
ZipUtils.setZipEocdCentralDirectoryOffset(
outputEocd,
outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
- outputApkSigningBlockRequest.done();
}
// Step 12. Output ZIP Central Directory and ZIP End of Central Directory
@@ -579,6 +666,20 @@ public class ApkSigner {
}
}
+ private static void requestOutputEntryInspection(
+ ApkSignerEngine signerEngine,
+ String entryName,
+ byte[] uncompressedData)
+ throws IOException {
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(
+ uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest.done();
+ }
+ }
+
private static long outputDataToOutputApk(
String entryName,
byte[] uncompressedData,
@@ -651,7 +752,7 @@ public class ApkSigner {
int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
if ((dataAlignmentMultiple <= 1)
|| ((inputOffset % dataAlignmentMultiple)
- == (outputOffset % dataAlignmentMultiple))) {
+ == (outputOffset % dataAlignmentMultiple))) {
// This record's data will be aligned same as in the input APK.
return new OutputSizeAndDataOffset(
inputRecord.outputRecord(inputLfhSection, outputLfhSection),
@@ -917,14 +1018,18 @@ 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) {
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
-
/** Returns the name of this signer. */
public String getName() {
return mName;
@@ -943,11 +1048,36 @@ public class ApkSigner {
return mCertificates;
}
+
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
/** 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;
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param name signer's name. The name is reflected in the name of files comprising the
+ * JAR signature of the APK.
+ * @param privateKey signing key
+ * @param certificates list of one or more X.509 certificates. The subject public key of
+ * the first certificate must correspond to the {@code privateKey}.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
/**
* Constructs a new {@code Builder}.
@@ -957,14 +1087,21 @@ public class ApkSigner {
* @param privateKey signing key
* @param certificates list of one or more X.509 certificates. The subject public key of
* the first certificate must correspond to the {@code privateKey}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic variant (RFC6979) should be used.
*/
- public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/**
@@ -972,7 +1109,8 @@ public class ApkSigner {
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(mName, mPrivateKey, mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates,
+ mDeterministicDsaSigning);
}
}
}
@@ -992,15 +1130,22 @@ public class ApkSigner {
public static class Builder {
private final List<SignerConfig> mSignerConfigs;
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;
private boolean mV4SigningEnabled = false;
+ private boolean mAlignFileSize = false;
+ private boolean mVerityEnabled = false;
private boolean mV4ErrorReportingEnabled = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy;
private Integer mMinSdkVersion;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
private final ApkSignerEngine mSignerEngine;
@@ -1069,6 +1214,35 @@ public class ApkSigner {
}
/**
+ * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+ * signing certificate rotation for certificates previously used to sign source stamps.
+ */
+ public Builder setSourceStampSigningCertificateLineage(
+ SigningCertificateLineage sourceStampSigningCertificateLineage) {
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should overwrite existing source stamp, if found.
+ *
+ * @param force {@code true} to require the APK to be overwrite existing source stamp
+ */
+ public Builder setForceSourceStampOverwrite(boolean force) {
+ mForceSourceStampOverwrite = force;
+ return this;
+ }
+
+ /**
+ * 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)
@@ -1189,6 +1363,58 @@ public class ApkSigner {
}
/**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop. This method is useful for overriding the
+ * default behavior where Android T is set as the minimum API level for rotation.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ checkInitializedWithoutEngine();
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
*
* <p>By default, whether APK is signed using JAR signing is determined by {@code
@@ -1273,7 +1499,7 @@ public class ApkSigner {
* <p>V4 signing requires that the APK be v2 or v3 signed.
*
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
- * or v3 and generate an v4 signature file
+ * or v3 and generate an v4 signature file
*/
public Builder setV4SigningEnabled(boolean enabled) {
checkInitializedWithoutEngine();
@@ -1291,7 +1517,7 @@ public class ApkSigner {
* the user did not explicitly request the v4 signing.
*
* @param enabled {@code false} to prevent errors encountered during the V4 signing from
- * halting the signing process
+ * halting the signing process
*/
public Builder setV4ErrorReportingEnabled(boolean enabled) {
checkInitializedWithoutEngine();
@@ -1299,6 +1525,34 @@ public class ApkSigner {
return this;
}
+ /**
+ * Sets whether the output APK files should be sized as multiples of 4K.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
+ */
+ public Builder setAlignFileSize(boolean alignFileSize) {
+ checkInitializedWithoutEngine();
+ mAlignFileSize = alignFileSize;
+ return this;
+ }
+
+ /**
+ * Sets whether to enable the verity signature algorithm for the v2 and v3 signature
+ * schemes.
+ *
+ * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
+ * v2 and v3 signature blocks.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mVerityEnabled = enabled;
+ return this;
+ }
+
/**
* Sets whether the APK should be signed even if it is marked as debuggable ({@code
* android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
@@ -1400,8 +1654,8 @@ public class ApkSigner {
mV4SigningEnabled = false;
} else {
throw new IllegalStateException(
- "APK Signature Scheme v4 signing requires at least "
- + "v2 or v3 signing to be enabled");
+ "APK Signature Scheme v4 signing requires at least "
+ + "v2 or v3 signing to be enabled");
}
}
@@ -1410,11 +1664,18 @@ public class ApkSigner {
return new ApkSigner(
mSignerConfigs,
mSourceStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
+ mForceSourceStampOverwrite,
+ mSourceStampTimestampEnabled,
mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
mV4SigningEnabled,
+ mAlignFileSize,
+ mVerityEnabled,
mV4ErrorReportingEnabled,
mDebuggableApkPermitted,
mOtherSignersSignaturesPreserved,
diff --git a/src/main/java/com/android/apksig/ApkVerificationIssue.java b/src/main/java/com/android/apksig/ApkVerificationIssue.java
new file mode 100644
index 0000000..fa2b7aa
--- /dev/null
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+/**
+ * This class is intended as a lightweight representation of an APK signature verification issue
+ * where the client does not require the additional textual details provided by a subclass.
+ */
+public class ApkVerificationIssue {
+ /* The V2 signer(s) could not be read from the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNERS = 1;
+ /* A V2 signature block exists without any V2 signers */
+ public static final int V2_SIG_NO_SIGNERS = 2;
+ /* Failed to parse a signer's block in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNER = 3;
+ /* Failed to parse the signer's signature record in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
+ /* The V2 signer contained no signatures */
+ public static final int V2_SIG_NO_SIGNATURES = 5;
+ /* The V2 signer's certificate could not be parsed */
+ public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
+ /* No signing certificates exist for the V2 signer */
+ public static final int V2_SIG_NO_CERTIFICATES = 7;
+ /* Failed to parse the V2 signer's digest record */
+ public static final int V2_SIG_MALFORMED_DIGEST = 8;
+ /* The V3 signer(s) could not be read from the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNERS = 9;
+ /* A V3 signature block exists without any V3 signers */
+ public static final int V3_SIG_NO_SIGNERS = 10;
+ /* Failed to parse a signer's block in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNER = 11;
+ /* Failed to parse the signer's signature record in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
+ /* The V3 signer contained no signatures */
+ public static final int V3_SIG_NO_SIGNATURES = 13;
+ /* The V3 signer's certificate could not be parsed */
+ public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
+ /* No signing certificates exist for the V3 signer */
+ public static final int V3_SIG_NO_CERTIFICATES = 15;
+ /* Failed to parse the V3 signer's digest record */
+ public static final int V3_SIG_MALFORMED_DIGEST = 16;
+ /* The source stamp signer contained no signatures */
+ public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
+ /* The source stamp signer's certificate could not be parsed */
+ public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
+ /* The source stamp contains a signature produced using an unknown algorithm */
+ public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
+ /* Failed to parse the signer's signature in the source stamp signature block */
+ public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
+ /* The source stamp's signature block failed verification */
+ public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
+ /* An exception was encountered when verifying the source stamp */
+ public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
+ /* The certificate digest in the APK does not match the expected digest */
+ public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
+ /*
+ * The APK contains a source stamp signature block without a corresponding stamp certificate
+ * digest in the APK contents.
+ */
+ public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
+ /*
+ * The APK does not contain the source stamp certificate digest file nor the source stamp
+ * signature block.
+ */
+ public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
+ /*
+ * None of the signatures provided by the source stamp were produced with a known signature
+ * algorithm.
+ */
+ public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
+ /*
+ * The source stamp signer's certificate in the signing block does not match the certificate in
+ * the APK.
+ */
+ public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
+ /* The APK could not be properly parsed due to a ZIP or APK format exception */
+ public static final int MALFORMED_APK = 28;
+ /* An unexpected exception was caught when attempting to verify the APK's signatures */
+ public static final int UNEXPECTED_EXCEPTION = 29;
+ /* The APK contains the certificate digest file but does not contain a stamp signature block */
+ public static final int SOURCE_STAMP_SIG_MISSING = 30;
+ /* Source stamp block contains a malformed attribute. */
+ public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
+ /* Source stamp block contains an unknown attribute. */
+ public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+ /** No V1 / jar signing signature blocks were found in the APK. */
+ public static final int JAR_SIG_NO_SIGNATURES = 36;
+ /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+ public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+ /** The source stamp timestamp attribute has an invalid value. */
+ public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
+
+ private final int mIssueId;
+ private final String mFormat;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
+ * {@code params}.
+ */
+ public ApkVerificationIssue(String format, Object... params) {
+ mIssueId = -1;
+ mFormat = format;
+ mParams = params;
+ }
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
+ * params}.
+ */
+ public ApkVerificationIssue(int issueId, Object... params) {
+ mIssueId = issueId;
+ mFormat = null;
+ mParams = params;
+ }
+
+ /**
+ * Returns the numeric ID for this issue.
+ */
+ public int getIssueId() {
+ return mIssueId;
+ }
+
+ /**
+ * Returns the optional parameters for this issue.
+ */
+ public Object[] getParams() {
+ return mParams;
+ }
+
+ @Override
+ public String toString() {
+ // If this instance was created by a subclass with a format string then return the same
+ // formatted String as the subclass.
+ if (mFormat != null) {
+ return String.format(mFormat, mParams);
+ }
+ StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
+ for (Object param : mParams) {
+ result.append(", ").append(param.toString());
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 9275290..4d92638 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -17,16 +17,30 @@
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.computeSha256DigestBytes;
+import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest;
+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_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
-import com.android.apksig.internal.apk.AndroidBinXmlParser;
+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.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.apk.stamp.SourceStampVerifier;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
import com.android.apksig.internal.util.AndroidSdkVersion;
@@ -48,6 +62,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -65,12 +80,15 @@ import java.util.Set;
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
+// This class is not used in any Android apps; apksig is only used in Android for source stamp
+// verification provided by the apksig-stamp-verifier target.
+@SuppressWarnings("AndroidJdkLibsChecker")
public class ApkVerifier {
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
loadSupportedApkSigSchemeNames();
- private static Map<Integer,String> loadSupportedApkSigSchemeNames() {
+ private static Map<Integer, String> loadSupportedApkSigSchemeNames() {
Map<Integer, String> supportedMap = new HashMap<>(2);
supportedMap.put(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2");
@@ -111,12 +129,12 @@ public class ApkVerifier {
* or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method
* throws an exception.
*
- * @throws IOException if an I/O error is encountered while reading the APK
- * @throws ApkFormatException if the APK is malformed
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
- * required cryptographic algorithm implementation is missing
- * @throws IllegalStateException if this verifier's configuration is missing required
- * information.
+ * required cryptographic algorithm implementation is missing
+ * @throws IllegalStateException if this verifier's configuration is missing required
+ * information.
*/
public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException,
IllegalStateException {
@@ -146,25 +164,13 @@ public class ApkVerifier {
* The verification result also includes errors, warnings, and information about signers.
*
* @param apk APK file contents
- *
- * @throws IOException if an I/O error is encountered while reading the APK
- * @throws ApkFormatException if the APK is malformed
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
- * required cryptographic algorithm implementation is missing
+ * required cryptographic algorithm implementation is missing
*/
private Result verify(DataSource apk)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
- if (mMinSdkVersion != null) {
- if (mMinSdkVersion < 0) {
- throw new IllegalArgumentException(
- "minSdkVersion must not be negative: " + mMinSdkVersion);
- }
- if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
- throw new IllegalArgumentException(
- "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
- + ")");
- }
- }
int maxSdkVersion = mMaxSdkVersion;
ApkUtils.ZipSections zipSections;
@@ -176,26 +182,11 @@ public class ApkVerifier {
ByteBuffer androidManifest = null;
- int minSdkVersion;
- if (mMinSdkVersion != null) {
- // No need to obtain minSdkVersion from the APK's AndroidManifest.xml
- minSdkVersion = mMinSdkVersion;
- } else {
- // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
- if (androidManifest == null) {
- androidManifest = getAndroidManifestFromApk(apk, zipSections);
- }
- minSdkVersion =
- ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
- if (minSdkVersion > mMaxSdkVersion) {
- throw new IllegalArgumentException(
- "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
- + mMaxSdkVersion + ")");
- }
- }
+ int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
Result result = new Result();
- Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
// The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
// name, but the verifiers use this parameter as the schemes supported by the target SDK
@@ -206,41 +197,66 @@ public class ApkVerifier {
// verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2
// verification is performed it would see the stripping protection attribute, see that V3
// is in the list of supported signatures, and report a stripped signature.
- Map<Integer, String> supportedSchemeNames;
- if (maxSdkVersion >= AndroidSdkVersion.P) {
- supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
- } else if (maxSdkVersion >= AndroidSdkVersion.N) {
- supportedSchemeNames = new HashMap<>(1);
- supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
- SUPPORTED_APK_SIG_SCHEME_NAMES.get(
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
- } else {
- supportedSchemeNames = Collections.emptyMap();
- }
+ Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion);
+
// Android N and newer attempts to verify APKs using the APK Signing Block, which can
// include v2 and/or v3 signatures. If none is found, it falls back to JAR signature
// verification. If the signature is found but does not verify, the APK is rejected.
Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
if (maxSdkVersion >= AndroidSdkVersion.N) {
RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
+ // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+ // also includes stripping protection for the minimum SDK version on which the rotated
+ // signing key should be used.
+ int rotationMinSdkVersion = 0;
+ if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
+ try {
+ ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .build()
+ .verify();
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ rotationMinSdkVersion = v31Result.signers.stream().mapToInt(
+ signer -> signer.minSdkVersion).min().orElse(0);
+ result.mergeFrom(v31Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31,
+ getApkContentDigestsFromSigningSchemeResult(v31Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3.1 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
// Android P and newer attempts to verify APKs using APK Signature Scheme v3
- if (maxSdkVersion >= AndroidSdkVersion.P) {
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) {
try {
- ApkSigningBlockUtils.Result v3Result =
- V3SchemeVerifier.verify(
- executor,
- apk,
- zipSections,
- Math.max(minSdkVersion, AndroidSdkVersion.P),
- maxSdkVersion);
+ V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (rotationMinSdkVersion > 0) {
+ builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+ }
+ ApkSigningBlockUtils.Result v3Result = builder.build().verify();
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
result.mergeFrom(v3Result);
- if (apkContentDigests.isEmpty()) {
- apkContentDigests.putAll(
- getApkContentDigestsFromSigningSchemeResult(v3Result));
- }
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+ getApkContentDigestsFromSigningSchemeResult(v3Result));
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
- // v3 signature not required
+ // v3 signature not required unless a v3.1 signature was found as a v3.1
+ // signature is intended to support key rotation on T+ with the v3 signature
+ // containing the original signing key.
+ if (foundApkSigSchemeIds.contains(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
}
if (result.containsErrors()) {
return result;
@@ -264,10 +280,9 @@ public class ApkVerifier {
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
result.mergeFrom(v2Result);
- if (apkContentDigests.isEmpty()) {
- apkContentDigests.putAll(
- getApkContentDigestsFromSigningSchemeResult(v2Result));
- }
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ getApkContentDigestsFromSigningSchemeResult(v2Result));
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v2 signature not required
}
@@ -276,46 +291,6 @@ public class ApkVerifier {
}
}
- if (maxSdkVersion >= AndroidSdkVersion.R) {
- try {
- List<CentralDirectoryRecord> cdRecords =
- V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
- CentralDirectoryRecord sourceStampCdRecord = null;
- for (CentralDirectoryRecord cdRecord : cdRecords) {
- if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
- cdRecord.getName())) {
- sourceStampCdRecord = cdRecord;
- break;
- }
- }
- // If SourceStamp file is found inside the APK, there must be a SourceStamp
- // block in the APK signing block as well.
- if (sourceStampCdRecord != null) {
- byte[] sourceStampCertificateDigest =
- LocalFileRecord.getUncompressedData(
- apk,
- sourceStampCdRecord,
- zipSections.getZipCentralDirectoryOffset());
- ApkSigningBlockUtils.Result sourceStampResult =
- SourceStampVerifier.verify(
- apk,
- zipSections,
- sourceStampCertificateDigest,
- apkContentDigests,
- Math.max(minSdkVersion, AndroidSdkVersion.R),
- maxSdkVersion);
- result.mergeFrom(sourceStampResult);
- }
- } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
- result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
- } catch (ZipFormatException e) {
- throw new ApkFormatException("Failed to read APK", e);
- }
- if (result.containsErrors()) {
- return result;
- }
- }
-
// If v4 file is specified, use additional verification on it
if (mV4SignatureFile != null) {
final ApkSigningBlockUtils.Result v4Result =
@@ -346,6 +321,9 @@ public class ApkVerifier {
}
}
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
// Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
// Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
@@ -360,6 +338,46 @@ public class ApkVerifier {
minSdkVersion,
maxSdkVersion);
result.mergeFrom(v1Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verify the SourceStamp, if found in the APK.
+ try {
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+ cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+ // If SourceStamp file is found inside the APK, there must be a SourceStamp
+ // block in the APK signing block as well.
+ if (sourceStampCdRecord != null) {
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ Math.max(minSdkVersion, AndroidSdkVersion.R),
+ maxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ }
+ } catch (SignatureNotFoundException ignored) {
+ result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
}
if (result.containsErrors()) {
return result;
@@ -446,7 +464,7 @@ public class ApkVerifier {
}
try {
if (!Arrays.equals(oldSignerCert.getEncoded(),
- v3Signers.get(0).mCerts.get(0).getEncoded())) {
+ v3Signers.get(0).mCerts.get(0).getEncoded())) {
result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH);
}
} catch (CertificateEncodingException e) {
@@ -475,9 +493,6 @@ public class ApkVerifier {
// The apkDigest field in the v4 signature should match the selected v2/v3.
if (result.isVerifiedUsingV4Scheme()) {
List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
- if (v4Signers.size() != 1) {
- result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
- }
List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
v4Signers.get(0).getContentDigests();
@@ -487,21 +502,22 @@ public class ApkVerifier {
final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
if (result.isVerifiedUsingV3Scheme()) {
- List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
- if (v3Signers.size() != 1) {
+ int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1;
+ if (v4Signers.size() != expectedSize) {
result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
}
- // Compare certificates.
- checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result);
-
- // Compare digests.
- final byte[] digestFromV3 = pickBestDigestForV4(
- v3Signers.get(0).getContentDigests());
- if (!Arrays.equals(digestFromV4, digestFromV3)) {
- result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
+ result);
+ if (result.isVerifiedUsingV31Scheme()) {
+ checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
+ digestFromV4, result);
}
} else if (result.isVerifiedUsingV2Scheme()) {
+ if (v4Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
if (v2Signers.size() != 1) {
result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
@@ -521,13 +537,53 @@ public class ApkVerifier {
}
}
+ // If the targetSdkVersion has a minimum required signature scheme version then verify
+ // that the APK was signed with at least that version.
+ try {
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ } catch (ApkFormatException e) {
+ // If the manifest is not available then skip the minimum signature scheme requirement
+ // to support bundle verification.
+ }
+ if (androidManifest != null) {
+ int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+ androidManifest.slice());
+ int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+ // The platform currently only enforces a single minimum signature scheme version, but
+ // when later platform versions support another minimum version this will need to be
+ // expanded to verify the minimum based on the target and maximum SDK version.
+ if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME
+ && maxSdkVersion >= targetSdkVersion) {
+ switch (minSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ if (result.isVerifiedUsingV2Scheme()) {
+ break;
+ }
+ // Allow this case to fall through to the next as a signature satisfying a
+ // later scheme version will also satisfy this requirement.
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) {
+ break;
+ }
+ result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+ targetSdkVersion,
+ minSchemeVersion);
+ }
+ }
+ }
+
if (result.containsErrors()) {
return result;
}
// Verified
result.setVerified();
- if (result.isVerifiedUsingV3Scheme()) {
+ if (result.isVerifiedUsingV31Scheme()) {
+ List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners();
+ result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV3Scheme()) {
List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate());
} else if (result.isVerifiedUsingV2Scheme()) {
@@ -546,7 +602,357 @@ public class ApkVerifier {
return result;
}
- private static void checkV4Certificate(List<X509Certificate> v4Certs, List<X509Certificate> v2v3Certs, Result result) {
+ /**
+ * Verifies and returns the minimum SDK version, either as provided to the builder or as read
+ * from the {@code apk}'s AndroidManifest.xml.
+ */
+ private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws ApkFormatException, IOException {
+ if (mMinSdkVersion != null) {
+ if (mMinSdkVersion < 0) {
+ throw new IllegalArgumentException(
+ "minSdkVersion must not be negative: " + mMinSdkVersion);
+ }
+ if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) {
+ throw new IllegalArgumentException(
+ "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion
+ + ")");
+ }
+ return mMinSdkVersion;
+ }
+
+ ByteBuffer androidManifest = null;
+ // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int minSdkVersion =
+ ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (minSdkVersion > mMaxSdkVersion) {
+ throw new IllegalArgumentException(
+ "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+ + mMaxSdkVersion + ")");
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the mapping of signature scheme version to signature scheme name for all signature
+ * schemes starting from V2 supported by the {@code maxSdkVersion}.
+ */
+ private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) {
+ Map<Integer, String> supportedSchemeNames;
+ if (maxSdkVersion >= AndroidSdkVersion.P) {
+ supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES;
+ } else if (maxSdkVersion >= AndroidSdkVersion.N) {
+ supportedSchemeNames = new HashMap<>(1);
+ supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ SUPPORTED_APK_SIG_SCHEME_NAMES.get(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ } else {
+ supportedSchemeNames = Collections.emptyMap();
+ }
+ return supportedSchemeNames;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ * <p>The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified} returns {@code true}. The details of the source stamp verification can
+ * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or
+ * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the
+ * verification fails additional details regarding the failure can be obtained from {@link
+ * Result#getAllErrors()}}.
+ */
+ public Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private Result verifySourceStamp(DataSource apk, String expectedCertDigest) {
+ try {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
+ stampSigningBlockFound = true;
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ if (stampSigningBlockFound) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ } else {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ }
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus
+ .CERT_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest,
+ expectedCertDigest);
+ }
+ }
+
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion);
+ Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+ Result result = new Result();
+ ApkSigningBlockUtils.Result v3Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ Math.max(minSdkVersion, AndroidSdkVersion.P));
+ if (v3Result != null && v3Result.containsErrors()) {
+ result.mergeFrom(v3Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ ApkSigningBlockUtils.Result v2Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P
+ || foundApkSigSchemeIds.isEmpty())) {
+ v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ Math.max(minSdkVersion, AndroidSdkVersion.N));
+ if (v2Result != null && v2Result.containsErrors()) {
+ result.mergeFrom(v2Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) {
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ // Since the caller is only seeking to verify the source stamp the Result can be marked
+ // as verified if the source stamp verification was successful.
+ if (sourceStampResult.verified) {
+ result.setVerified();
+ } else {
+ // To prevent APK signature verification with a failed / missing source stamp the
+ // source stamp verification will only log warnings; to allow the caller to capture
+ // the failure reason treat all warnings as errors.
+ result.setWarningsAsErrors(true);
+ }
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+ }
+
+ /**
+ * Creates and returns a {@code Result} that can be returned for source stamp verification
+ * with the provided source stamp {@code verificationStatus}, and logs an error for the
+ * specified {@code issue} and {@code params}.
+ */
+ private static Result createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue,
+ Object... params) {
+ Result result = new Result();
+ result.addError(issue, params);
+ return mergeSourceStampResult(verificationStatus, result);
+ }
+
+ /**
+ * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the
+ * source stamp status to the provided {@code verificationStatus}.
+ */
+ private static Result mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus,
+ Result result) {
+ result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus);
+ return result;
+ }
+
+ /**
+ * 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 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)
+ throws IOException, NoSuchAlgorithmException {
+ if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
+ || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) {
+ 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;
+ signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
+ sigSchemeBlockId, result);
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ return null;
+ }
+ foundApkSigSchemeIds.add(apkSigSchemeVersion);
+
+ Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+ V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, supportedSchemeNames,
+ foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result);
+ } else {
+ V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, result);
+ }
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+ signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+ contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(),
+ contentDigest.getValue());
+ }
+ }
+ sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests);
+ return result;
+ }
+
+ private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers,
+ List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) {
+ if (v3Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV3)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ }
+
+ private static void checkV4Certificate(List<X509Certificate> v4Certs,
+ List<X509Certificate> v2v3Certs, Result result) {
try {
byte[] v4Cert = v4Certs.get(0).getEncoded();
byte[] cert = v2v3Certs.get(0).getEncoded();
@@ -558,7 +964,8 @@ public class ApkVerifier {
}
}
- private static byte[] pickBestDigestForV4(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+ private static byte[] pickBestDigestForV4(
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
collectApkContentDigests(contentDigests, apkContentDigests);
return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
@@ -573,7 +980,41 @@ public class ApkVerifier {
return apkContentDigests;
}
- private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ manifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ private static void collectApkContentDigests(
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
@@ -589,7 +1030,7 @@ public class ApkVerifier {
private static ByteBuffer getAndroidManifestFromApk(
DataSource apk, ApkUtils.ZipSections zipSections)
- throws IOException, ApkFormatException {
+ throws IOException, ApkFormatException {
List<CentralDirectoryRecord> cdRecords =
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
try {
@@ -601,67 +1042,11 @@ public class ApkVerifier {
}
}
- /**
- * Android resource ID of the {@code android:targetSandboxVersion} attribute in
- * AndroidManifest.xml.
- */
- private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
-
- /**
- * Returns the security sandbox version targeted by an APK with the provided
- * {@code AndroidManifest.xml}.
- *
- * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
- * resource format
- *
- * @throws ApkFormatException if an error occurred while determining the version
- */
- private static int getTargetSandboxVersionFromBinaryAndroidManifest(
- ByteBuffer androidManifestContents) throws ApkFormatException {
- // Return the value of the android:targetSandboxVersion attribute of the top-level manifest
- // element
- try {
- AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
- int eventType = parser.getEventType();
- while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
- if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
- && (parser.getDepth() == 1)
- && ("manifest".equals(parser.getName()))
- && (parser.getNamespace().isEmpty())) {
- // In each manifest element, targetSandboxVersion defaults to 1
- int result = 1;
- for (int i = 0; i < parser.getAttributeCount(); i++) {
- if (parser.getAttributeNameResourceId(i)
- == TARGET_SANDBOX_VERSION_ATTR_ID) {
- int valueType = parser.getAttributeValueType(i);
- switch (valueType) {
- case AndroidBinXmlParser.VALUE_TYPE_INT:
- result = parser.getAttributeIntValue(i);
- break;
- default:
- throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
- + ": unsupported value type of"
- + " AndroidManifest.xml"
- + " android:targetSandboxVersion"
- + ". Only integer values supported.");
- }
- break;
- }
- }
- return result;
- }
- eventType = parser.next();
- }
- throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
- + " : no manifest element in AndroidManifest.xml");
- } catch (AndroidBinXmlParser.XmlParserException e) {
- throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
- + ": malformed AndroidManifest.xml",
- e);
+ private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
+ if (targetSdkVersion >= AndroidSdkVersion.R) {
+ return VERSION_APK_SIGNATURE_SCHEME_V2;
}
+ return VERSION_JAR_SIGNATURE_SCHEME;
}
/**
@@ -676,6 +1061,7 @@ public class ApkVerifier {
private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>();
private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
private SourceStampInfo mSourceStampInfo;
@@ -683,8 +1069,10 @@ public class ApkVerifier {
private boolean mVerifiedUsingV1Scheme;
private boolean mVerifiedUsingV2Scheme;
private boolean mVerifiedUsingV3Scheme;
+ private boolean mVerifiedUsingV31Scheme;
private boolean mVerifiedUsingV4Scheme;
private boolean mSourceStampVerified;
+ private boolean mWarningsAsErrors;
private SigningCertificateLineage mSigningCertificateLineage;
/**
@@ -720,6 +1108,13 @@ public class ApkVerifier {
}
/**
+ * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+ */
+ public boolean isVerifiedUsingV31Scheme() {
+ return mVerifiedUsingV31Scheme;
+ }
+
+ /**
* Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
*/
public boolean isVerifiedUsingV4Scheme() {
@@ -785,7 +1180,23 @@ public class ApkVerifier {
return mV3SchemeSigners;
}
- private List<V4SchemeSignerInfo> getV4SchemeSigners() {
+ /**
+ * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+ * signature.
+ *
+ * <note> Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single
+ * signer identities.</note>
+ */
+ public List<V3SchemeSignerInfo> getV31SchemeSigners() {
+ return mV31SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v4 signers associated with the APK's
+ * signature.
+ */
+ public List<V4SchemeSignerInfo> getV4SchemeSigners() {
return mV4SchemeSigners;
}
@@ -813,10 +1224,24 @@ public class ApkVerifier {
}
/**
+ * Sets whether warnings should be treated as errors.
+ */
+ void setWarningsAsErrors(boolean value) {
+ mWarningsAsErrors = value;
+ }
+
+ /**
* Returns errors encountered while verifying the APK's signatures.
*/
public List<IssueWithParams> getErrors() {
- return mErrors;
+ if (!mWarningsAsErrors) {
+ return mErrors;
+ } else {
+ List<IssueWithParams> allErrors = new ArrayList<>();
+ allErrors.addAll(mErrors);
+ allErrors.addAll(mWarnings);
+ return allErrors;
+ }
}
/**
@@ -838,6 +1263,21 @@ public class ApkVerifier {
}
}
+ private void mergeFrom(ApkSigResult source) {
+ switch (source.signatureSchemeVersion) {
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.mSigners.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown ApkSigResult Signing Block Scheme Id "
+ + source.signatureSchemeVersion);
+ }
+ }
+
private void mergeFrom(ApkSigningBlockUtils.Result source) {
switch (source.signatureSchemeVersion) {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
@@ -851,6 +1291,16 @@ public class ApkVerifier {
for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
}
+ // Do not overwrite a previously set lineage from a v3.1 signing block.
+ if (mSigningCertificateLineage == null) {
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31:
+ mVerifiedUsingV31Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
mSigningCertificateLineage = source.signingCertificateLineage;
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
@@ -868,8 +1318,6 @@ public class ApkVerifier {
default:
throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
}
- mErrors.addAll(source.getErrors());
- mWarnings.addAll(source.getWarnings());
}
/**
@@ -880,11 +1328,17 @@ public class ApkVerifier {
if (!mErrors.isEmpty()) {
return true;
}
+ if (mWarningsAsErrors && !mWarnings.isEmpty()) {
+ return true;
+ }
if (!mV1SchemeSigners.isEmpty()) {
for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
if (signer.containsErrors()) {
return true;
}
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
}
}
if (!mV2SchemeSigners.isEmpty()) {
@@ -892,6 +1346,9 @@ public class ApkVerifier {
if (signer.containsErrors()) {
return true;
}
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
}
}
if (!mV3SchemeSigners.isEmpty()) {
@@ -899,16 +1356,67 @@ public class ApkVerifier {
if (signer.containsErrors()) {
return true;
}
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
}
}
- if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) {
- return true;
+ if (mSourceStampInfo != null) {
+ if (mSourceStampInfo.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) {
+ return true;
+ }
}
return false;
}
/**
+ * Returns all errors for this result, including any errors from signature scheme signers
+ * and the source stamp.
+ */
+ public List<IssueWithParams> getAllErrors() {
+ List<IssueWithParams> errors = new ArrayList<>();
+ errors.addAll(mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(mWarnings);
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ errors.addAll(mSourceStampInfo.getErrors());
+ if (mWarningsAsErrors) {
+ errors.addAll(mSourceStampInfo.getWarnings());
+ }
+ }
+ return errors;
+ }
+
+ /**
* Information about a JAR signer associated with the APK's signature.
*/
public static class V1SchemeSignerInfo {
@@ -1080,6 +1588,9 @@ public class ApkVerifier {
private final List<IssueWithParams> mWarnings;
private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
mContentDigests;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
mIndex = result.index;
@@ -1087,6 +1598,11 @@ public class ApkVerifier {
mErrors = result.getErrors();
mWarnings = result.getWarnings();
mContentDigests = result.contentDigests;
+ mMinSdkVersion = result.minSdkVersion;
+ mMaxSdkVersion = result.maxSdkVersion;
+ mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt(
+ attribute -> attribute.getId()).anyMatch(
+ attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
}
/**
@@ -1132,6 +1648,33 @@ public class ApkVerifier {
public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
return mContentDigests;
}
+
+ /**
+ * Returns the minimum SDK version on which this signer should be verified.
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /**
+ * Returns the maximum SDK version on which this signer should be verified.
+ */
+ public int getMaxSdkVersion() {
+ return mMaxSdkVersion;
+ }
+
+ /**
+ * Returns whether rotation is targeting a development release.
+ *
+ * <p>A development release uses the SDK version of the previously released platform
+ * until the SDK of the development release is finalized. To allow rotation to target
+ * a development release after T, this attribute must be set to ensure rotation is
+ * used on the development release but ignored on the released platform with the same
+ * API level.
+ */
+ public boolean getRotationTargetsDevRelease() {
+ return mRotationTargetsDevRelease;
+ }
}
/**
@@ -1204,15 +1747,58 @@ public class ApkVerifier {
* Information about SourceStamp associated with the APK's signature.
*/
public static class SourceStampInfo {
+ public enum SourceStampVerificationStatus {
+ /** The stamp is present and was successfully verified. */
+ STAMP_VERIFIED,
+ /** The stamp is present but failed verification. */
+ STAMP_VERIFICATION_FAILED,
+ /** The expected cert digest did not match the digest in the APK. */
+ CERT_DIGEST_MISMATCH,
+ /** The stamp is not present at all. */
+ STAMP_MISSING,
+ /** The stamp is at least partially present, but was not able to be verified. */
+ STAMP_NOT_VERIFIED,
+ /** The stamp was not able to be verified due to an unexpected error. */
+ VERIFICATION_ERROR
+ }
+
private final List<X509Certificate> mCertificates;
+ private final List<X509Certificate> mCertificateLineage;
private final List<IssueWithParams> mErrors;
private final List<IssueWithParams> mWarnings;
+ private final List<IssueWithParams> mInfoMessages;
+
+ private final SourceStampVerificationStatus mSourceStampVerificationStatus;
- private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ private final long mTimestamp;
+
+ private SourceStampInfo(ApkSignerInfo result) {
mCertificates = result.certs;
- mErrors = result.getErrors();
- mWarnings = result.getWarnings();
+ mCertificateLineage = result.certificateLineage;
+ mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getErrors());
+ mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getWarnings());
+ mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getInfoMessages());
+ if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+ mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+ } else {
+ mSourceStampVerificationStatus =
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+ }
+ mTimestamp = result.timestamp;
+ }
+
+ SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+ mCertificates = Collections.emptyList();
+ mCertificateLineage = Collections.emptyList();
+ mErrors = Collections.emptyList();
+ mWarnings = Collections.emptyList();
+ mInfoMessages = Collections.emptyList();
+ mSourceStampVerificationStatus = sourceStampVerificationStatus;
+ mTimestamp = 0;
}
/**
@@ -1226,10 +1812,25 @@ public class ApkVerifier {
return mCertificates.isEmpty() ? null : mCertificates.get(0);
}
+ /**
+ * Returns a list containing all of the certificates in the stamp certificate lineage.
+ */
+ public List<X509Certificate> getCertificatesInLineage() {
+ return mCertificateLineage;
+ }
+
public boolean containsErrors() {
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;
}
@@ -1237,6 +1838,30 @@ public class ApkVerifier {
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
+
+ /**
+ * 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.
+ */
+ public SourceStampVerificationStatus getSourceStampVerificationStatus() {
+ return mSourceStampVerificationStatus;
+ }
+
+ /**
+ * Returns the epoch timestamp in seconds representing the time this source stamp block
+ * was signed, or 0 if the timestamp is not available.
+ */
+ public long getTimestampEpochSeconds() {
+ return mTimestamp;
+ }
}
}
@@ -1583,6 +2208,19 @@ public class ApkVerifier {
+ " %1$d"),
/**
+ * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+ * APK is not signed with that version or later.
+ *
+ * <ul>
+ * <li>Parameter 1: target SDK Version (@code Integer})</li>
+ * <li>Parameter 2: minimum signature scheme version ((@code Integer})</li>
+ * </ul>
+ */
+ MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+ "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+ + " not signed with this or a later signature scheme"),
+
+ /**
* APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
* signature from this signer, but does not contain an APK Signature Scheme v2 signature
* from this signer.
@@ -2047,6 +2685,61 @@ public class ApkVerifier {
+ " using APK Signature Scheme v3 are not all a part of the same overall lineage."),
/**
+ * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+ * was not found.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * </ul>
+ */
+ V31_BLOCK_MISSING(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but a v3.1 block was not found"),
+
+ /**
+ * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+ * targeting rotation in the v3.1 signer block.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_MISMATCH(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+ /**
+ * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+ * SDK version stripping protection attribute was not written to the v3 signer.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_ATTR_MISSING(
+ "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+ + " not contain the attribute to detect if this signature is stripped"),
+
+ /**
+ * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+ * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+ * same as the SDK version for rotation then this should be written to a v3.0 block.
+ */
+ V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+ "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+ /**
+ * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in
+ * the signer; this attribute is only intended for v3.1 signers to indicate they should be
+ * targeting the next development release that is using the SDK version of the previously
+ * released platform SDK version.
+ */
+ V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER(
+ "The rotation-targets-dev-release attribute is only supported on v3.1 signers; "
+ + "this attribute will be ignored by the platform in a v3.0 signer"),
+
+ /**
* APK Signing Block contains an unknown entry.
*
* <ul>
@@ -2201,6 +2894,14 @@ public class ApkVerifier {
"V4 signature format version %1$d is different from the tool's current "
+ "version %2$d"),
+ /**
+ * The APK does not contain the source stamp certificate digest file nor the signature block
+ * when verification expected a source stamp to be present.
+ */
+ SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING(
+ "Neither the source stamp certificate digest file nor the signature block are "
+ + "present in the APK"),
+
/** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
@@ -2247,8 +2948,16 @@ public class ApkVerifier {
/** SourceStamp offers no signatures. */
SOURCE_STAMP_NO_SIGNATURE("No signature"),
- /** SourceStamp offers an unsupported signature. */
- SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
+ /**
+ * SourceStamp offers an unsupported signature.
+ * <ul>
+ * <li>Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp
+ * signing block.
+ * <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of
+ * supported signatures.
+ * </ul>
+ */
+ SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"),
/**
* SourceStamp's certificate listed in the APK signing block does not match the certificate
@@ -2263,7 +2972,97 @@ public class ApkVerifier {
*/
SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
"Certificate mismatch between SourceStamp block in APK signing block and"
- + " SourceStamp file in APK: <%1$s> vs <%2$s>");
+ + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK contains a source stamp signature block without the expected certificate digest
+ * in the APK contents.
+ */
+ SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST(
+ "A source stamp signature block was found without a corresponding certificate "
+ + "digest in the APK"),
+
+ /**
+ * When verifying just the source stamp, the certificate digest in the APK does not match
+ * the expected digest.
+ * <ul>
+ * <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK.
+ * <li>Parameter 2: SHA-256 digest of the expected source stamp certificate.
+ * </ul>
+ */
+ SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH(
+ "The source stamp certificate digest in the APK, %1$s, does not match the "
+ + "expected digest, %2$s"),
+
+ /**
+ * Source stamp block contains a malformed attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+ * </ul>
+ */
+ SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"),
+
+ /**
+ * Source stamp block contains an unknown attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute ID ({@code Integer})</li>
+ * </ul>
+ */
+ SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"),
+
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage "
+ + "structure in the source stamp attributes section."),
+
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ SOURCE_STAMP_POR_CERT_MISMATCH(
+ "APK signing certificate differs from the associated certificate found in the "
+ + "signer's SigningCertificateLineage."),
+
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute "
+ + "contains a proof-of-rotation record with signature(s) that did not verify."),
+
+ /**
+ * The source stamp timestamp attribute has an invalid value (<= 0).
+ * <ul>
+ * <li>Parameter 1: The invalid timestamp value.
+ * </ul>
+ */
+ SOURCE_STAMP_INVALID_TIMESTAMP(
+ "The source stamp"
+ + " timestamp attribute has an invalid value: %1$d"),
+
+ /**
+ * The APK could not be properly parsed due to a ZIP or APK format exception.
+ * <ul>
+ * <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK.
+ * </ul>
+ */
+ MALFORMED_APK(
+ "Malformed APK; the following exception was caught when attempting to parse the "
+ + "APK: %1$s"),
+
+ /**
+ * An unexpected exception was caught when attempting to verify the signature(s) within the
+ * APK.
+ * <ul>
+ * <li>Parameter 1: The {@code Exception} caught during verification.
+ * </ul>
+ */
+ UNEXPECTED_EXCEPTION(
+ "An unexpected exception was caught when verifying the signature: %1$s");
private final String mFormat;
@@ -2284,7 +3083,7 @@ public class ApkVerifier {
* {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
* form.
*/
- public static class IssueWithParams {
+ public static class IssueWithParams extends ApkVerificationIssue {
private final Issue mIssue;
private final Object[] mParams;
@@ -2293,6 +3092,7 @@ public class ApkVerifier {
* parameters.
*/
public IssueWithParams(Issue issue, Object[] params) {
+ super(issue.mFormat, params);
mIssue = issue;
mParams = params;
}
@@ -2407,7 +3207,6 @@ public class ApkVerifier {
* {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}.
*
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
- *
* @see #setMinCheckedPlatformVersion(int)
*/
public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
@@ -2423,7 +3222,6 @@ public class ApkVerifier {
* {@link #setMinCheckedPlatformVersion(int)}.
*
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
- *
* @see #setMinCheckedPlatformVersion(int)
*/
public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
@@ -2449,4 +3247,120 @@ public class ApkVerifier {
mMaxSdkVersion);
}
}
+
+ /**
+ * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link
+ * IssueWithParams} equivalent.
+ */
+ public static class ApkVerificationIssueAdapter {
+ private ApkVerificationIssueAdapter() {
+ }
+
+ // This field is visible for testing
+ static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
+
+ static {
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
+ Issue.V2_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS,
+ Issue.V2_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER,
+ Issue.V2_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE,
+ Issue.V2_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES,
+ Issue.V2_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE,
+ Issue.V2_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES,
+ Issue.V2_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST,
+ Issue.V2_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS,
+ Issue.V3_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS,
+ Issue.V3_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER,
+ Issue.V3_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE,
+ Issue.V3_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES,
+ Issue.V3_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE,
+ Issue.V3_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES,
+ Issue.V3_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST,
+ Issue.V3_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE,
+ Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+ Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE,
+ Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION,
+ Issue.SOURCE_STAMP_VERIFY_EXCEPTION);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK,
+ Issue.MALFORMED_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION,
+ Issue.UNEXPECTED_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+ Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE,
+ Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE,
+ Issue.SOURCE_STAMP_MALFORMED_LINEAGE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH,
+ Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+ Issue.JAR_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ Issue.JAR_SIG_PARSE_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ /**
+ * Converts the provided {@code verificationIssues} to a {@code List} of corresponding
+ * {@link IssueWithParams} instances.
+ */
+ public static List<IssueWithParams> getIssuesFromVerificationIssues(
+ List<? extends ApkVerificationIssue> verificationIssues) {
+ List<IssueWithParams> result = new ArrayList<>(verificationIssues.size());
+ for (ApkVerificationIssue issue : verificationIssues) {
+ if (issue instanceof IssueWithParams) {
+ result.add((IssueWithParams) issue);
+ } else {
+ result.add(
+ new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()),
+ issue.getParams()));
+ }
+ }
+ return result;
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/Constants.java b/src/main/java/com/android/apksig/Constants.java
new file mode 100644
index 0000000..f64064c
--- /dev/null
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+
+/**
+ * Exports internally defined constants to allow clients to reference these values without relying
+ * on internal code.
+ */
+public class Constants {
+ private Constants() {}
+
+ public static final int VERSION_SOURCE_STAMP = 0;
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+ public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+ public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
+}
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index c89f4db..0525886 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -17,17 +17,26 @@
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.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.apk.stamp.SourceStampSigner;
+import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
import com.android.apksig.internal.apk.v1.V1SchemeSigner;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v4.V4SchemeSigner;
import com.android.apksig.internal.apk.v4.V4Signature;
@@ -59,9 +68,10 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
/**
@@ -89,14 +99,22 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private final boolean mV1SigningEnabled;
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
+ private final boolean mVerityEnabled;
private final boolean mDebuggableApkPermitted;
private final boolean mOtherSignersSignaturesPreserved;
private final String mCreatedBy;
private final List<SignerConfig> mSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final boolean mSourceStampTimestampEnabled;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
+ private List<byte[]> mPreservedV2Signers = Collections.emptyList();
+ private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList();
+
private List<V1SchemeSigner.SignerConfig> mV1SignerConfigs = Collections.emptyList();
private DigestAlgorithm mV1ContentDigestAlgorithm;
@@ -153,13 +171,33 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+ /**
+ * A Set of block IDs to be discarded when requesting to preserve the original signatures.
+ */
+ private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS;
+ static {
+ DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3);
+ // The verity padding block is recomputed on an
+ // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID);
+ // The source stamp block is not currently preserved; appending a new signature scheme
+ // block will invalidate the previous source stamp.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID);
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
private DefaultApkSignerEngine(
List<SignerConfig> signerConfigs,
SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ boolean sourceStampTimestampEnabled,
int minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
+ boolean verityEnabled,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
String createdBy,
@@ -168,14 +206,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
- if (otherSignersSignaturesPreserved) {
- throw new UnsupportedOperationException(
- "Preserving other signer's signatures is not yet implemented");
- }
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
+ mVerityEnabled = verityEnabled;
mV1SignaturePending = v1SigningEnabled;
mV2SignaturePending = v2SigningEnabled;
mV3SignaturePending = v3SigningEnabled;
@@ -184,7 +219,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
mCreatedBy = createdBy;
mSignerConfigs = signerConfigs;
mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
mSigningCertificateLineage = signingCertificateLineage;
if (v1SigningEnabled) {
@@ -245,6 +284,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
v1SignerConfig.privateKey = signerConfig.getPrivateKey();
v1SignerConfig.certificates = certificates;
v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+ v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning();
// For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm
// of comparable strength to the digest algorithm used for computing the signature.
// When there are multiple signers, pick the strongest digest algorithm out of their
@@ -301,12 +341,28 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
}
- private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
- boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
- List<ApkSigningBlockUtils.SignerConfig> rawConfigs =
- createSigningBlockSignerConfigs(
- apkSigningBlockPaddingSupported,
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ private boolean signingLineageHas31Support() {
+ return mSigningCertificateLineage != null
+ && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+ && mMinSdkVersion < mRotationMinSdkVersion;
+ }
+
+ private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
+ List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
+ // While the V3 signature scheme supports rotation, it is possible for a caller to specify
+ // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1;
+ // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block
+ // will use the original signing key.
+ if (signingLineageHas31Support()) {
+ SigningCertificateLineage subLineage = mSigningCertificateLineage
+ .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0));
+ if (subLineage.size() != 1) {
+ throw new IllegalArgumentException(
+ "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage"
+ + " for the v3.0 signing block is missing. Please provide"
+ + " the oldest signer to enable v3.1 signing.");
+ }
+ }
List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
@@ -331,20 +387,43 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
// this needs to change
config.maxSdkVersion = Integer.MAX_VALUE;
} else {
- // otherwise, we only want to use this signer up to the minimum platform version
- // on which a newer one is acceptable
- config.maxSdkVersion = currentMinSdk - 1;
+ if (mRotationTargetsDevRelease && currentMinSdk == mRotationMinSdkVersion) {
+ // The currentMinSdk is both the SDK version for the active development release
+ // as well as the most recent released platform. To ensure the v3.0 signer will
+ // target the released platform, overlap the maxSdkVersion for the v3.0 signer
+ // with the minSdkVersion of the rotated signer in the v3.1 block
+ config.maxSdkVersion = currentMinSdk;
+ } else {
+ // otherwise, we only want to use this signer up to the minimum platform version
+ // on which a newer one is acceptable
+ config.maxSdkVersion = currentMinSdk - 1;
+ }
}
config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms);
- if (mSigningCertificateLineage != null) {
+ // Only use a rotated key and signing lineage if the config's max SDK version is greater
+ // than that requested to support rotation.
+ if (mSigningCertificateLineage != null
+ && ((mRotationTargetsDevRelease
+ ? config.maxSdkVersion > mRotationMinSdkVersion
+ : config.maxSdkVersion >= mRotationMinSdkVersion))) {
config.mSigningCertificateLineage =
mSigningCertificateLineage.getSubLineage(config.certificates.get(0));
+ if (config.minSdkVersion < mRotationMinSdkVersion) {
+ config.minSdkVersion = mRotationMinSdkVersion;
+ }
}
// we know that this config will be used, so add it to our result, order doesn't matter
// at this point (and likely only one will be needed
processedConfigs.add(config);
currentMinSdk = config.minSdkVersion;
- if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) {
+ // If the rotation is targeting a development release and this is the v3.1 signer, then
+ // the minSdkVersion of this signer should equal the maxSdkVersion of the next signer;
+ // this ensures a package with the minSdkVersion set to the mRotationMinSdkVersion has
+ // a v3.0 block with the min / max SDK version set to this same minSdkVersion from the
+ // v3.1 block.
+ if ((mRotationTargetsDevRelease && currentMinSdk < mMinSdkVersion)
+ || (!mRotationTargetsDevRelease && currentMinSdk <= mMinSdkVersion)
+ || currentMinSdk <= AndroidSdkVersion.P) {
// this satisfies all we need, stop here
break;
}
@@ -355,26 +434,65 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
"Provided key algorithms not supported on all desired "
+ "Android SDK versions");
}
+
return processedConfigs;
}
- private ApkSigningBlockUtils.SignerConfig createV4SignerConfig()
- throws InvalidKeyException, IllegalStateException {
- List<ApkSigningBlockUtils.SignerConfig> configs =
- createSigningBlockSignerConfigs(
- true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
- if (configs.size() != 1) {
- throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
+ private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+ }
+
+ private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
+ List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
+ // If the signing key has been rotated, the caller has requested to use the rotated
+ // signing key starting from an SDK version where v3.1 is supported, and the minimum
+ // SDK version for the APK is less than the requested rotation minimum, then the APK
+ // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0
+ // scheme with the original signing key. If the APK's minSdkVersion is >= the requested
+ // SDK version for rotation then just use the v3.0 signing block for this.
+ if (!signingLineageHas31Support()) {
+ return null;
+ }
+
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
+ Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator =
+ v3SignerConfigs.iterator();
+ while (v3SignerIterator.hasNext()) {
+ ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+ // All signing configs with a min SDK version that supports v3.1 should be used
+ // in the v3.1 signing block and removed from the v3.0 block.
+ if (signerConfig.minSdkVersion >= mRotationMinSdkVersion) {
+ v31SignerConfigs.add(signerConfig);
+ v3SignerIterator.remove();
+ }
+ }
+ return v31SignerConfigs;
+ }
+
+ private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+ List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ if (v4Configs.size() != 1) {
+ // V4 uses signer config to connect back to v3. Use the same filtering logic.
+ v4Configs = processV3Configs(v4Configs);
}
- return configs.get(0);
+ List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs);
+ return new V4SchemeSigner.SignerConfig(v4Configs, v41configs);
}
private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
throws InvalidKeyException {
- return createSigningBlockSignerConfig(
+ ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
mSourceStampSignerConfig,
- /* apkSigningBlockPaddingSupported= */ true,
+ /* apkSigningBlockPaddingSupported= */ false,
ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ if (mSourceStampSigningCertificateLineage != null) {
+ config.mSigningCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+ config.certificates.get(0));
+ }
+ return config;
}
private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) {
@@ -420,13 +538,19 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
newSignerConfig.signatureAlgorithms =
V2SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
try {
newSignerConfig.signatureAlgorithms =
V3SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
} catch (InvalidKeyException e) {
// It is possible for a signer used for v1/v2 signing to not be allowed for use
@@ -439,8 +563,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
try {
newSignerConfig.signatureAlgorithms =
- V4SchemeSigner.getSuggestedSignatureAlgorithms(publicKey,
- mMinSdkVersion, apkSigningBlockPaddingSupported);
+ V4SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported,
+ signerConfig.getDeterministicDsaSigning());
} catch (InvalidKeyException e) {
// V4 is an optional signing schema, ok to proceed without.
newSignerConfig.signatureAlgorithms = null;
@@ -449,7 +574,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
newSignerConfig.signatureAlgorithms =
Collections.singletonList(
- SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
+ SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
break;
default:
throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
@@ -479,9 +604,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
@Override
@SuppressWarnings("AndroidJdkLibsChecker")
public Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
- V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result();
+ V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
- V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
+ V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
String entryName = entry.getKey();
@@ -520,11 +645,92 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
if (mOtherSignersSignaturesPreserved) {
- // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
- // in this engine.
+ boolean schemeSignatureBlockPreserved = false;
+ mPreservedSignatureBlocks = new ArrayList<>();
+ try {
+ List<Pair<byte[], Integer>> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
+ for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+ // If a V2 signature block is found and the engine is configured to use V2
+ // then save any of the previous signers that are not part of the current
+ // signing request.
+ if (mV2SigningEnabled) {
+ List<Pair<List<X509Certificate>, byte[]>> v2Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ mPreservedV2Signers = new ArrayList<>(v2Signers.size());
+ for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) {
+ if (!isConfiguredWithSigner(v2Signer.getFirst())) {
+ mPreservedV2Signers.add(v2Signer.getSecond());
+ schemeSignatureBlockPreserved = true;
+ }
+ }
+ } else {
+ // else V2 signing is not enabled; save the entire signature block to be
+ // added to the final APK signing block.
+ mPreservedSignatureBlocks.add(signatureBlock);
+ schemeSignatureBlockPreserved = true;
+ }
+ } else if (signatureBlock.getSecond()
+ == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+ // Preserving other signers in the presence of a V3 signature block is only
+ // supported if the engine is configured to resign the APK with the V3
+ // signature scheme, and the V3 signer in the signature block is the same
+ // as the engine is configured to use.
+ if (!mV3SigningEnabled) {
+ throw new IllegalStateException(
+ "Preserving an existing V3 signature is not supported");
+ }
+ List<Pair<List<X509Certificate>, byte[]>> v3Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ if (v3Signers.size() > 1) {
+ throw new IllegalArgumentException(
+ "The provided APK signing block contains " + v3Signers.size()
+ + " V3 signers; the V3 signature scheme only supports"
+ + " one signer");
+ }
+ // If there is only a single V3 signer then ensure it is the signer
+ // configured to sign the APK.
+ if (v3Signers.size() == 1
+ && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) {
+ throw new IllegalStateException(
+ "The V3 signature scheme only supports one signer; a request "
+ + "was made to preserve the existing V3 signature, "
+ + "but the engine is configured to sign with a "
+ + "different signer");
+ }
+ } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains(
+ signatureBlock.getSecond())) {
+ mPreservedSignatureBlocks.add(signatureBlock);
+ }
+ }
+ } catch (ApkFormatException | CertificateException | IOException e) {
+ throw new IllegalArgumentException("Unable to parse the provided signing block", e);
+ }
+ // Signature scheme V3+ only support a single signer; if the engine is configured to
+ // sign with V3+ then ensure no scheme signature blocks have been preserved.
+ if (mV3SigningEnabled && schemeSignatureBlockPreserved) {
+ throw new IllegalStateException(
+ "Signature scheme V3+ only supports a single signer and cannot be "
+ + "appended to the existing signature scheme blocks");
+ }
return;
}
- // TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
+ }
+
+ /**
+ * Returns whether the engine is configured to sign the APK with a signer using the specified
+ * {@code signerCerts}.
+ */
+ private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ if (signerCerts.containsAll(signerConfig.getCertificates())) {
+ return true;
+ }
+ }
+ return false;
}
@Override
@@ -539,7 +745,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
case OUTPUT:
return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
case OUTPUT_BY_ENGINE:
- if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
// We copy the main section of the JAR manifest from input to output. Thus, this
// invalidates v1 signature and we need to see the entry's data.
mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
@@ -607,7 +813,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
// the entry's data is as output by the engine.
invalidateV1Signature();
GetJarEntryDataRequest dataRequest;
- if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
+ if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) {
dataRequest = new GetJarEntryDataRequest(entryName);
mInputJarManifestEntryDataRequest = dataRequest;
} else {
@@ -742,7 +948,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
V1SchemeSigner.generateManifestFile(
mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
byte[] emittedSignatureManifest =
- mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
+ mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME);
if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
// Emitted v1 signature is no longer valid.
try {
@@ -825,7 +1031,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
checkNotClosed();
checkV1SigningDoneIfEnabled();
- if (!mV2SigningEnabled && !mV3SigningEnabled) {
+ if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) {
return null;
}
checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
@@ -841,6 +1047,13 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
+ // If the engine is configured to preserve previous signature blocks and any were found in
+ // the existing APK signing block then add them to the list to be used to generate the
+ // new APK signing block.
+ if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null
+ && !mPreservedSignatureBlocks.isEmpty()) {
+ signingSchemeBlocks.addAll(mPreservedSignatureBlocks);
+ }
// create APK Signature Scheme V2 Signature if requested
if (mV2SigningEnabled) {
@@ -854,32 +1067,84 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
zipCentralDirectory,
eocd,
v2SignerConfigs,
- mV3SigningEnabled);
+ mV3SigningEnabled,
+ mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (mV3SigningEnabled) {
invalidateV3Signature();
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
createV3SignerConfigs(apkSigningBlockPaddingSupported);
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs(
+ v3SignerConfigs);
+ if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) {
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ v31SigningSchemeBlockAndDigests =
+ new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+ v31SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+ zipCentralDirectory, eocd, v3SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (signingLineageHas31Support()) {
+ builder.setRotationMinSdkVersion(mRotationMinSdkVersion);
+ }
v3SigningSchemeBlockAndDigests =
- V3SchemeSigner.generateApkSignatureSchemeV3Block(
- mExecutor,
- beforeCentralDir,
- zipCentralDirectory,
- eocd,
- v3SignerConfigs);
+ builder.build().generateApkSignatureSchemeV3BlockAndDigests();
signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (isEligibleForSourceStamp()) {
ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
createSourceStampSignerConfig();
- Map<ContentDigestAlgorithm, byte[]> digestInfo =
- mV3SigningEnabled
- ? v3SigningSchemeBlockAndDigests.digestInfo
- : v2SigningSchemeBlockAndDigests.digestInfo;
- signingSchemeBlocks.add(
- SourceStampSigner.generateSourceStampBlock(
- sourceStampSignerConfig, digestInfo));
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos =
+ new HashMap<>();
+ if (mV3SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV2SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV1SigningEnabled) {
+ Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>();
+ try {
+ // Jar signing related variables must have been already populated at this point
+ // if V1 signing is enabled since it is happening before computations on the APK
+ // signing block (V2/V3/V4/SourceStamp signing).
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ byte[] jarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ // The digest of the jar manifest does not need to be computed in chunks due to
+ // the small size of the manifest.
+ v1SigningSchemeDigests.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+ } catch (ApkFormatException e) {
+ throw new RuntimeException("Failed to generate manifest file", e);
+ }
+ signatureSchemeDigestInfos.put(
+ VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+ }
+ 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
@@ -907,7 +1172,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
throw new SignatureException("Missing V4 output file.");
}
try {
- ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
} catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
if (ignoreFailures) {
@@ -918,27 +1183,26 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
/** For external use only to generate V4 & tree separately. */
- public byte[] produceV4Signature(
- DataSource dataSource,
- OutputStream sigOutput)
- throws SignatureException {
- if (sigOutput == null) {
- throw new SignatureException("Missing V4 output streams.");
- }
- try {
- ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
- Pair<V4Signature, byte[]> pair =
- V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
- pair.getFirst().writeTo(sigOutput);
- return pair.getSecond();
- } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
- throw new SignatureException("V4 signing failed", e);
- }
+ public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+ throws SignatureException {
+ if (sigOutput == null) {
+ throw new SignatureException("Missing V4 output streams.");
+ }
+ try {
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+ Pair<V4Signature, byte[]> pair =
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+ pair.getFirst().writeTo(sigOutput);
+ return pair.getSecond();
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ throw new SignatureException("V4 signing failed", e);
+ }
}
@Override
public boolean isEligibleForSourceStamp() {
- return mSourceStampSignerConfig != null && (mV2SigningEnabled || mV3SigningEnabled);
+ return mSourceStampSignerConfig != null
+ && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
}
@Override
@@ -1113,17 +1377,6 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
return InputJarEntryInstructions.OutputPolicy.SKIP;
}
- private static byte[] computeSha256DigestBytes(byte[] data) {
- MessageDigest messageDigest;
- try {
- messageDigest = MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("SHA-256 is not found", e);
- }
- messageDigest.update(data);
- return messageDigest.digest();
- }
-
private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
private final List<JarEntry> mAdditionalJarEntries;
private volatile boolean mDone;
@@ -1379,12 +1632,15 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
private SignerConfig(
- String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ String name, PrivateKey privateKey, List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/** Returns the name of this signer. */
@@ -1405,11 +1661,19 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
return mCertificates;
}
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
/** 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;
/**
* Constructs a new {@code Builder}.
@@ -1421,12 +1685,29 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
* the first certificate must correspond to the {@code privateKey}.
*/
public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
+
+ /**
+ * Constructs a new {@code Builder}.
+ *
+ * @param name signer's name. The name is reflected in the name of files comprising the
+ * JAR signature of the APK.
+ * @param privateKey signing key
+ * @param certificates list of one or more X.509 certificates. The subject public key of
+ * the first certificate must correspond to the {@code privateKey}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic signing algorithm variant (RFC6979) should be used.
+ */
+ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/**
@@ -1434,7 +1715,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(mName, mPrivateKey, mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates,
+ mDeterministicDsaSigning);
}
}
}
@@ -1443,11 +1725,16 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
public static class Builder {
private List<SignerConfig> mSignerConfigs;
private SignerConfig mStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private boolean mSourceStampTimestampEnabled = true;
private final int mMinSdkVersion;
private boolean mV1SigningEnabled = true;
private boolean mV2SigningEnabled = true;
private boolean mV3SigningEnabled = true;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
+ private boolean mVerityEnabled = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy = "1.0 (Android)";
@@ -1533,10 +1820,15 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
return new DefaultApkSignerEngine(
mSignerConfigs,
mStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
+ mSourceStampTimestampEnabled,
mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
+ mVerityEnabled,
mDebuggableApkPermitted,
mOtherSignersSignaturesPreserved,
mCreatedBy,
@@ -1550,6 +1842,25 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
/**
+ * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+ * signing certificate rotation for certificates previously used to sign source stamps.
+ */
+ public Builder setSourceStampSigningCertificateLineage(
+ SigningCertificateLineage sourceStampSigningCertificateLineage) {
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ return this;
+ }
+
+ /**
+ * 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.
@@ -1587,6 +1898,18 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
/**
+ * Sets whether the APK should be signed using the verity signature algorithm in the v2 and
+ * v3 signature blocks.
+ *
+ * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and
+ * v3 signature schemes.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ mVerityEnabled = enabled;
+ return this;
+ }
+
+ /**
* Sets whether the APK should be signed even if it is marked as debuggable ({@code
* android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
* compatibility reasons, the default value of this setting is {@code true}.
@@ -1633,5 +1956,49 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
return this;
}
+
+ /**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop.
+ *
+ * <p>By default, if a signing lineage is specified with {@link
+ * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+ * V3.1 will be used to only apply the rotation on devices running Android T+.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
}
}
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 54340d7..43b7f5e 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -23,6 +23,7 @@ import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode;
@@ -123,6 +124,11 @@ public class SigningCertificateLineage {
return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
}
+ public static SigningCertificateLineage readFromBytes(byte[] lineageBytes)
+ throws IOException {
+ return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes)));
+ }
+
public static SigningCertificateLineage readFromFile(File file)
throws IOException {
if (file == null) {
@@ -185,41 +191,62 @@ public class SigningCertificateLineage {
*/
public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
throws IOException, ApkFormatException {
- SignatureInfo signatureInfo;
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(e.getMessage());
+ }
+
+ List<SignatureInfo> signatureInfoList = new ArrayList<>();
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if there's only a V3 signature block.
+ }
try {
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
- signatureInfo =
+ signatureInfoList.add(
ApkSigningBlockUtils.findSignature(apk, zipSections,
- V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
- } catch (ZipFormatException e) {
- throw new ApkFormatException(e.getMessage());
- } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+ }
+ catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if the provided APK is not signed with the v3 signature scheme
+ }
+ if (signatureInfoList.isEmpty()) {
throw new IllegalArgumentException(
"The provided APK does not contain a valid V3 signature block.");
}
- // FORMAT:
- // * length-prefixed sequence of length-prefixed signers:
- // * length-prefixed signed data
- // * minSDK
- // * maxSDK
- // * length-prefixed sequence of length-prefixed signatures
- // * length-prefixed public key
- ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
List<SigningCertificateLineage> lineages = new ArrayList<>(1);
- while (signers.hasRemaining()) {
- ByteBuffer signer = getLengthPrefixedSlice(signers);
- ByteBuffer signedData = getLengthPrefixedSlice(signer);
- try {
- SigningCertificateLineage lineage = readFromSignedData(signedData);
- lineages.add(lineage);
- } catch (IllegalArgumentException ignored) {
- // The current signer block does not contain a valid lineage, but it is possible
- // another block will.
+ for (SignatureInfo signatureInfo : signatureInfoList) {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signers:
+ // * length-prefixed signed data
+ // * minSDK
+ // * maxSDK
+ // * length-prefixed sequence of length-prefixed signatures
+ // * length-prefixed public key
+ ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ while (signers.hasRemaining()) {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ try {
+ SigningCertificateLineage lineage = readFromSignedData(signedData);
+ lineages.add(lineage);
+ } catch (IllegalArgumentException ignored) {
+ // The current signer block does not contain a valid lineage, but it is possible
+ // another block will.
+ }
}
}
+
SigningCertificateLineage result;
if (lineages.isEmpty()) {
throw new IllegalArgumentException(
@@ -263,7 +290,7 @@ public class SigningCertificateLineage {
while (additionalAttributes.hasRemaining()) {
ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
int id = attribute.getInt();
- if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
+ if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
byte[] value = ByteBufferUtils.toByteArray(attribute);
SigningCertificateLineage lineage = readFromV3AttributeValue(value);
lineages.add(lineage);
@@ -282,6 +309,10 @@ public class SigningCertificateLineage {
return result;
}
+ public byte[] getBytes() {
+ return write().array();
+ }
+
public void writeToFile(File file) throws IOException {
if (file == null) {
throw new NullPointerException("file == null");
@@ -401,7 +432,8 @@ public class SigningCertificateLineage {
// TODO switch to one signature algorithm selection, or add support for multiple algorithms
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, false /* padding support */);
+ publicKey, mMinSdkVersion, false /* verityEnabled */,
+ false /* deterministicDsaSigning */);
return algorithms.get(0);
}
@@ -491,20 +523,8 @@ public class SigningCertificateLineage {
return result;
}
- public byte[] generateV3SignerAttribute() {
- // FORMAT (little endian):
- // * length-prefixed bytes: attribute pair
- // * uint32: ID
- // * bytes: value - encoded V3 SigningCertificateLineage
- byte[] encodedLineage =
- V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
- int payloadSize = 4 + 4 + encodedLineage.length;
- ByteBuffer result = ByteBuffer.allocate(payloadSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- result.putInt(4 + encodedLineage.length);
- result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID);
- result.put(encodedLineage);
- return result.array();
+ public byte[] encodeSigningCertificateLineage() {
+ return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
}
public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs(
diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java
new file mode 100644
index 0000000..98da68e
--- /dev/null
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,911 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+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_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK source stamp verifier intended only to verify the validity of the stamp signature.
+ *
+ * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
+ * when obtaining the digests for verification. This verifier should only be used in cases where
+ * another mechanism has already been used to verify the APK signatures.
+ */
+public class SourceStampVerifier {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private SourceStampVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ int minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ * <p>The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
+ * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
+ * can be obtained as follows:
+ * <ul>
+ * <li>Obtain the generic errors via {@link Result#getErrors()}
+ * <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ * <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ * <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
+ * for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
+ * </ul>
+ */
+ public SourceStampVerifier.Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ Result result = new Result();
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ return result;
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
+ String expectedCertDigest) {
+ Result result = new Result();
+ try {
+ ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List<CentralDirectoryRecord> cdRecords =
+ ZipUtils.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ stampSigningBlockFound = true;
+ } catch (SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ result.addVerificationError(stampSigningBlockFound
+ ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
+ : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ return result;
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
+ sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ result.addVerificationError(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ actualCertDigest, expectedCertDigest);
+ return result;
+ }
+ }
+
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
+ }
+ }
+
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
+ signatureSchemeApkContentDigests.isEmpty())) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
+ }
+ }
+
+ if (mMinSdkVersion < AndroidSdkVersion.N
+ || signatureSchemeApkContentDigests.isEmpty()) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ apkContentDigests);
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+ }
+ return result;
+ }
+
+ /**
+ * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
+ * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ public static void parseSigners(
+ ByteBuffer apkSignatureSchemeBlock,
+ int apkSigSchemeVersion,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ Result result) {
+ boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+ // Both the V2 and V3 signature blocks contain the following:
+ // * length-prefixed sequence of length-prefixed signers
+ ByteBuffer signers;
+ try {
+ signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
+ } catch (ApkFormatException e) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+ return;
+ }
+ if (!signers.hasRemaining()) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+ : ApkVerificationIssue.V3_SIG_NO_SIGNERS);
+ return;
+ }
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ while (signers.hasRemaining()) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ if (isV2Block) {
+ result.addV2Signer(signerInfo);
+ } else {
+ result.addV3Signer(signerInfo);
+ }
+ try {
+ ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
+ parseSigner(
+ signer,
+ apkSigSchemeVersion,
+ certFactory,
+ apkContentDigests,
+ signerInfo);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addVerificationWarning(
+ isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses the provided signer block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+ * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+ * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+ * integrity of the APK.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ private static void parseSigner(
+ ByteBuffer signerBlock,
+ int apkSigSchemeVersion,
+ CertificateFactory certFactory,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ Result.SignerInfo signerInfo)
+ throws ApkFormatException {
+ boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+ // Both the V2 and V3 signer blocks contain the following:
+ // * length-prefixed signed data
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: digest of contents
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
+ ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+ ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+
+ // Parse the digests block
+ while (digests.hasRemaining()) {
+ try {
+ ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
+ int sigAlgorithmId = digest.getInt();
+ byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
+ : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
+ return;
+ }
+ }
+
+ // Parse the certificates block
+ if (certificates.hasRemaining()) {
+ byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
+ : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
+ return;
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+ signerInfo.setSigningCertificate(certificate);
+ }
+
+ if (signerInfo.getSigningCertificate() == null) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+ : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+ return;
+ }
+ }
+
+ /**
+ * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
+ * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
+ * returned.
+ *
+ * <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
+ * will be updated to include a warning, but the source stamp verification can still proceed.
+ */
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ZipSections zipSections,
+ Result result)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ String cdRecordName = cdRecord.getName();
+ if (cdRecordName == null) {
+ continue;
+ }
+ if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
+ manifestCdRecord = cdRecord;
+ continue;
+ }
+ if (cdRecordName.startsWith("META-INF/")
+ && (cdRecordName.endsWith(".RSA")
+ || cdRecordName.endsWith(".DSA")
+ || cdRecordName.endsWith(".EC"))) {
+ signatureBlockRecords.add(cdRecord);
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ if (signatureBlockRecords.isEmpty()) {
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+ } else {
+ for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
+ signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
+ for (Certificate certificate : certFactory.generateCertificates(
+ new ByteArrayInputStream(signatureBlockBytes))) {
+ // If multiple certificates are found within the signature block only the
+ // first is used as the signer of this block.
+ if (certificate instanceof X509Certificate) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ signerInfo.setSigningCertificate((X509Certificate) certificate);
+ result.addV1Signer(signerInfo);
+ break;
+ }
+ }
+ } catch (CertificateException e) {
+ // Log a warning for the parsing exception but still proceed with the stamp
+ // verification.
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ signatureBlockRecord.getName(), e);
+ break;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ /**
+ * Result of verifying the APK's source stamp signature; this signature can only be considered
+ * verified if {@link #isVerified()} returns true.
+ */
+ public static class Result {
+ private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
+ private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
+ private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
+ mV2SchemeSigners, mV3SchemeSigners);
+ private SourceStampInfo mSourceStampInfo;
+
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+ private boolean mVerified;
+
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
+ }
+
+ private void addV1Signer(SignerInfo signerInfo) {
+ mV1SchemeSigners.add(signerInfo);
+ }
+
+ private void addV2Signer(SignerInfo signerInfo) {
+ mV2SchemeSigners.add(signerInfo);
+ }
+
+ private void addV3Signer(SignerInfo signerInfo) {
+ mV3SchemeSigners.add(signerInfo);
+ }
+
+ /**
+ * Returns {@code true} if the APK's source stamp signature
+ */
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ private void mergeFrom(ApkSigResult source) {
+ switch (source.signatureSchemeVersion) {
+ case Constants.VERSION_SOURCE_STAMP:
+ mVerified = source.verified;
+ if (!source.mSigners.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown ApkSigResult Signing Block Scheme Id "
+ + source.signatureSchemeVersion);
+ }
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV1SchemeSigners() {
+ return mV1SchemeSigners;
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV3SchemeSigners() {
+ return mV3SchemeSigners;
+ }
+
+ /**
+ * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
+ * APK, or null if the source stamp signature verification failed before the stamp signature
+ * block could be fully parsed.
+ */
+ public SourceStampInfo getSourceStampInfo() {
+ return mSourceStampInfo;
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying the APK.
+ *
+ * <p>Any error prevents the APK from being considered verified.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ if (mSourceStampInfo.containsErrors()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the errors encountered while verifying the APK's source stamp.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered while verifying the APK's source stamp.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns all errors for this result, including any errors from signature scheme signers
+ * and the source stamp.
+ */
+ public List<ApkVerificationIssue> getAllErrors() {
+ List<ApkVerificationIssue> errors = new ArrayList<>();
+ errors.addAll(mErrors);
+
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ errors.addAll(signer.getErrors());
+ }
+ }
+ if (mSourceStampInfo != null) {
+ errors.addAll(mSourceStampInfo.getErrors());
+ }
+ return errors;
+ }
+
+ /**
+ * Returns all warnings for this result, including any warnings from signature scheme
+ * signers and the source stamp.
+ */
+ public List<ApkVerificationIssue> getAllWarnings() {
+ List<ApkVerificationIssue> warnings = new ArrayList<>();
+ warnings.addAll(mWarnings);
+
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ warnings.addAll(signer.getWarnings());
+ }
+ }
+ if (mSourceStampInfo != null) {
+ warnings.addAll(mSourceStampInfo.getWarnings());
+ }
+ return warnings;
+ }
+
+ /**
+ * Contains information about an APK's signer and any errors encountered while parsing the
+ * corresponding signature block.
+ */
+ public static class SignerInfo {
+ private X509Certificate mSigningCertificate;
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+ void setSigningCertificate(X509Certificate signingCertificate) {
+ mSigningCertificate = signingCertificate;
+ }
+
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
+ }
+
+ /**
+ * Returns the current signing certificate used by this signer.
+ */
+ public X509Certificate getSigningCertificate() {
+ return mSigningCertificate;
+ }
+
+ /**
+ * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
+ * encountered during processing of this signer's signature block.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
+ * encountered during processing of this signer's signature block.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns {@code true} if any errors were encountered while parsing this signer's
+ * signature block.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+ }
+
+ /**
+ * Contains information about an APK's source stamp and any errors encountered while
+ * parsing the stamp signature block.
+ */
+ public static class SourceStampInfo {
+ private final List<X509Certificate> mCertificates;
+ private final List<X509Certificate> mCertificateLineage;
+
+ 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;
+
+ /*
+ * Since this utility is intended just to verify the source stamp, and the source stamp
+ * currently only logs warnings to prevent failing the APK signature verification, treat
+ * all warnings as errors. If the stamp verification is updated to log errors this
+ * should be set to false to ensure only errors trigger a failure verifying the source
+ * stamp.
+ */
+ private static final boolean mWarningsAsErrors = true;
+
+ private SourceStampInfo(ApkSignerInfo result) {
+ mCertificates = result.certs;
+ mCertificateLineage = result.certificateLineage;
+ mErrors.addAll(result.getErrors());
+ mWarnings.addAll(result.getWarnings());
+ mInfoMessages.addAll(result.getInfoMessages());
+ mTimestamp = result.timestamp;
+ }
+
+ /**
+ * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * <p>This certificate contains the SourceStamp's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificates.isEmpty() ? null : mCertificates.get(0);
+ }
+
+ /**
+ * Returns a {@code List} of {@link X509Certificate} instances representing the source
+ * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
+ * if the stamp's signing certificate has not been rotated.
+ */
+ public List<X509Certificate> getCertificatesInLineage() {
+ return mCertificateLineage;
+ }
+
+ /**
+ * Returns whether any errors were encountered during the source stamp verification.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
+ }
+
+ /**
+ * 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.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ if (!mWarningsAsErrors) {
+ return mErrors;
+ }
+ List<ApkVerificationIssue> result = new ArrayList<>();
+ result.addAll(mErrors);
+ result.addAll(mWarnings);
+ return result;
+ }
+
+ /**
+ * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
+ * were encountered during source stamp verification.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * 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.
+ */
+ public long getTimestampEpochSeconds() {
+ return mTimestamp;
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link SourceStampVerifier} instances.
+ *
+ * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
+ * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
+ * queried to determine the APK's minimum supported level, so the caller should specify a lower
+ * bound with {@link #setMinCheckedPlatformVersion(int)}.
+ */
+ public static class Builder {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private int mMinSdkVersion = 1;
+ private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+ /**
+ * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+ * apk}.
+ */
+ public Builder(File apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkFile = apk;
+ mApkDataSource = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+ * apk}.
+ */
+ public Builder(DataSource apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkDataSource = apk;
+ mApkFile = null;
+ }
+
+ /**
+ * Sets the oldest Android platform version for which the APK's source stamp is verified.
+ *
+ * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+ * on all Android platforms starting from the platform version with the provided {@code
+ * minSdkVersion}. The upper end of the platform versions range can be modified via
+ * {@link #setMaxCheckedPlatformVersion(int)}.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ */
+ public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets the newest Android platform version for which the APK's source stamp is verified.
+ *
+ * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+ * on all platform versions up to and including the proviced {@code maxSdkVersion}. The
+ * lower end of the platform versions range can be modified via {@link
+ * #setMinCheckedPlatformVersion(int)}.
+ *
+ * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+ mMaxSdkVersion = maxSdkVersion;
+ return this;
+ }
+
+ /**
+ * Returns a {@link SourceStampVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public SourceStampVerifier build() {
+ return new SourceStampVerifier(
+ mApkFile,
+ mApkDataSource,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
+}
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 870971f..156ea17 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -17,6 +17,7 @@
package com.android.apksig.apk;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -24,6 +25,7 @@ import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -42,7 +44,8 @@ public abstract class ApkUtils {
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
- public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
+ SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
private ApkUtils() {}
@@ -54,101 +57,27 @@ public abstract class ApkUtils {
*/
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
- Pair<ByteBuffer, Long> eocdAndOffsetInFile =
- ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
- if (eocdAndOffsetInFile == null) {
- throw new ZipFormatException("ZIP End of Central Directory record not found");
- }
-
- ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
- long eocdOffset = eocdAndOffsetInFile.getSecond();
- eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
- long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
- if (cdStartOffset > eocdOffset) {
- throw new ZipFormatException(
- "ZIP Central Directory start offset out of range: " + cdStartOffset
- + ". ZIP End of Central Directory offset: " + eocdOffset);
- }
-
- long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
- long cdEndOffset = cdStartOffset + cdSizeBytes;
- if (cdEndOffset > eocdOffset) {
- throw new ZipFormatException(
- "ZIP Central Directory overlaps with End of Central Directory"
- + ". CD end: " + cdEndOffset
- + ", EoCD start: " + eocdOffset);
- }
-
- int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
-
+ com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
return new ZipSections(
- cdStartOffset,
- cdSizeBytes,
- cdRecordCount,
- eocdOffset,
- eocdBuf);
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipCentralDirectorySizeBytes(),
+ zipSections.getZipCentralDirectoryRecordCount(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectory());
}
/**
* Information about the ZIP sections of an APK.
*/
- public static class ZipSections {
- private final long mCentralDirectoryOffset;
- private final long mCentralDirectorySizeBytes;
- private final int mCentralDirectoryRecordCount;
- private final long mEocdOffset;
- private final ByteBuffer mEocd;
-
+ public static class ZipSections extends com.android.apksig.zip.ZipSections {
public ZipSections(
long centralDirectoryOffset,
long centralDirectorySizeBytes,
int centralDirectoryRecordCount,
long eocdOffset,
ByteBuffer eocd) {
- mCentralDirectoryOffset = centralDirectoryOffset;
- mCentralDirectorySizeBytes = centralDirectorySizeBytes;
- mCentralDirectoryRecordCount = centralDirectoryRecordCount;
- mEocdOffset = eocdOffset;
- mEocd = eocd;
- }
-
- /**
- * Returns the start offset of the ZIP Central Directory. This value is taken from the
- * ZIP End of Central Directory record.
- */
- public long getZipCentralDirectoryOffset() {
- return mCentralDirectoryOffset;
- }
-
- /**
- * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
- * ZIP End of Central Directory record.
- */
- public long getZipCentralDirectorySizeBytes() {
- return mCentralDirectorySizeBytes;
- }
-
- /**
- * Returns the number of records in the ZIP Central Directory. This value is taken from the
- * ZIP End of Central Directory record.
- */
- public int getZipCentralDirectoryRecordCount() {
- return mCentralDirectoryRecordCount;
- }
-
- /**
- * Returns the start offset of the ZIP End of Central Directory record. The record extends
- * until the very end of the APK.
- */
- public long getZipEndOfCentralDirectoryOffset() {
- return mEocdOffset;
- }
-
- /**
- * Returns the contents of the ZIP End of Central Directory.
- */
- public ByteBuffer getZipEndOfCentralDirectory() {
- return mEocd;
+ super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
+ eocdOffset, eocd);
}
}
@@ -167,10 +96,37 @@ public abstract class ApkUtils {
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
}
- // See https://source.android.com/security/apksigning/v2.html
- private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
- private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
- private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+ /**
+ * Updates the length of EOCD comment.
+ *
+ * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+ */
+ public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
+ ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+ eocd.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.updateZipEocdCommentLen(eocd);
+ }
+
+ /**
+ * Returns the APK Signing Block of the provided {@code apk}.
+ *
+ * @throws ApkFormatException if the APK is not a valid ZIP archive
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk)
+ throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
+ ApkUtils.ZipSections inputZipSections;
+ try {
+ inputZipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+ return findApkSigningBlock(apk, inputZipSections);
+ }
/**
* Returns the APK Signing Block of the provided APK.
@@ -178,74 +134,20 @@ public abstract class ApkUtils {
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
- * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
- // FORMAT (see https://source.android.com/security/apksigning/v2.html):
- // OFFSET DATA TYPE DESCRIPTION
- // * @+0 bytes uint64: size in bytes (excluding this field)
- // * @+8 bytes payload
- // * @-24 bytes uint64: size in bytes (same as the one above)
- // * @-16 bytes uint128: magic
-
- long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
- long centralDirEndOffset =
- centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
- long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
- if (centralDirEndOffset != eocdStartOffset) {
- throw new ApkSigningBlockNotFoundException(
- "ZIP Central Directory is not immediately followed by End of Central Directory"
- + ". CD end: " + centralDirEndOffset
- + ", EoCD start: " + eocdStartOffset);
- }
-
- if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
- throw new ApkSigningBlockNotFoundException(
- "APK too small for APK Signing Block. ZIP Central Directory offset: "
- + centralDirStartOffset);
- }
- // Read the magic and offset in file from the footer section of the block:
- // * uint64: size of block
- // * 16 bytes: magic
- ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
- footer.order(ByteOrder.LITTLE_ENDIAN);
- if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
- || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
- throw new ApkSigningBlockNotFoundException(
- "No APK Signing Block before ZIP Central Directory");
- }
- // Read and compare size fields
- long apkSigBlockSizeInFooter = footer.getLong(0);
- if ((apkSigBlockSizeInFooter < footer.capacity())
- || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
- }
- int totalSize = (int) (apkSigBlockSizeInFooter + 8);
- long apkSigBlockOffset = centralDirStartOffset - totalSize;
- if (apkSigBlockOffset < 0) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block offset out of range: " + apkSigBlockOffset);
- }
- ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
- apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
- long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
- if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block sizes in header and footer do not match: "
- + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
- }
- return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+ ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
+ zipSections);
+ return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
- public static class ApkSigningBlock {
- private final long mStartOffsetInApk;
- private final DataSource mContents;
-
+ public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
/**
* Constructs a new {@code ApkSigningBlock}.
*
@@ -254,23 +156,7 @@ public abstract class ApkUtils {
* @param contents contents of the APK Signing Block
*/
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
- mStartOffsetInApk = startOffsetInApk;
- mContents = contents;
- }
-
- /**
- * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
- */
- public long getStartOffset() {
- return mStartOffsetInApk;
- }
-
- /**
- * Returns the data source which provides the full contents of the APK Signing Block,
- * including its footer.
- */
- public DataSource getContents() {
- return mContents;
+ super(startOffsetInApk, contents);
}
}
@@ -322,6 +208,30 @@ public abstract class ApkUtils {
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
/**
+ * Android resource ID of the {@code android:targetSandboxVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+
+ /**
+ * Android resource ID of the {@code android:targetSdkVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
+ private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
+
+ /**
+ * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
+ */
+ private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
+ private static final String MANIFEST_ELEMENT_TAG = "manifest";
+
+ /**
+ * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
+ */
+ private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
+
+ /**
* Returns the lowest Android platform version (API Level) supported by an APK with the
* provided {@code AndroidManifest.xml}.
*
@@ -604,4 +514,157 @@ public abstract class ApkUtils {
e);
}
}
+
+ /**
+ * Returns the security sandbox version targeted by an APK with the provided
+ * {@code AndroidManifest.xml}.
+ *
+ * <p>If the security sandbox version is not specified in the manifest a default value of 1 is
+ * returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ */
+ public static int getTargetSandboxVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) {
+ try {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // An ApkFormatException indicates the target sandbox is not specified in the manifest;
+ // return a default value of 1.
+ return 1;
+ }
+ }
+
+ /**
+ * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
+ *
+ * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
+ * value is specified then a value of 1 is returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ */
+ public static int getTargetSdkVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) {
+ // If the targetSdkVersion is not specified then the platform will use the value of the
+ // minSdkVersion; if neither is specified then the platform will use a value of 1.
+ int minSdkVersion = 1;
+ try {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
+ // element is not specified at all.
+ }
+ androidManifestContents.rewind();
+ try {
+ minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
+ } catch (ApkFormatException e) {
+ // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
+ // the uses-sdk element is not specified at all.
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
+ *
+ * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
+ * integer an ApkFormatException is thrown.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ * @throws ApkFormatException if an error occurred while determining the versionCode, or if the
+ * versionCode attribute value is not available.
+ */
+ public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
+ throws ApkFormatException {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
+ }
+
+ /**
+ * Returns the versionCode and versionCodeMajor of the APK according to its {@code
+ * AndroidManifest.xml} combined together as a single long value.
+ *
+ * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
+ * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ * @throws ApkFormatException if an error occurred while determining the version, or if the
+ * versionCode attribute value is not available.
+ */
+ public static long getLongVersionCodeFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws ApkFormatException {
+ // If the versionCode is not found then allow the ApkFormatException to be thrown to notify
+ // the caller that the versionCode is not available.
+ int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
+ long versionCodeMajor = 0;
+ try {
+ androidManifestContents.rewind();
+ versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // This is expected if the versionCodeMajor has not been defined for the APK; in this
+ // case the return value is just the versionCode.
+ }
+ return (versionCodeMajor << 32) | versionCode;
+ }
+
+ /**
+ * Returns the integer value of the requested {@code attributeId} in the specified {@code
+ * elementName} from the provided {@code androidManifestContents} in binary Android resource
+ * format.
+ *
+ * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
+ * if the requested attribute is not found.
+ */
+ private static int getAttributeValueFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents, String elementName, int attributeId)
+ throws ApkFormatException {
+ if (elementName == null) {
+ throw new NullPointerException("elementName cannot be null");
+ }
+ try {
+ AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+ int eventType = parser.getEventType();
+ while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+ if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+ && (elementName.equals(parser.getName()))) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if (parser.getAttributeNameResourceId(i) == attributeId) {
+ int valueType = parser.getAttributeValueType(i);
+ switch (valueType) {
+ case AndroidBinXmlParser.VALUE_TYPE_INT:
+ case AndroidBinXmlParser.VALUE_TYPE_STRING:
+ return parser.getAttributeIntValue(i);
+ default:
+ throw new ApkFormatException(
+ "Unsupported value type, " + valueType
+ + ", for attribute " + String.format("0x%08X",
+ attributeId) + " under element " + elementName);
+
+ }
+ }
+ }
+ }
+ eventType = parser.next();
+ }
+ throw new ApkFormatException(
+ "Failed to determine APK's " + elementName + " attribute "
+ + String.format("0x%08X", attributeId) + " value");
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new ApkFormatException(
+ "Unable to determine value for attribute " + String.format("0x%08X",
+ attributeId) + " under element " + elementName
+ + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+ }
+ }
+
+ public static byte[] computeSha256DigestBytes(byte[] data) {
+ return ApkUtilsLite.computeSha256DigestBytes(data);
+ }
}
diff --git a/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
new file mode 100644
index 0000000..13f2301
--- /dev/null
+++ b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Lightweight version of the ApkUtils for clients that only require a subset of the utility
+ * functionality.
+ */
+public class ApkUtilsLite {
+ private ApkUtilsLite() {}
+
+ /**
+ * Finds the main ZIP sections of the provided APK.
+ *
+ * @throws IOException if an I/O error occurred while reading the APK
+ * @throws ZipFormatException if the APK is malformed
+ */
+ public static ZipSections findZipSections(DataSource apk)
+ throws IOException, ZipFormatException {
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+ ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+ if (eocdAndOffsetInFile == null) {
+ throw new ZipFormatException("ZIP End of Central Directory record not found");
+ }
+
+ ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+ long eocdOffset = eocdAndOffsetInFile.getSecond();
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
+ if (cdStartOffset > eocdOffset) {
+ throw new ZipFormatException(
+ "ZIP Central Directory start offset out of range: " + cdStartOffset
+ + ". ZIP End of Central Directory offset: " + eocdOffset);
+ }
+
+ long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
+ long cdEndOffset = cdStartOffset + cdSizeBytes;
+ if (cdEndOffset > eocdOffset) {
+ throw new ZipFormatException(
+ "ZIP Central Directory overlaps with End of Central Directory"
+ + ". CD end: " + cdEndOffset
+ + ", EoCD start: " + eocdOffset);
+ }
+
+ int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
+
+ return new ZipSections(
+ cdStartOffset,
+ cdSizeBytes,
+ cdRecordCount,
+ eocdOffset,
+ eocdBuf);
+ }
+
+ // See https://source.android.com/security/apksigning/v2.html
+ private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+ private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+ private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+ /**
+ * Returns the APK Signing Block of the provided APK.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+ throws IOException, ApkSigningBlockNotFoundException {
+ // FORMAT (see https://source.android.com/security/apksigning/v2.html):
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes payload
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+
+ long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+ long centralDirEndOffset =
+ centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+ long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+ if (centralDirEndOffset != eocdStartOffset) {
+ throw new ApkSigningBlockNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central Directory"
+ + ". CD end: " + centralDirEndOffset
+ + ", EoCD start: " + eocdStartOffset);
+ }
+
+ if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ + centralDirStartOffset);
+ }
+ // Read the magic and offset in file from the footer section of the block:
+ // * uint64: size of block
+ // * 16 bytes: magic
+ ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
+ footer.order(ByteOrder.LITTLE_ENDIAN);
+ if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+ || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+ throw new ApkSigningBlockNotFoundException(
+ "No APK Signing Block before ZIP Central Directory");
+ }
+ // Read and compare size fields
+ long apkSigBlockSizeInFooter = footer.getLong(0);
+ if ((apkSigBlockSizeInFooter < footer.capacity())
+ || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+ }
+ int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+ long apkSigBlockOffset = centralDirStartOffset - totalSize;
+ if (apkSigBlockOffset < 0) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block offset out of range: " + apkSigBlockOffset);
+ }
+ ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
+ apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+ long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+ if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block sizes in header and footer do not match: "
+ + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+ }
+ return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+ }
+
+ /**
+ * Information about the location of the APK Signing Block inside an APK.
+ */
+ public static class ApkSigningBlock {
+ private final long mStartOffsetInApk;
+ private final DataSource mContents;
+
+ /**
+ * Constructs a new {@code ApkSigningBlock}.
+ *
+ * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+ * Signing Block inside the APK file
+ * @param contents contents of the APK Signing Block
+ */
+ public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+ mStartOffsetInApk = startOffsetInApk;
+ mContents = contents;
+ }
+
+ /**
+ * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
+ */
+ public long getStartOffset() {
+ return mStartOffsetInApk;
+ }
+
+ /**
+ * Returns the data source which provides the full contents of the APK Signing Block,
+ * including its footer.
+ */
+ public DataSource getContents() {
+ return mContents;
+ }
+ }
+
+ public static byte[] computeSha256DigestBytes(byte[] data) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is not found", e);
+ }
+ messageDigest.update(data);
+ return messageDigest.digest();
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
new file mode 100644
index 0000000..6151351
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signature verification result.
+ */
+public class ApkSigResult {
+ public final int signatureSchemeVersion;
+
+ /** Whether the APK's Signature Scheme signature verifies. */
+ public boolean verified;
+
+ public final List<ApkSignerInfo> mSigners = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+ public ApkSigResult(int signatureSchemeVersion) {
+ this.signatureSchemeVersion = signatureSchemeVersion;
+ }
+
+ /**
+ * Returns {@code true} if this result encountered errors during verification.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!mSigners.isEmpty()) {
+ for (ApkSignerInfo signer : mSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if this result encountered warnings during verification.
+ */
+ public boolean containsWarnings() {
+ if (!mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!mSigners.isEmpty()) {
+ for (ApkSignerInfo signer : mSigners) {
+ if (signer.containsWarnings()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addError(int issueId, Object... parameters) {
+ mErrors.add(new ApkVerificationIssue(issueId, parameters));
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addWarning(int issueId, Object... parameters) {
+ mWarnings.add(new ApkVerificationIssue(issueId, parameters));
+ }
+
+ /**
+ * Returns the errors encountered during verification.
+ */
+ public List<? extends ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered during verification.
+ */
+ public List<? extends ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
new file mode 100644
index 0000000..3e79341
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signer.
+ */
+public class ApkSignerInfo {
+ public int index;
+ public long timestamp;
+ 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<>();
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addError(int issueId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(issueId, params));
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addWarning(int issueId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(issueId, params));
+ }
+
+ /**
+ * 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() {
+ return !mErrors.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any warnings were encountered during verification for this signer.
+ */
+ public boolean containsWarnings() {
+ return !mWarnings.isEmpty();
+ }
+
+ /**
+ * 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() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered during verification for this signer.
+ */
+ 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 bff38ac..44dcc79 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,7 @@
package com.android.apksig.internal.apk;
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
@@ -23,7 +24,6 @@ import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUN
import com.android.apksig.ApkVerifier;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.asn1.Asn1BerParser;
import com.android.apksig.internal.asn1.Asn1DecodingException;
@@ -40,8 +40,10 @@ import com.android.apksig.internal.pkcs7.SignerIdentifier;
import com.android.apksig.internal.pkcs7.SignerInfo;
import com.android.apksig.internal.util.ByteBufferDataSource;
import com.android.apksig.internal.util.ChainedDataSource;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.util.VerityTreeBuilder;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
import com.android.apksig.internal.zip.ZipUtils;
@@ -53,7 +55,6 @@ import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.math.BigInteger;
-import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.DigestException;
@@ -67,6 +68,7 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
@@ -86,15 +88,14 @@ import javax.security.auth.x500.X500Principal;
public class ApkSigningBlockUtils {
- private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
- public static final byte[] APK_SIGNING_BLOCK_MAGIC =
+ private static final byte[] APK_SIGNING_BLOCK_MAGIC =
new byte[] {
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
};
- private static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
+ public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
{CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
@@ -103,6 +104,7 @@ public class ApkSigningBlockUtils {
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
/**
@@ -110,58 +112,10 @@ public class ApkSigningBlockUtils {
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
*/
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
- ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
- ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
- return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+ return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2);
}
/**
- * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
- * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
- */
- private static int compareContentDigestAlgorithm(
- ContentDigestAlgorithm alg1,
- ContentDigestAlgorithm alg2) {
- switch (alg1) {
- case CHUNKED_SHA256:
- switch (alg2) {
- case CHUNKED_SHA256:
- return 0;
- case CHUNKED_SHA512:
- case VERITY_CHUNKED_SHA256:
- return -1;
- default:
- throw new IllegalArgumentException("Unknown alg2: " + alg2);
- }
- case CHUNKED_SHA512:
- switch (alg2) {
- case CHUNKED_SHA256:
- case VERITY_CHUNKED_SHA256:
- return 1;
- case CHUNKED_SHA512:
- return 0;
- default:
- throw new IllegalArgumentException("Unknown alg2: " + alg2);
- }
- case VERITY_CHUNKED_SHA256:
- switch (alg2) {
- case CHUNKED_SHA256:
- return 1;
- case VERITY_CHUNKED_SHA256:
- return 0;
- case CHUNKED_SHA512:
- return -1;
- default:
- throw new IllegalArgumentException("Unknown alg2: " + alg2);
- }
- default:
- throw new IllegalArgumentException("Unknown alg1: " + alg1);
- }
- }
-
-
-
- /**
* Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
* APK and comparing them against the digests listed in APK Signing Block. The expected digests
* are taken from {@code SignerInfos} of the provided {@code result}.
@@ -279,155 +233,27 @@ public class ApkSigningBlockUtils {
ByteBuffer apkSigningBlock,
int blockId,
Result result) throws SignatureNotFoundException {
- checkByteOrderLittleEndian(apkSigningBlock);
- // FORMAT:
- // OFFSET DATA TYPE DESCRIPTION
- // * @+0 bytes uint64: size in bytes (excluding this field)
- // * @+8 bytes pairs
- // * @-24 bytes uint64: size in bytes (same as the one above)
- // * @-16 bytes uint128: magic
- ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
-
- int entryCount = 0;
- while (pairs.hasRemaining()) {
- entryCount++;
- if (pairs.remaining() < 8) {
- throw new SignatureNotFoundException(
- "Insufficient data to read size of APK Signing Block entry #" + entryCount);
- }
- long lenLong = pairs.getLong();
- if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
- throw new SignatureNotFoundException(
- "APK Signing Block entry #" + entryCount
- + " size out of range: " + lenLong);
- }
- int len = (int) lenLong;
- int nextEntryPos = pairs.position() + len;
- if (len > pairs.remaining()) {
- throw new SignatureNotFoundException(
- "APK Signing Block entry #" + entryCount + " size out of range: " + len
- + ", available: " + pairs.remaining());
- }
- int id = pairs.getInt();
- if (id == blockId) {
- return getByteBuffer(pairs, len - 4);
- }
- pairs.position(nextEntryPos);
- }
-
- throw new SignatureNotFoundException(
- "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
- }
-
- public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
- if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
- throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
- }
- }
-
- /**
- * Returns new byte buffer whose content is a shared subsequence of this buffer's content
- * between the specified start (inclusive) and end (exclusive) positions. As opposed to
- * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
- * buffer's byte order.
- */
- private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
- if (start < 0) {
- throw new IllegalArgumentException("start: " + start);
- }
- if (end < start) {
- throw new IllegalArgumentException("end < start: " + end + " < " + start);
- }
- int capacity = source.capacity();
- if (end > source.capacity()) {
- throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
- }
- int originalLimit = source.limit();
- int originalPosition = source.position();
try {
- source.position(0);
- source.limit(end);
- source.position(start);
- ByteBuffer result = source.slice();
- result.order(source.order());
- return result;
- } finally {
- source.position(0);
- source.limit(originalLimit);
- source.position(originalPosition);
+ return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
}
}
- /**
- * Relative <em>get</em> method for reading {@code size} number of bytes from the current
- * position of this buffer.
- *
- * <p>This method reads the next {@code size} bytes at this buffer's current position,
- * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
- * {@code size}, byte order set to this buffer's byte order; and then increments the position by
- * {@code size}.
- */
- private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
- if (size < 0) {
- throw new IllegalArgumentException("size: " + size);
- }
- int originalLimit = source.limit();
- int position = source.position();
- int limit = position + size;
- if ((limit < position) || (limit > originalLimit)) {
- throw new BufferUnderflowException();
- }
- source.limit(limit);
- try {
- ByteBuffer result = source.slice();
- result.order(source.order());
- source.position(limit);
- return result;
- } finally {
- source.limit(originalLimit);
- }
+ public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
}
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
- if (source.remaining() < 4) {
- throw new ApkFormatException(
- "Remaining buffer too short to contain length of length-prefixed field"
- + ". Remaining: " + source.remaining());
- }
- int len = source.getInt();
- if (len < 0) {
- throw new IllegalArgumentException("Negative length");
- } else if (len > source.remaining()) {
- throw new ApkFormatException(
- "Length-prefixed field longer than remaining buffer"
- + ". Field length: " + len + ", remaining: " + source.remaining());
- }
- return getByteBuffer(source, len);
+ return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source);
}
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
- int len = buf.getInt();
- if (len < 0) {
- throw new ApkFormatException("Negative length");
- } else if (len > buf.remaining()) {
- throw new ApkFormatException(
- "Underflow while reading length-prefixed value. Length: " + len
- + ", available: " + buf.remaining());
- }
- byte[] result = new byte[len];
- buf.get(result);
- return result;
+ return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf);
}
public static String toHex(byte[] value) {
- StringBuilder sb = new StringBuilder(value.length * 2);
- int len = value.length;
- for (int i = 0; i < len; i++) {
- int hi = (value[i] & 0xff) >>> 4;
- int lo = value[i] & 0x0f;
- sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
- }
- return sb.toString();
+ return ApkSigningBlockUtilsLite.toHex(value);
}
public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
@@ -767,6 +593,7 @@ public class ApkSigningBlockUtils {
}
}
+ @SuppressWarnings("ByteBufferBackingArray")
private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
throws IOException, NoSuchAlgorithmException {
@@ -810,6 +637,7 @@ public class ApkSigningBlockUtils {
}
}
+ @SuppressWarnings("ByteBufferBackingArray")
public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
throws IOException, NoSuchAlgorithmException {
ByteBuffer encoded = createVerityDigestBuffer(false);
@@ -840,7 +668,8 @@ public class ApkSigningBlockUtils {
if ("X.509".equals(publicKey.getFormat())) {
encodedPublicKey = publicKey.getEncoded();
// if the key is an RSA key check for a negative modulus
- if ("RSA".equals(publicKey.getAlgorithm())) {
+ String keyAlgorithm = publicKey.getAlgorithm();
+ if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
try {
// Parse the encoded public key into the separate elements of the
// SubjectPublicKeyInfo to obtain the SubjectPublicKey.
@@ -944,20 +773,8 @@ public class ApkSigningBlockUtils {
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
List<Pair<Integer, byte[]>> sequence) {
- int resultSize = 0;
- for (Pair<Integer, byte[]> element : sequence) {
- resultSize += 12 + element.getSecond().length;
- }
- ByteBuffer result = ByteBuffer.allocate(resultSize);
- result.order(ByteOrder.LITTLE_ENDIAN);
- for (Pair<Integer, byte[]> element : sequence) {
- byte[] second = element.getSecond();
- result.putInt(8 + second.length);
- result.putInt(element.getFirst());
- result.putInt(second.length);
- result.put(second);
- }
- return result.array();
+ return ApkSigningBlockUtilsLite
+ .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence);
}
/**
@@ -974,30 +791,11 @@ public class ApkSigningBlockUtils {
public static SignatureInfo findSignature(
DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)
throws IOException, SignatureNotFoundException {
- // Find the APK Signing Block.
- DataSource apkSigningBlock;
- long apkSigningBlockOffset;
try {
- ApkUtils.ApkSigningBlock apkSigningBlockInfo =
- ApkUtils.findApkSigningBlock(apk, zipSections);
- apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
- apkSigningBlock = apkSigningBlockInfo.getContents();
- } catch (ApkSigningBlockNotFoundException e) {
- throw new SignatureNotFoundException(e.getMessage(), e);
+ return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
}
- ByteBuffer apkSigningBlockBuf =
- apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
- apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
-
- // Find the APK Signature Scheme Block inside the APK Signing Block.
- ByteBuffer apkSignatureSchemeBlock =
- findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result);
- return new SignatureInfo(
- apkSignatureSchemeBlock,
- apkSigningBlockOffset,
- zipSections.getZipCentralDirectoryOffset(),
- zipSections.getZipEndOfCentralDirectoryOffset(),
- zipSections.getZipEndOfCentralDirectory());
}
/**
@@ -1051,7 +849,7 @@ public class ApkSigningBlockUtils {
// uint64: size (excluding this field)
// uint32: ID
// (size - 4) bytes: value
- // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes)
+ // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
// uint64: size (same as the one above)
// uint128: magic
@@ -1085,7 +883,6 @@ public class ApkSigningBlockUtils {
long blockSizeFieldValue = resultSize - 8L;
result.putLong(blockSizeFieldValue);
-
for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
int apkSignatureSchemeId = schemeBlockPair.getSecond();
@@ -1106,6 +903,116 @@ public class ApkSigningBlockUtils {
}
/**
+ * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a
+ * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the
+ * contents / value of the signature block and the second element is the ID of the block.
+ *
+ * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock}
+ */
+ public static List<Pair<byte[], Integer>> getApkSignatureBlocks(
+ DataSource apkSigningBlock) throws IOException {
+ // FORMAT:
+ // uint64: size (excluding this field)
+ // repeated ID-value pairs:
+ // uint64: size (excluding this field)
+ // uint32: ID
+ // (size - 4) bytes: value
+ // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
+ // uint64: size (same as the one above)
+ // uint128: magic
+ long apkSigningBlockSize = apkSigningBlock.size();
+ if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) {
+ throw new IllegalArgumentException(
+ "APK signing block size out of range: " + apkSigningBlockSize);
+ }
+ // Remove the header and footer from the signing block to iterate over only the repeated
+ // ID-value pairs.
+ ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8,
+ (int) apkSigningBlock.size() - 32);
+ apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>();
+ while (apkSigningBlockBuffer.hasRemaining()) {
+ long blockLength = apkSigningBlockBuffer.getLong();
+ if (blockLength > Integer.MAX_VALUE || blockLength < 4) {
+ throw new IllegalArgumentException(
+ "Block index " + (signatureBlocks.size() + 1) + " size out of range: "
+ + blockLength);
+ }
+ int blockId = apkSigningBlockBuffer.getInt();
+ // Since the block ID has already been read from the signature block read the next
+ // blockLength - 4 bytes as the value.
+ byte[] blockValue = new byte[(int) blockLength - 4];
+ apkSigningBlockBuffer.get(blockValue);
+ signatureBlocks.add(Pair.of(blockValue, blockId));
+ }
+ return signatureBlocks;
+ }
+
+ /**
+ * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code
+ * List} of {@code Pair} instances where the first element is a {@code List} of {@link
+ * X509Certificate}s and the second element is a byte array of the individual signer's block.
+ *
+ * <p>This method supports any signature block that adheres to the following format up to the
+ * signing certificate(s):
+ * <pre>
+ * * length-prefixed sequence of length-prefixed signers
+ * * length-prefixed signed data
+ * * length-prefixed sequence of length-prefixed digests:
+ * * uint32: signature algorithm ID
+ * * length-prefixed bytes: digest of contents
+ * * length-prefixed sequence of certificates:
+ * * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ * </pre>
+ *
+ * <p>Note, this is a convenience method to obtain any signers from an existing signature block;
+ * the signature of each signer will not be verified.
+ *
+ * @throws ApkFormatException if an error is encountered while parsing the provided {@code
+ * signatureBlock}
+ * @throws CertificateException if the signing certificate(s) within an individual signer block
+ * cannot be parsed
+ */
+ public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners(
+ byte[] signatureBlock) throws ApkFormatException, CertificateException {
+ ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock);
+ signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer);
+ List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>();
+ while (signersBuffer.hasRemaining()) {
+ // Parse the next signer block, save all of its bytes for the resulting List, and
+ // rewind the buffer to allow the signing certificate(s) to be parsed.
+ ByteBuffer signer = getLengthPrefixedSlice(signersBuffer);
+ byte[] signerBytes = new byte[signer.remaining()];
+ signer.get(signerBytes);
+ signer.rewind();
+
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ // The first length prefixed slice is the sequence of digests which are not required
+ // when obtaining the signing certificate(s).
+ getLengthPrefixedSlice(signedData);
+ ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certificates = new ArrayList<>();
+ while (certificatesBuffer.hasRemaining()) {
+ int certLength = certificatesBuffer.getInt();
+ byte[] certBytes = new byte[certLength];
+ if (certLength > certificatesBuffer.remaining()) {
+ throw new IllegalArgumentException(
+ "Cert index " + (certificates.size() + 1) + " under signer index "
+ + (signers.size() + 1) + " size out of range: " + certLength);
+ }
+ certificatesBuffer.get(certBytes);
+ GuaranteedEncodedFormX509Certificate signerCert =
+ new GuaranteedEncodedFormX509Certificate(
+ X509CertificateUtils.generateCertificate(certBytes), certBytes);
+ certificates.add(signerCert);
+ }
+ signers.add(Pair.of(certificates, signerBytes));
+ }
+ return signers;
+ }
+
+ /**
* Computes the digests of the given APK components according to the algorithms specified in the
* given SignerConfigs.
*
@@ -1171,57 +1078,39 @@ public class ApkSigningBlockUtils {
* @throws NoSupportedSignaturesException if no supported signatures were
* found for an Android platform version in the range.
*/
- public static List<SupportedSignature> getSignaturesToVerify(
- List<SupportedSignature> signatures, int minSdkVersion, int maxSdkVersion)
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion)
throws NoSupportedSignaturesException {
- // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
- // Android's behavior on those versions.
- //
- // Here we assume that, once introduced, a signature algorithm continues to be supported in
- // all future Android versions. We also assume that the better-than relationship between
- // algorithms is exactly the same on all Android platform versions (except that older
- // platforms might support fewer algorithms). If these assumption are no longer true, the
- // logic here will need to change accordingly.
- Map<Integer, SupportedSignature> bestSigAlgorithmOnSdkVersion = new HashMap<>();
- int minProvidedSignaturesVersion = Integer.MAX_VALUE;
- for (SupportedSignature sig : signatures) {
- SignatureAlgorithm sigAlgorithm = sig.algorithm;
- int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion();
- if (sigMinSdkVersion > maxSdkVersion) {
- continue;
- }
- if (sigMinSdkVersion < minProvidedSignaturesVersion) {
- minProvidedSignaturesVersion = sigMinSdkVersion;
- }
-
- SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
- if ((candidate == null)
- || (compareSignatureAlgorithm(
- sigAlgorithm, candidate.algorithm) > 0)) {
- bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
- }
- }
+ return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+ }
- // Must have some supported signature algorithms for minSdkVersion.
- if (minSdkVersion < minProvidedSignaturesVersion) {
- throw new NoSupportedSignaturesException(
- "Minimum provided signature version " + minProvidedSignaturesVersion +
- " > minSdkVersion " + minSdkVersion);
- }
- if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
- throw new NoSupportedSignaturesException("No supported signature");
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+ * signature within the signing block using the standard JCA.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion,
+ boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException {
+ try {
+ return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion,
+ maxSdkVersion, onlyRequireJcaSupport);
+ } catch (NoApkSupportedSignaturesException e) {
+ throw new NoSupportedSignaturesException(e.getMessage());
}
- List<SupportedSignature> signaturesToVerify =
- new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
- Collections.sort(
- signaturesToVerify,
- (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
- return signaturesToVerify;
}
- public static class NoSupportedSignaturesException extends Exception {
- private static final long serialVersionUID = 1L;
-
+ public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException {
public NoSupportedSignaturesException(String message) {
super(message);
}
@@ -1384,19 +1273,14 @@ public class ApkSigningBlockUtils {
public SigningCertificateLineage mSigningCertificateLineage;
}
- public static class Result {
- public final int signatureSchemeVersion;
-
- /** Whether the APK's APK Signature Scheme signature verifies. */
- public boolean verified;
-
- public final List<Result.SignerInfo> signers = new ArrayList<>();
+ public static class Result extends ApkSigResult {
public SigningCertificateLineage signingCertificateLineage = null;
+ public final List<Result.SignerInfo> signers = new ArrayList<>();
private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
public Result(int signatureSchemeVersion) {
- this.signatureSchemeVersion = signatureSchemeVersion;
+ super(signatureSchemeVersion);
}
public boolean containsErrors() {
@@ -1435,17 +1319,17 @@ public class ApkSigningBlockUtils {
mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters));
}
+ @Override
public List<ApkVerifier.IssueWithParams> getErrors() {
return mErrors;
}
+ @Override
public List<ApkVerifier.IssueWithParams> getWarnings() {
return mWarnings;
}
- public static class SignerInfo {
- public int index;
- public List<X509Certificate> certs = new ArrayList<>();
+ public static class SignerInfo extends ApkSignerInfo {
public List<ContentDigest> contentDigests = new ArrayList<>();
public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
public List<Signature> signatures = new ArrayList<>();
@@ -1539,13 +1423,9 @@ public class ApkSigningBlockUtils {
}
}
- public static class SupportedSignature {
- public final SignatureAlgorithm algorithm;
- public final byte[] signature;
-
+ public static class SupportedSignature extends ApkSupportedSignature {
public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
- this.algorithm = algorithm;
- this.signature = signature;
+ super(algorithm, signature);
}
}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
new file mode 100644
index 0000000..40ae947
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
+ * utility functionality.
+ */
+public class ApkSigningBlockUtilsLite {
+ private ApkSigningBlockUtilsLite() {}
+
+ private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+ /**
+ * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+ * and the additional information relevant for verifying the block against the file.
+ *
+ * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+ * identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+ * block ID.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+ * @throws IOException if an I/O error occurs while reading the APK
+ */
+ public static SignatureInfo findSignature(
+ DataSource apk, ZipSections zipSections, int blockId)
+ throws IOException, SignatureNotFoundException {
+ // Find the APK Signing Block.
+ DataSource apkSigningBlock;
+ long apkSigningBlockOffset;
+ try {
+ ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
+ ApkUtilsLite.findApkSigningBlock(apk, zipSections);
+ apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+ apkSigningBlock = apkSigningBlockInfo.getContents();
+ } catch (ApkSigningBlockNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage(), e);
+ }
+ ByteBuffer apkSigningBlockBuf =
+ apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+ apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Find the APK Signature Scheme Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeBlock =
+ findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
+ return new SignatureInfo(
+ apkSignatureSchemeBlock,
+ apkSigningBlockOffset,
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectory());
+ }
+
+ public static ByteBuffer findApkSignatureSchemeBlock(
+ ByteBuffer apkSigningBlock,
+ int blockId) throws SignatureNotFoundException {
+ checkByteOrderLittleEndian(apkSigningBlock);
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes pairs
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+ ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+ int entryCount = 0;
+ while (pairs.hasRemaining()) {
+ entryCount++;
+ if (pairs.remaining() < 8) {
+ throw new SignatureNotFoundException(
+ "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+ }
+ long lenLong = pairs.getLong();
+ if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount
+ + " size out of range: " + lenLong);
+ }
+ int len = (int) lenLong;
+ int nextEntryPos = pairs.position() + len;
+ if (len > pairs.remaining()) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount + " size out of range: " + len
+ + ", available: " + pairs.remaining());
+ }
+ int id = pairs.getInt();
+ if (id == blockId) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
+ }
+
+ public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoApkSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion)
+ throws NoApkSupportedSignaturesException {
+ return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+ * signature within the signing block using the standard JCA.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoApkSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion,
+ boolean onlyRequireJcaSupport) throws
+ NoApkSupportedSignaturesException {
+ // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
+ // Android's behavior on those versions.
+ //
+ // Here we assume that, once introduced, a signature algorithm continues to be supported in
+ // all future Android versions. We also assume that the better-than relationship between
+ // algorithms is exactly the same on all Android platform versions (except that older
+ // platforms might support fewer algorithms). If these assumption are no longer true, the
+ // logic here will need to change accordingly.
+ Map<Integer, T>
+ bestSigAlgorithmOnSdkVersion = new HashMap<>();
+ int minProvidedSignaturesVersion = Integer.MAX_VALUE;
+ for (T sig : signatures) {
+ SignatureAlgorithm sigAlgorithm = sig.algorithm;
+ int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
+ : sigAlgorithm.getMinSdkVersion();
+ if (sigMinSdkVersion > maxSdkVersion) {
+ continue;
+ }
+ if (sigMinSdkVersion < minProvidedSignaturesVersion) {
+ minProvidedSignaturesVersion = sigMinSdkVersion;
+ }
+
+ T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
+ if ((candidate == null)
+ || (compareSignatureAlgorithm(
+ sigAlgorithm, candidate.algorithm) > 0)) {
+ bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
+ }
+ }
+
+ // Must have some supported signature algorithms for minSdkVersion.
+ if (minSdkVersion < minProvidedSignaturesVersion) {
+ throw new NoApkSupportedSignaturesException(
+ "Minimum provided signature version " + minProvidedSignaturesVersion +
+ " > minSdkVersion " + minSdkVersion);
+ }
+ if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
+ throw new NoApkSupportedSignaturesException("No supported signature");
+ }
+ List<T> signaturesToVerify =
+ new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+ Collections.sort(
+ signaturesToVerify,
+ (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+ return signaturesToVerify;
+ }
+
+ /**
+ * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+ * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+ */
+ public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+ ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+ ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+ return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+ }
+
+ /**
+ * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
+ * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
+ */
+ private static int compareContentDigestAlgorithm(
+ ContentDigestAlgorithm alg1,
+ ContentDigestAlgorithm alg2) {
+ switch (alg1) {
+ case CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ case VERITY_CHUNKED_SHA256:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case CHUNKED_SHA512:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ case VERITY_CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case VERITY_CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 1;
+ case VERITY_CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown alg1: " + alg1);
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ /**
+ * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+ * position of this buffer.
+ *
+ * <p>This method reads the next {@code size} bytes at this buffer's current position,
+ * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+ * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+ * {@code size}.
+ */
+ private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ int originalLimit = source.limit();
+ int position = source.position();
+ int limit = position + size;
+ if ((limit < position) || (limit > originalLimit)) {
+ throw new BufferUnderflowException();
+ }
+ source.limit(limit);
+ try {
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ source.position(limit);
+ return result;
+ } finally {
+ source.limit(originalLimit);
+ }
+ }
+
+ public static String toHex(byte[] value) {
+ StringBuilder sb = new StringBuilder(value.length * 2);
+ int len = value.length;
+ for (int i = 0; i < len; i++) {
+ int hi = (value[i] & 0xff) >>> 4;
+ int lo = value[i] & 0x0f;
+ sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+ }
+ return sb.toString();
+ }
+
+ public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+ if (source.remaining() < 4) {
+ throw new ApkFormatException(
+ "Remaining buffer too short to contain length of length-prefixed field"
+ + ". Remaining: " + source.remaining());
+ }
+ int len = source.getInt();
+ if (len < 0) {
+ throw new IllegalArgumentException("Negative length");
+ } else if (len > source.remaining()) {
+ throw new ApkFormatException(
+ "Length-prefixed field longer than remaining buffer"
+ + ". Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new ApkFormatException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new ApkFormatException(
+ "Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ List<Pair<Integer, byte[]>> sequence) {
+ int resultSize = 0;
+ for (Pair<Integer, byte[]> element : sequence) {
+ resultSize += 12 + element.getSecond().length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair<Integer, byte[]> element : sequence) {
+ byte[] second = element.getSecond();
+ result.putInt(8 + second.length);
+ result.putInt(element.getFirst());
+ result.putInt(second.length);
+ result.put(second);
+ }
+ return result.array();
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
new file mode 100644
index 0000000..61652a4
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base implementation of a supported signature for an APK.
+ */
+public class ApkSupportedSignature {
+ public final SignatureAlgorithm algorithm;
+ public final byte[] signature;
+
+ /**
+ * Constructs a new supported signature using the provided {@code algorithm} and {@code
+ * signature} bytes.
+ */
+ public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+ this.algorithm = algorithm;
+ this.signature = signature;
+ }
+
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
index e463743..b806d1e 100644
--- a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
@@ -25,7 +25,10 @@ public enum ContentDigestAlgorithm {
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
/** SHA2-256 over 4 KB chunks for APK verity. */
- VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8);
+ VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
+
+ /** Non-chunk SHA2-256. */
+ SHA256(4, "SHA-256", 256 / 8);
private final int mId;
private final String mJcaMessageDigestAlgorithm;
diff --git a/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
new file mode 100644
index 0000000..52c6085
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base exception that is thrown when there are no signatures that support the full range of
+ * requested platform versions.
+ */
+public class NoApkSupportedSignaturesException extends Exception {
+ public NoApkSupportedSignaturesException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
index 0db8cb8..804eb37 100644
--- a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
@@ -38,7 +38,8 @@ public enum SignatureAlgorithm {
Pair.of("SHA256withRSA/PSS",
new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.M),
/**
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
@@ -52,7 +53,8 @@ public enum SignatureAlgorithm {
"SHA512withRSA/PSS",
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.M),
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA256(
@@ -60,7 +62,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA512(
@@ -68,7 +71,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA512,
"RSA",
Pair.of("SHA512withRSA", null),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
ECDSA_WITH_SHA256(
@@ -76,7 +80,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.HONEYCOMB),
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
ECDSA_WITH_SHA512(
@@ -84,7 +89,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA512,
"EC",
Pair.of("SHA512withECDSA", null),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.HONEYCOMB),
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
DSA_WITH_SHA256(
@@ -92,7 +98,20 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
- AndroidSdkVersion.N),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /**
+ * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
+ * deterministically according to RFC 6979.
+ */
+ DETDSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDetDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
/**
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
@@ -104,7 +123,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
- AndroidSdkVersion.P),
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.INITIAL_RELEASE),
/**
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
@@ -116,7 +136,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
- AndroidSdkVersion.P),
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.HONEYCOMB),
/**
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
@@ -128,24 +149,28 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
- AndroidSdkVersion.P);
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.INITIAL_RELEASE);
private final int mId;
private final String mJcaKeyAlgorithm;
private final ContentDigestAlgorithm mContentDigestAlgorithm;
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
private final int mMinSdkVersion;
+ private final int mJcaSigAlgMinSdkVersion;
SignatureAlgorithm(int id,
ContentDigestAlgorithm contentDigestAlgorithm,
String jcaKeyAlgorithm,
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
- int minSdkVersion) {
+ int minSdkVersion,
+ int jcaSigAlgMinSdkVersion) {
mId = id;
mContentDigestAlgorithm = contentDigestAlgorithm;
mJcaKeyAlgorithm = jcaKeyAlgorithm;
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
mMinSdkVersion = minSdkVersion;
+ mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
}
/**
@@ -181,6 +206,13 @@ public enum SignatureAlgorithm {
return mMinSdkVersion;
}
+ /**
+ * Returns the minimum SDK version that supports the JCA signature algorithm.
+ */
+ public int getJcaSigAlgMinSdkVersion() {
+ return mJcaSigAlgMinSdkVersion;
+ }
+
public static SignatureAlgorithm findById(int id) {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
if (alg.getId() == id) {
diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
new file mode 100644
index 0000000..95f06ef
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base exception that is thrown when the APK is not signed with the requested signature scheme.
+ */
+public class SignatureNotFoundException extends Exception {
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
new file mode 100644
index 0000000..93627ff
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
+public class SourceStampCertificateLineage {
+
+ private final static int FIRST_VERSION = 1;
+ private final static int CURRENT_VERSION = FIRST_VERSION;
+
+ /**
+ * Deserializes the binary representation of a SourceStampCertificateLineage. Also
+ * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+ * parent.
+ */
+ public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+ throws IOException {
+ List<SigningCertificateNode> result = new ArrayList<>();
+ int nodeCount = 0;
+ if (inputBytes == null || !inputBytes.hasRemaining()) {
+ return null;
+ }
+
+ ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ }
+
+ // FORMAT (little endian):
+ // * uint32: version code
+ // * sequence of length-prefixed (uint32): nodes
+ // * length-prefixed bytes: signed data
+ // * length-prefixed bytes: certificate
+ // * uint32: signature algorithm id
+ // * uint32: flags
+ // * uint32: signature algorithm id (used by to sign next cert in lineage)
+ // * length-prefixed bytes: signature over above signed data
+
+ X509Certificate lastCert = null;
+ int lastSigAlgorithmId = 0;
+
+ try {
+ int version = inputBytes.getInt();
+ if (version != CURRENT_VERSION) {
+ // we only have one version to worry about right now, so just check it
+ throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ + " different than any of which we are aware");
+ }
+ HashSet<X509Certificate> certHistorySet = new HashSet<>();
+ while (inputBytes.hasRemaining()) {
+ nodeCount++;
+ ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+ ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+ int flags = nodeBytes.getInt();
+ int sigAlgorithmId = nodeBytes.getInt();
+ SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+ byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+ if (lastCert != null) {
+ // Use previous level cert to verify current level
+ String jcaSignatureAlgorithm =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ PublicKey publicKey = lastCert.getPublicKey();
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ if (!sig.verify(signature)) {
+ throw new SecurityException("Unable to verify signature of certificate #"
+ + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ + " SourceStampCertificateLineage object");
+ }
+ }
+
+ signedData.rewind();
+ byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+ int signedSigAlgorithm = signedData.getInt();
+ if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+ throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ + nodeBytes + " when verifying SourceStampCertificateLineage object");
+ }
+ lastCert = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedCert));
+ lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+ if (certHistorySet.contains(lastCert)) {
+ throw new SecurityException("Encountered duplicate entries in "
+ + "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ + "signing certificates should be unique");
+ }
+ certHistorySet.add(lastCert);
+ lastSigAlgorithmId = sigAlgorithmId;
+ result.add(new SigningCertificateNode(
+ lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+ SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+ }
+ } catch(ApkFormatException | BufferUnderflowException e){
+ throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
+ } catch(NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e){
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #" + nodeCount
+ + " when parsing SourceStampCertificateLineage object", e);
+ } catch(CertificateException e){
+ throw new SecurityException("Failed to decode certificate #" + nodeCount
+ + " when parsing SourceStampCertificateLineage object", e);
+ }
+ return result;
+ }
+
+ /**
+ * Represents one signing certificate in the SourceStampCertificateLineage, which
+ * generally means it is/was used at some point to sign source stamps.
+ */
+ public static class SigningCertificateNode {
+
+ public SigningCertificateNode(
+ X509Certificate signingCert,
+ SignatureAlgorithm parentSigAlgorithm,
+ SignatureAlgorithm sigAlgorithm,
+ byte[] signature,
+ int flags) {
+ this.signingCert = signingCert;
+ this.parentSigAlgorithm = parentSigAlgorithm;
+ this.sigAlgorithm = sigAlgorithm;
+ this.signature = signature;
+ this.flags = flags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SigningCertificateNode)) return false;
+
+ SigningCertificateNode that = (SigningCertificateNode) o;
+ if (!signingCert.equals(that.signingCert)) return false;
+ if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+ if (sigAlgorithm != that.sigAlgorithm) return false;
+ if (!Arrays.equals(signature, that.signature)) return false;
+ if (flags != that.flags) return false;
+
+ // we made it
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
+ result = prime * result +
+ ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
+ result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
+ result = prime * result + Arrays.hashCode(signature);
+ result = prime * result + flags;
+ return result;
+ }
+
+ /**
+ * the signing cert for this node. This is part of the data signed by the parent node.
+ */
+ public final X509Certificate signingCert;
+
+ /**
+ * the algorithm used by this node's parent to bless this data. Its ID value is part of
+ * the data signed by the parent node. {@code null} for first node.
+ */
+ public final SignatureAlgorithm parentSigAlgorithm;
+
+ /**
+ * the algorithm used by this node to bless the next node's data. Its ID value is part
+ * of the signed data of the next node. {@code null} for the last node.
+ */
+ public SignatureAlgorithm sigAlgorithm;
+
+ /**
+ * signature over the signed data (above). The signature is from this node's parent
+ * signing certificate, which should correspond to the signing certificate used to sign an
+ * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+ */
+ public final byte[] signature;
+
+ /**
+ * the flags detailing how the platform should treat this signing cert
+ */
+ public int flags;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
new file mode 100644
index 0000000..2a949ad
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+/** Constants used for source stamp signing and verification. */
+public class SourceStampConstants {
+ private SourceStampConstants() {}
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+ public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
+ /**
+ * The source stamp timestamp attribute value is an 8-byte little-endian encoded long
+ * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
+ * of the attribute value buffer will be used to read the timestamp, and any additional buffer
+ * space will be ignored.
+ */
+ public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
index 3590c71..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
@@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.apksig.internal.apk.stamp;
-import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
-import static com.android.apksig.internal.apk.stamp.SourceStampSigner.SOURCE_STAMP_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
-import com.android.apksig.ApkVerifier;
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
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.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSupportedSignature;
+import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
-import com.android.apksig.internal.util.Pair;
-import com.android.apksig.internal.util.X509CertificateUtils;
-import com.android.apksig.util.DataSource;
-import java.io.IOException;
+import java.io.ByteArrayInputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -47,12 +47,9 @@ import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* Source Stamp verifier.
@@ -64,122 +61,139 @@ import java.util.stream.Collectors;
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
*/
-public abstract class SourceStampVerifier {
-
+class SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
- private SourceStampVerifier() {}
+ private SourceStampVerifier() {
+ }
/**
- * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
- * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
- * {@code true}. If verification fails, the result will contain errors -- see {@link
- * ApkSigningBlockUtils.Result#getErrors()}.
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digest provided.
*
- * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
- * required cryptographic algorithm implementation is missing
- * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
- * found
- * @throws IOException if an I/O error occurs when reading the APK
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+ * maxSdkVersion]} range.
*/
- public static ApkSigningBlockUtils.Result verify(
- DataSource apk,
- ApkUtils.ZipSections zipSections,
+ public static void verifyV1SourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSignerInfo result,
+ byte[] apkDigest,
byte[] sourceStampCertificateDigest,
- Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
int minSdkVersion,
int maxSdkVersion)
- throws IOException, NoSuchAlgorithmException,
- ApkSigningBlockUtils.SignatureNotFoundException {
- ApkSigningBlockUtils.Result result =
- new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
- SignatureInfo signatureInfo =
- ApkSigningBlockUtils.findSignature(apk, zipSections, SOURCE_STAMP_BLOCK_ID, result);
+ throws ApkFormatException, NoSuchAlgorithmException {
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
- verify(
- signatureInfo.signatureBlock,
- sourceStampCertificateDigest,
- apkContentDigests,
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+ verifySourceStampSignature(
+ apkDigest,
minSdkVersion,
maxSdkVersion,
+ sourceStampCertificate,
+ apkDigestSignatures,
result);
- return result;
- }
-
- /**
- * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
- * {@code result}. APK is considered verified only if there are no errors reported in the {@code
- * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
- * more information about the contract of this method.
- */
- private static void verify(
- ByteBuffer sourceStampBlock,
- byte[] sourceStampCertificateDigest,
- Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
- int minSdkVersion,
- int maxSdkVersion,
- ApkSigningBlockUtils.Result result)
- throws NoSuchAlgorithmException {
- ApkSigningBlockUtils.Result.SignerInfo signerInfo =
- new ApkSigningBlockUtils.Result.SignerInfo();
- result.signers.add(signerInfo);
- try {
- CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
- ByteBuffer sourceStampBlockData =
- ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
- parseSourceStamp(
- sourceStampBlockData,
- certFactory,
- signerInfo,
- apkContentDigests,
- sourceStampCertificateDigest,
- minSdkVersion,
- maxSdkVersion);
- result.verified = !result.containsErrors() && !result.containsWarnings();
- } catch (CertificateException e) {
- throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
- } catch (ApkFormatException | BufferUnderflowException e) {
- signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
- }
}
/**
* Parses the SourceStamp block and populates the {@code result}.
*
- * <p>This verifies signatures over digests contained in the APK signing block.
+ * <p>This verifies signatures over digest of multiple signature schemes provided.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
* maxSdkVersion]} range.
*/
- private static void parseSourceStamp(
+ public static void verifyV2SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
- Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ ApkSignerInfo result,
+ Map<Integer, byte[]> signatureSchemeApkDigests,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
int maxSdkVersion)
throws ApkFormatException, NoSuchAlgorithmException {
- List<Pair<Integer, byte[]>> digests = new ArrayList<>();
- for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
- apkContentDigests.entrySet()) {
- digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+
+ // Parse signed signature schemes block.
+ ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
+ Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
+ while (signedSignatureSchemes.hasRemaining()) {
+ ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
+ int signatureSchemeId = signedSignatureScheme.getInt();
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
+ signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
+ }
+
+ for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
+ signatureSchemeApkDigests.entrySet()) {
+ // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
+ // v3.0 block must always be present with a v3.1 block is it sufficient to just use the
+ // v3.0 block?
+ if (signatureSchemeApkDigest.getKey()
+ == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
+ continue;
+ }
+ if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+ verifySourceStampSignature(
+ signatureSchemeApkDigest.getValue(),
+ minSdkVersion,
+ maxSdkVersion,
+ sourceStampCertificate,
+ signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
+ result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
}
- Collections.sort(digests, Comparator.comparing(Pair::getFirst));
- byte[] digestBytes =
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+ if (sourceStampBlockData.hasRemaining()) {
+ // The stamp block contains some additional attributes.
+ ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
+ ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+
+ byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
+ stampAttributeData.get(stampAttributeBytes);
+ stampAttributeData.flip();
+
+ verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
+ sourceStampCertificate, stampAttributeDataSignatures, result);
+ if (result.containsErrors() || result.containsWarnings()) {
+ return;
+ }
+ parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
+ }
+ }
+
+ private static X509Certificate verifySourceStampCertificate(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ byte[] sourceStampCertificateDigest,
+ ApkSignerInfo result)
+ throws NoSuchAlgorithmException, ApkFormatException {
// Parse the SourceStamp certificate.
- byte[] sourceStampEncodedCertificate =
- ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+ byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
X509Certificate sourceStampCertificate;
try {
- sourceStampCertificate =
- X509CertificateUtils.generateCertificate(
- sourceStampEncodedCertificate, certFactory);
+ sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(sourceStampEncodedCertificate));
} catch (CertificateException e) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
- return;
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+ return null;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
@@ -189,7 +203,6 @@ public abstract class SourceStampVerifier {
new GuaranteedEncodedFormX509Certificate(
sourceStampCertificate, sourceStampEncodedCertificate);
result.certs.add(sourceStampCertificate);
-
// Verify the SourceStamp certificate found in the signing block is the same as the
// SourceStamp certificate found in the APK.
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
@@ -197,55 +210,71 @@ public abstract class SourceStampVerifier {
byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
result.addWarning(
- ApkVerifier.Issue
+ ApkVerificationIssue
.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
- ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
- ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
- return;
+ toHex(sourceStampBlockCertificateDigest),
+ toHex(sourceStampCertificateDigest));
+ return null;
}
+ return sourceStampCertificate;
+ }
+ private static void verifySourceStampSignature(
+ byte[] data,
+ int minSdkVersion,
+ int maxSdkVersion,
+ X509Certificate sourceStampCertificate,
+ ByteBuffer signatures,
+ ApkSignerInfo result) {
// Parse the signatures block and identify supported signatures
- ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
int signatureCount = 0;
- List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+ List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
- ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
- byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
- result.signatures.add(
- new ApkSigningBlockUtils.Result.SignerInfo.Signature(
- sigAlgorithmId, sigBytes));
+ byte[] sigBytes = readLengthPrefixedByteArray(signature);
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
- result.addWarning(
- ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ result.addInfoMessage(
+ ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+ sigAlgorithmId);
continue;
}
supportedSignatures.add(
- new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+ new ApkSupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+ result.addWarning(
+ ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
- if (result.signatures.isEmpty()) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ if (supportedSignatures.isEmpty()) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
return;
}
-
// Verify signatures over digests using the SourceStamp's certificate.
- List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+ List<ApkSupportedSignature> signaturesToVerify;
try {
signaturesToVerify =
- ApkSigningBlockUtils.getSignaturesToVerify(
- supportedSignatures, minSdkVersion, maxSdkVersion);
- } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ getSignaturesToVerify(
+ supportedSignatures, minSdkVersion, maxSdkVersion, true);
+ } catch (NoApkSupportedSignaturesException e) {
+ // To facilitate debugging capture the signature algorithms and resulting exception in
+ // the warning.
+ StringBuilder signatureAlgorithms = new StringBuilder();
+ for (ApkSupportedSignature supportedSignature : supportedSignatures) {
+ if (signatureAlgorithms.length() > 0) {
+ signatureAlgorithms.append(", ");
+ }
+ signatureAlgorithms.append(supportedSignature.algorithm);
+ }
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ signatureAlgorithms.toString(), e);
return;
}
- for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+ for (ApkSupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
@@ -258,21 +287,78 @@ public abstract class SourceStampVerifier {
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
- sig.update(digestBytes);
+ sig.update(data);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addWarning(
- ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
- result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
- | SignatureException e) {
+ | SignatureException
+ | NoSuchAlgorithmException e) {
result.addWarning(
- ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
}
+
+ private static void parseStampAttributes(ByteBuffer stampAttributeData,
+ X509Certificate sourceStampCertificate, ApkSignerInfo result)
+ throws ApkFormatException {
+ ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
+ int stampAttributeCount = 0;
+ while (stampAttributes.hasRemaining()) {
+ stampAttributeCount++;
+ try {
+ ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
+ int id = attribute.getInt();
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
+ readStampCertificateLineage(value, sourceStampCertificate, result);
+ } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
+ long timestamp = ByteBuffer.wrap(value).order(
+ ByteOrder.LITTLE_ENDIAN).getLong();
+ if (timestamp > 0) {
+ result.timestamp = timestamp;
+ } else {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+ timestamp);
+ }
+ } else {
+ result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
+ }
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+ stampAttributeCount);
+ return;
+ }
+ }
+ }
+
+ private static void readStampCertificateLineage(byte[] lineageBytes,
+ X509Certificate sourceStampCertificate, ApkSignerInfo result) {
+ try {
+ // SourceStampCertificateLineage is verified when built
+ List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
+ SourceStampCertificateLineage.readSigningCertificateLineage(
+ ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
+ for (int i = 0; i < nodes.size(); i++) {
+ result.certificateLineage.add(nodes.get(i).signingCert);
+ }
+ // Make sure that the last cert in the chain matches this signer cert
+ if (!sourceStampCertificate.equals(
+ result.certificateLineage.get(result.certificateLineage.size() - 1))) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ }
+ } catch (SecurityException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ } catch (IllegalArgumentException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ } catch (Exception e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
index d7e10e7..dee24bd 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -34,7 +34,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* SourceStamp signer.
@@ -45,13 +44,15 @@ import java.util.stream.Collectors;
*
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
+ *
+ * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
*/
-public abstract class SourceStampSigner {
-
- public static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+public abstract class V1SourceStampSigner {
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
- private SourceStampSigner() {}
+ private V1SourceStampSigner() {}
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
@@ -97,8 +98,8 @@ public abstract class SourceStampSigner {
// FORMAT:
// * length-prefixed stamp block.
- return Pair.of(
- encodeAsLengthPrefixedElement(sourceStampSignerBlock), SOURCE_STAMP_BLOCK_ID);
+ return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
}
private static final class SourceStampBlock {
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
new file mode 100644
index 0000000..c3fdeec
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+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.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
+ */
+public abstract class V1SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigningBlockUtils.Result verify(
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(
+ apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ apkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result)
+ throws NoSuchAlgorithmException {
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.signers.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ getApkDigests(apkContentDigests));
+ SourceStampVerifier.verifyV1SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ digestBytes,
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+ return digests;
+ }
+}
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
new file mode 100644
index 0000000..ee3f840
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
+ */
+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(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 {
+ 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");
+ }
+
+ // Extract the digests for signature schemes.
+ List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_JAR_SIGNATURE_SCHEME,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
+
+ SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+ try {
+ sourceStampBlock.stampCertificate =
+ mSourceStampSignerConfig.certificates.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException(
+ "Retrieving the encoded form of the stamp certificate failed", e);
+ }
+
+ sourceStampBlock.signedDigests = signatureSchemeDigests;
+
+ sourceStampBlock.stampAttributes = encodeStampAttributes(
+ generateStampAttributes(mSourceStampSignerConfig.mSigningCertificateLineage));
+ sourceStampBlock.signedStampAttributes =
+ ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
+ sourceStampBlock.stampAttributes);
+
+ // FORMAT:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ // * length-prefixed bytes: encoded stamp attributes
+ // * length-prefixed sequence of length-prefixed signed stamp attributes:
+ // * uint32: signature algorithm id
+ // * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
+ byte[] sourceStampSignerBlock =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][]{
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ sourceStampBlock.stampAttributes,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedStampAttributes),
+ });
+
+ // FORMAT:
+ // * length-prefixed stamp block.
+ return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private static void getSignedDigestsFor(
+ int signatureSchemeVersion,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
+ SignerConfig mSourceStampSignerConfig,
+ List<Pair<Integer, byte[]>> signatureSchemeDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+ return;
+ }
+
+ Map<ContentDigestAlgorithm, byte[]> digestInfo =
+ 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()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: digest algorithm id
+ // * length-prefixed bytes: digest of the respective digest algorithm
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed digests:
+ // * uint32: signature algorithm id
+ // * length-prefixed bytes: signed digest for the respective signature algorithm
+ List<Pair<Integer, byte[]>> signedDigest =
+ ApkSigningBlockUtils.generateSignaturesOverData(
+ mSourceStampSignerConfig, digestBytes);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ signatureSchemeDigests.add(
+ Pair.of(
+ signatureSchemeVersion,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedDigest)));
+ }
+
+ private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
+ int payloadSize = 0;
+ for (byte[] attributeValue : stampAttributes.values()) {
+ // Pair size + Attribute ID + Attribute value
+ payloadSize += 4 + 4 + attributeValue.length;
+ }
+
+ // FORMAT (little endian):
+ // * length-prefixed bytes: pair
+ // * uint32: ID
+ // * bytes: value
+ ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize);
+ for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
+ // Pair size
+ result.putInt(4 + stampAttribute.getValue().length);
+ result.putInt(stampAttribute.getKey());
+ result.put(stampAttribute.getValue());
+ }
+ return result.array();
+ }
+
+ private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+ HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
+
+ 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) {
+ stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
+ lineage.encodeSigningCertificateLineage());
+ }
+ return stampAttributes;
+ }
+
+ private static final class SourceStampBlock {
+ public byte[] stampCertificate;
+ public List<Pair<Integer, byte[]>> signedDigests;
+ // Optional stamp attributes that are not required for verification.
+ 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/stamp/V2SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
new file mode 100644
index 0000000..a215b98
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
+ */
+public abstract class V2SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigResult#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigResult#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigResult verify(
+ DataSource apk,
+ ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ ApkSigResult result =
+ new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtilsLite.findSignature(
+ apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigResult result)
+ throws NoSuchAlgorithmException {
+ ApkSignerInfo signerInfo = new ApkSignerInfo();
+ result.mSigners.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
+ SourceStampVerifier.verifyV2SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ getSignatureSchemeDigests(signatureSchemeApkContentDigests),
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static Map<Integer, byte[]> getSignatureSchemeDigests(
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
+ Map<Integer, byte[]> digests = new HashMap<>();
+ for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
+ signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
+ List<Pair<Integer, byte[]>> apkDigests =
+ getApkDigests(signatureSchemeApkContentDigest.getValue());
+ digests.put(
+ signatureSchemeApkContentDigest.getKey(),
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
+ }
+ return digests;
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
+ @Override
+ public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
+ return pair1.getFirst() - pair2.getFirst();
+ }
+ });
+ return digests;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
new file mode 100644
index 0000000..db1d15f
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v1;
+
+/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
+public class V1SchemeConstants {
+ private V1SchemeConstants() {}
+
+ public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+ public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
+ "X-Android-APK-Signed";
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
index 89f16d5..ee3c9d8 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
@@ -16,6 +16,7 @@
package com.android.apksig.internal.apk.v1;
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
@@ -59,17 +60,15 @@ import java.util.jar.Manifest;
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
*/
public abstract class V1SchemeSigner {
-
- public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+ public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
new Attributes.Name("Created-By");
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
- static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
- new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+ new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
/**
* Signer configuration.
@@ -91,6 +90,11 @@ public abstract class V1SchemeSigner {
* Digest algorithm used for the signature.
*/
public DigestAlgorithm signatureDigestAlgorithm;
+
+ /**
+ * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
+ */
+ public boolean deterministicDsaSigning;
}
/** Hidden constructor to prevent instantiation. */
@@ -108,7 +112,7 @@ public abstract class V1SchemeSigner {
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
// Prior to API Level 18, only SHA-1 can be used with RSA.
if (minSdkVersion < 18) {
return DigestAlgorithm.SHA1;
@@ -303,7 +307,7 @@ public abstract class V1SchemeSigner {
signatureJarEntries.add(
Pair.of(signatureBlockFileName, signatureBlock));
}
- signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents));
+ signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
return signatureJarEntries;
}
@@ -321,7 +325,7 @@ public abstract class V1SchemeSigner {
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
result.add(signatureBlockFileName);
}
- result.add(MANIFEST_ENTRY_NAME);
+ result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
return result;
}
@@ -497,7 +501,8 @@ public abstract class V1SchemeSigner {
PublicKey publicKey = signingCert.getPublicKey();
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
Pair<String, AlgorithmIdentifier> signatureAlgs =
- getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm);
+ getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
+ signerConfig.deterministicDsaSigning);
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
// Generate the cryptographic signature of the signature file
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index 111ac71..0ebef0e 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -26,6 +26,7 @@ import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.ApkVerifier.IssueWithParams;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.asn1.Asn1BerParser;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1DecodingException;
@@ -46,21 +47,25 @@ import com.android.apksig.internal.util.InclusiveIntRange;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import java.security.InvalidKeyException;
+import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
+import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@@ -82,9 +87,6 @@ import java.util.jar.Attributes;
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
*/
public abstract class V1SchemeVerifier {
-
- private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME;
-
private V1SchemeVerifier() {}
/**
@@ -231,7 +233,8 @@ public abstract class V1SchemeVerifier {
if (!entryName.startsWith("META-INF/")) {
continue;
}
- if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) {
+ if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(
+ entryName))) {
manifestEntry = cdRecord;
continue;
}
@@ -679,7 +682,27 @@ public abstract class V1SchemeVerifier {
String jcaSignatureAlgorithm =
getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid);
Signature s = Signature.getInstance(jcaSignatureAlgorithm);
- s.initVerify(signingCertificate.getPublicKey());
+ PublicKey publicKey = signingCertificate.getPublicKey();
+ try {
+ s.initVerify(publicKey);
+ } catch (InvalidKeyException e) {
+ // An InvalidKeyException could be caught if the PublicKey in the certificate is not
+ // properly encoded; attempt to resolve any encoding errors, generate a new public
+ // key, and reattempt the initVerify with the newly encoded key.
+ try {
+ byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey);
+ publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic(
+ new X509EncodedKeySpec(encodedPublicKey));
+ } catch (InvalidKeySpecException ikse) {
+ // If an InvalidKeySpecException is caught then throw the original Exception
+ // since the key couldn't be properly re-encoded, and the original Exception
+ // will have more useful debugging info.
+ throw e;
+ }
+ s = Signature.getInstance(jcaSignatureAlgorithm);
+ s.initVerify(publicKey);
+ }
+
if (signerInfo.signedAttrs != null) {
// Signed attributes present -- verify signature against the ASN.1 DER encoded form
// of signed attributes. This verifies integrity of the signature file because
@@ -939,7 +962,7 @@ public abstract class V1SchemeVerifier {
if (!Arrays.equals(expected, actual)) {
mResult.addWarning(
Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
- V1SchemeSigner.MANIFEST_ENTRY_NAME,
+ V1SchemeConstants.MANIFEST_ENTRY_NAME,
jcaDigestAlgorithm,
mSignatureFileEntry.getName(),
Base64.getEncoder().encodeToString(actual),
@@ -1049,7 +1072,7 @@ public abstract class V1SchemeVerifier {
Set<Integer> foundApkSigSchemeIds) {
String signedWithApkSchemes =
sfMainSection.getAttributeValue(
- V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+ V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
// This field contains a comma-separated list of APK signature scheme IDs which were
// used to sign this APK. Android rejects APKs where an ID is known to the platform but
// the APK didn't verify using that scheme.
@@ -1239,40 +1262,7 @@ public abstract class V1SchemeVerifier {
DataSource apk,
ApkUtils.ZipSections apkSections)
throws IOException, ApkFormatException {
- // Read the ZIP Central Directory
- long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
- if (cdSizeBytes > Integer.MAX_VALUE) {
- throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
- }
- long cdOffset = apkSections.getZipCentralDirectoryOffset();
- ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
- cd.order(ByteOrder.LITTLE_ENDIAN);
-
- // Parse the ZIP Central Directory
- int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
- List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
- for (int i = 0; i < expectedCdRecordCount; i++) {
- CentralDirectoryRecord cdRecord;
- int offsetInsideCd = cd.position();
- try {
- cdRecord = CentralDirectoryRecord.getRecord(cd);
- } catch (ZipFormatException e) {
- throw new ApkFormatException(
- "Malformed ZIP Central Directory record #" + (i + 1)
- + " at file offset " + (cdOffset + offsetInsideCd),
- e);
- }
- String entryName = cdRecord.getName();
- if (entryName.endsWith("/")) {
- // Ignore directory entries
- continue;
- }
- cdRecords.add(cdRecord);
- }
- // There may be more data in Central Directory, but we don't warn or throw because Android
- // ignores unused CD data.
-
- return cdRecords;
+ return ZipUtils.parseZipCentralDirectory(apk, apkSections);
}
/**
@@ -1376,7 +1366,7 @@ public abstract class V1SchemeVerifier {
Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
entryName,
expectedDigest.jcaDigestAlgorithm,
- V1SchemeSigner.MANIFEST_ENTRY_NAME,
+ V1SchemeConstants.MANIFEST_ENTRY_NAME,
Base64.getEncoder().encodeToString(actualDigest),
Base64.getEncoder().encodeToString(expectedDigest.digest));
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
new file mode 100644
index 0000000..0e244c8
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v2;
+
+/** Constants used by the V2 Signature Scheme signing and verification. */
+public class V2SchemeConstants {
+ private V2SchemeConstants() {}
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+ public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
index 10488ed..b69b7d3 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
@@ -70,7 +70,8 @@ public abstract class V2SchemeSigner {
* protected by signatures inside the block.
*/
- public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V2SchemeSigner() {}
@@ -84,8 +85,8 @@ public abstract class V2SchemeSigner {
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
* Signature Scheme v2
*/
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
@@ -99,7 +100,7 @@ public abstract class V2SchemeSigner {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
@@ -111,8 +112,11 @@ public abstract class V2SchemeSigner {
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
- algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
@@ -123,7 +127,7 @@ public abstract class V2SchemeSigner {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
@@ -138,13 +142,27 @@ public abstract class V2SchemeSigner {
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ boolean v3SigningEnabled)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
+ signerConfigs, v3SigningEnabled, null);
+ }
+
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
- boolean v3SigningEnabled)
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
@@ -152,19 +170,24 @@ public abstract class V2SchemeSigner {
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV2Block(
- digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled),
+ digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
+ preservedV2SignerBlocks),
digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
- boolean v3SigningEnabled)
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+ if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
+ signerBlocks.addAll(preservedV2SignerBlocks);
+ }
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
@@ -184,7 +207,7 @@ public abstract class V2SchemeSigner {
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
- APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
private static byte[] generateSignerBlock(
@@ -264,9 +287,6 @@ public abstract class V2SchemeSigner {
});
}
- // Attribute to check whether a newer APK Signature Scheme signature was stripped
- protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
-
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
if (v3SigningEnabled) {
// FORMAT (little endian):
@@ -277,7 +297,7 @@ public abstract class V2SchemeSigner {
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(payloadSize - 4);
- result.putInt(STRIPPING_PROTECTION_ATTR_ID);
+ result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
return result.array();
} else {
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index 9b821a7..f367908 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -61,9 +61,6 @@ import java.util.Set;
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V2SchemeVerifier {
-
- private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
/** Hidden constructor to prevent instantiation. */
private V2SchemeVerifier() {}
@@ -101,7 +98,7 @@ public abstract class V2SchemeVerifier {
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
- APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
@@ -247,8 +244,7 @@ public abstract class V2SchemeVerifier {
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
- int maxSdkVersion)
- throws ApkFormatException, NoSuchAlgorithmException {
+ int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
@@ -435,7 +431,7 @@ public abstract class V2SchemeVerifier {
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
switch (id) {
- case V2SchemeSigner.STRIPPING_PROTECTION_ATTR_ID:
+ case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
// stripping protection added when signing with a newer scheme
int foundId = ByteBuffer.wrap(value).order(
ByteOrder.LITTLE_ENDIAN).getInt();
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
new file mode 100644
index 0000000..6963dd3
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v3;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+
+/** Constants used by the V3 Signature Scheme signing and verification. */
+public class V3SchemeConstants {
+ private V3SchemeConstants() {}
+
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+ public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
+ public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
+ /**
+ * By default, APK signing key rotation will target T, but packages that have previously
+ * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
+ * --rotation-min-sdk-version parameter when using apksigner or when invoking
+ * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
+ */
+ public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
+
+ /**
+ * This attribute is intended to be written to the V3.0 signer block as an additional attribute
+ * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
+ * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
+ * for rotation in the v3.1 signing block is not X, then the APK should be rejected.
+ */
+ public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
+
+ /**
+ * This attribute is written to the V3.1 signer block as an additional attribute to signify that
+ * the rotation-min-sdk-version is targeting a development release. This is required to support
+ * testing rotation on new development releases as the previous platform release SDK version
+ * is used as the development release SDK version until the development release SDK is
+ * finalized.
+ */
+ public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
index 15d5481..ee5d3b4 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -24,12 +24,14 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicK
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -44,6 +46,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.OptionalInt;
/**
* APK Signature Scheme v3 signer.
@@ -56,12 +59,37 @@ import java.util.Map;
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
* it can prove the new siging certificate was signed by the old.
*/
-public abstract class V3SchemeSigner {
+public class V3SchemeSigner {
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
- public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+ private final int mBlockId;
+ private final OptionalInt mOptionalRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
- /** Hidden constructor to prevent instantiation. */
- private V3SchemeSigner() {}
+ private V3SchemeSigner(DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ RunnablesExecutor executor,
+ int blockId,
+ OptionalInt optionalRotationMinSdkVersion,
+ boolean rotationTargetsDevRelease) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ mExecutor = executor;
+ mBlockId = blockId;
+ mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ }
/**
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
@@ -72,8 +100,8 @@ public abstract class V3SchemeSigner {
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
* Signature Scheme v3
*/
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
@@ -87,7 +115,7 @@ public abstract class V3SchemeSigner {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
@@ -99,8 +127,11 @@ public abstract class V3SchemeSigner {
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
- algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
@@ -111,7 +142,7 @@ public abstract class V3SchemeSigner {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
@@ -125,31 +156,92 @@ public abstract class V3SchemeSigner {
}
}
- public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
- generateApkSignatureSchemeV3Block(
- RunnablesExecutor executor,
- DataSource beforeCentralDir,
- DataSource centralDir,
- DataSource eocd,
- List<SignerConfig> signerConfigs)
- throws IOException, InvalidKeyException, NoSuchAlgorithmException,
- SignatureException {
+ public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ }
+
+ public static byte[] generateV3SignerAttribute(
+ SigningCertificateLineage signingCertificateLineage) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - encoded V3 SigningCertificateLineage
+ byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
+ int payloadSize = 4 + 4 + encodedLineage.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(4 + encodedLineage.length);
+ result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
+ result.put(encodedLineage);
+ return result.array();
+ }
+
+ private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ int rotationMinSdkVersion) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - int32 representing minimum SDK version for rotation
+ int payloadSize = 4 + 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
+ result.putInt(rotationMinSdkVersion);
+ return result.array();
+ }
+
+ private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - No value is used for this attribute
+ int payloadSize = 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+ return result.array();
+ }
+
+ /**
+ * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
+ * signing scheme block and digests based on the parameters provided to the {@link Builder}.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
+ * @throws SignatureException if an error occurs when computing digests or generating
+ * signatures
+ */
+ public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
- executor, beforeCentralDir, centralDir, eocd, signerConfigs);
- return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
- generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
- digestInfo.getSecond());
+ mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
+ return new SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
}
- private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
- List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+ Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
- List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+ List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
int signerNumber = 0;
- for (SignerConfig signerConfig : signerConfigs) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
@@ -167,10 +259,10 @@ public abstract class V3SchemeSigner {
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
- APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ mBlockId);
}
- private static byte[] generateSignerBlock(
+ private byte[] generateSignerBlock(
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
@@ -220,7 +312,7 @@ public abstract class V3SchemeSigner {
return encodeSigner(signer);
}
- private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
+ private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
byte[] signatures =
encodeAsLengthPrefixedElement(
@@ -249,7 +341,7 @@ public abstract class V3SchemeSigner {
return result.array();
}
- private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
+ private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
byte[] digests =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
@@ -285,13 +377,26 @@ public abstract class V3SchemeSigner {
return result.array();
}
- public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
-
- private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
- if (signerConfig.mSigningCertificateLineage == null) {
- return new byte[0];
+ private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
+ if (signerConfig.mSigningCertificateLineage != null) {
+ byte[] lineageAttr = generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
+ // If this rotation is not targeting a development release, or if this is not a v3.1
+ // signer block then just return the lineage attribute.
+ if (!mRotationTargetsDevRelease
+ || mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ return lineageAttr;
+ }
+ byte[] devReleaseRotationAttr = generateV31RotationTargetsDevReleaseAttribute();
+ byte[] attributes = new byte[lineageAttr.length + devReleaseRotationAttr.length];
+ System.arraycopy(lineageAttr, 0, attributes, 0, lineageAttr.length);
+ System.arraycopy(devReleaseRotationAttr, 0, attributes, lineageAttr.length,
+ devReleaseRotationAttr.length);
+ return attributes;
+ } else if (mOptionalRotationMinSdkVersion.isPresent()) {
+ return generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ mOptionalRotationMinSdkVersion.getAsInt());
}
- return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute();
+ return new byte[0];
}
private static final class V3SignatureSchemeBlock {
@@ -311,4 +416,97 @@ public abstract class V3SchemeSigner {
public byte[] additionalAttributes;
}
}
+
+ /** Builder of {@link V3SchemeSigner} instances. */
+ public static class Builder {
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+ private boolean mRotationTargetsDevRelease = false;
+
+ /**
+ * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
+ * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
+ * be used to sign the APK.
+ */
+ public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
+ List<SignerConfig> signerConfigs) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the {@code blockId} to be used for the V3 signature block.
+ *
+ * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
+ * signer's block.
+ *
+ * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
+ * is not modified or removed from the APK's signature block.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets whether the minimum SDK version of a signer is intended to target a development
+ * release; this is primarily required after the T SDK is finalized, and an APK needs to
+ * target U during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. A signer with a minimum SDK version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeSigner} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeSigner build() {
+ return new V3SchemeSigner(mBeforeCentralDir,
+ mCentralDir,
+ mEocd,
+ mSignerConfigs,
+ mExecutor,
+ mBlockId,
+ mOptionalRotationMinSdkVersion,
+ mRotationTargetsDevRelease);
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index 659d379..956027f 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -28,15 +28,16 @@ import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundExc
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
-import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
-import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -53,7 +54,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
+import java.util.OptionalInt;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -67,12 +68,42 @@ import java.util.TreeMap;
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
-public abstract class V3SchemeVerifier {
-
- private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
-
- /** Hidden constructor to prevent instantiation. */
- private V3SchemeVerifier() {}
+public class V3SchemeVerifier {
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mApk;
+ private final ApkUtils.ZipSections mZipSections;
+ private final ApkSigningBlockUtils.Result mResult;
+ private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final int mBlockId;
+ private final OptionalInt mOptionalRotationMinSdkVersion;
+ private final boolean mFullVerification;
+
+ private ByteBuffer mApkSignatureSchemeV3Block;
+
+ private V3SchemeVerifier(
+ RunnablesExecutor executor,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ ApkSigningBlockUtils.Result result,
+ int minSdkVersion,
+ int maxSdkVersion,
+ int blockId,
+ OptionalInt optionalRotationMinSdkVersion,
+ boolean fullVerification) {
+ mExecutor = executor;
+ mApk = apk;
+ mZipSections = zipSections;
+ mContentDigestsToVerify = contentDigestsToVerify;
+ mResult = result;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ mBlockId = blockId;
+ mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+ mFullVerification = fullVerification;
+ }
/**
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
@@ -87,7 +118,10 @@ public abstract class V3SchemeVerifier {
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
- * @throws ApkFormatException if the APK is malformed
+ * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
+ * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
+ * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
+ *
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws SignatureNotFoundException if no APK Signature Scheme v3
@@ -101,34 +135,11 @@ public abstract class V3SchemeVerifier {
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
- ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
- SignatureInfo signatureInfo =
- ApkSigningBlockUtils.findSignature(apk, zipSections,
- APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
-
- DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
- DataSource centralDir =
- apk.slice(
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
- ByteBuffer eocd = signatureInfo.eocd;
-
- // v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
- // platforms
- if (minSdkVersion < AndroidSdkVersion.P) {
- minSdkVersion = AndroidSdkVersion.P;
- }
-
- verify(executor,
- beforeApkSigningBlock,
- signatureInfo.signatureBlock,
- centralDir,
- eocd,
- minSdkVersion,
- maxSdkVersion,
- result);
- return result;
+ return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .verify();
}
/**
@@ -137,33 +148,40 @@ public abstract class V3SchemeVerifier {
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
* int)} for more information about the contract of this method.
*
- * @param result result populated by this method with interesting information about the APK,
- * such as information about signers, and verification errors and warnings.
+ * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
+ * APK, such as information about signers, and verification errors and warnings
*/
- private static void verify(
- RunnablesExecutor executor,
- DataSource beforeApkSigningBlock,
- ByteBuffer apkSignatureSchemeV3Block,
- DataSource centralDir,
- ByteBuffer eocd,
- int minSdkVersion,
- int maxSdkVersion,
- ApkSigningBlockUtils.Result result)
- throws IOException, NoSuchAlgorithmException {
- Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
- parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
+ public ApkSigningBlockUtils.Result verify()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ if (mApk == null || mZipSections == null) {
+ throw new IllegalStateException(
+ "A non-null apk and zip sections must be specified to verify an APK's v3 "
+ + "signatures");
+ }
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
- if (result.containsErrors()) {
- return;
+ DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ mApk.slice(
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+ ByteBuffer eocd = signatureInfo.eocd;
+
+ parseSigners();
+
+ if (mResult.containsErrors()) {
+ return mResult;
}
- ApkSigningBlockUtils.verifyIntegrity(
- executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
+ mContentDigestsToVerify, mResult);
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
// longest SigningCertificateHistory, if present, corresponds to the newest platform
// versions
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
- for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
sortedSigners.put(signer.minSdkVersion, signer);
}
@@ -173,7 +191,7 @@ public abstract class V3SchemeVerifier {
int lastLineageSize = 0;
// while we're iterating through the signers, build up the list of lineages
- List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
+ List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
int currentMin = signer.minSdkVersion;
@@ -183,7 +201,7 @@ public abstract class V3SchemeVerifier {
firstMin = currentMin;
} else {
if (currentMin != lastMax + 1) {
- result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
+ mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
break;
}
}
@@ -193,7 +211,7 @@ public abstract class V3SchemeVerifier {
if (signer.signingCertificateLineage != null) {
int currLineageSize = signer.signingCertificateLineage.size();
if (currLineageSize < lastLineageSize) {
- result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
break;
}
lastLineageSize = currLineageSize;
@@ -201,20 +219,24 @@ public abstract class V3SchemeVerifier {
}
}
- // make sure we support our desired sdk ranges
- if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
- result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
+ // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
+ // then the max level only needs to support up to that sdk version for rotation.
+ if (firstMin > mMinSdkVersion
+ || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
+ ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
+ mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
}
try {
- result.signingCertificateLineage =
+ mResult.signingCertificateLineage =
SigningCertificateLineage.consolidateLineages(lineages);
} catch (IllegalArgumentException e) {
- result.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
}
- if (!result.containsErrors()) {
- result.verified = true;
+ if (!mResult.containsErrors()) {
+ mResult.verified = true;
}
+ return mResult;
}
/**
@@ -233,16 +255,49 @@ public abstract class V3SchemeVerifier {
ByteBuffer apkSignatureSchemeV3Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+ try {
+ new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
+ .setResult(result)
+ .setContentDigestsToVerify(contentDigestsToVerify)
+ .setFullVerification(false)
+ .build()
+ .parseSigners();
+ } catch (IOException | SignatureNotFoundException e) {
+ // This should never occur since the apkSignatureSchemeV3Block was already provided.
+ throw new IllegalStateException("An exception was encountered when attempting to parse"
+ + " the signers from the provided APK Signature Scheme v3 block", e);
+ }
+ }
+
+ /**
+ * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
+ * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
+ * returned {@link ApkSigningBlockUtils.Result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+ * However, this does not verify the integrity of the rest of the APK but rather simply reports
+ * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
+ *
+ * <p>This method adds one or more errors to the returned {@code Result} if a verification error
+ * is encountered when parsing the signers.
+ */
+ public ApkSigningBlockUtils.Result parseSigners()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
ByteBuffer signers;
try {
- signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
+ if (mApkSignatureSchemeV3Block == null) {
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+ }
+ signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
} catch (ApkFormatException e) {
- result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
- return;
+ mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
+ return mResult;
}
if (!signers.hasRemaining()) {
- result.addError(Issue.V3_SIG_NO_SIGNERS);
- return;
+ mResult.addError(Issue.V3_SIG_NO_SIGNERS);
+ return mResult;
}
CertificateFactory certFactory;
@@ -258,15 +313,16 @@ public abstract class V3SchemeVerifier {
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
- result.signers.add(signerInfo);
+ mResult.signers.add(signerInfo);
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
- parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
+ parseSigner(signer, certFactory, signerInfo);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
- return;
+ return mResult;
}
}
+ return mResult;
}
/**
@@ -281,12 +337,9 @@ public abstract class V3SchemeVerifier {
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
- private static void parseSigner(
- ByteBuffer signerBlock,
- CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
- Set<ContentDigestAlgorithm> contentDigestsToVerify)
- throws ApkFormatException, NoSuchAlgorithmException {
+ private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result)
+ throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
@@ -375,7 +428,7 @@ public abstract class V3SchemeVerifier {
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
- contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+ mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
@@ -485,6 +538,7 @@ public abstract class V3SchemeVerifier {
// Parse the additional attributes block.
int additionalAttributeCount = 0;
+ boolean rotationAttrFound = false;
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
@@ -494,7 +548,7 @@ public abstract class V3SchemeVerifier {
byte[] value = ByteBufferUtils.toByteArray(attribute);
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
- if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
+ if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
try {
// SigningCertificateLineage is verified when built
result.signingCertificateLineage =
@@ -512,6 +566,31 @@ public abstract class V3SchemeVerifier {
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
}
+ } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
+ rotationAttrFound = true;
+ // API targeting for rotation was added with V3.1; if the maxSdkVersion
+ // does not support v3.1 then ignore this attribute.
+ if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
+ && mFullVerification) {
+ int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
+ .order(ByteOrder.LITTLE_ENDIAN).getInt();
+ if (mOptionalRotationMinSdkVersion.isPresent()) {
+ int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
+ if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
+ result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
+ attrRotationMinSdkVersion, rotationMinSdkVersion);
+ }
+ } else {
+ result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
+ }
+ }
+ } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
+ // This attribute should only be used by a v3.1 signer to indicate rotation
+ // is targeting the development release that is using the SDK version of the
+ // previously released platform version.
+ if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+ }
} else {
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
@@ -521,5 +600,168 @@ public abstract class V3SchemeVerifier {
return;
}
}
+ if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
+ result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
+ mOptionalRotationMinSdkVersion.getAsInt());
+ }
+ }
+
+ /** Builder of {@link V3SchemeVerifier} instances. */
+ public static class Builder {
+ private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+ private DataSource mApk;
+ private ApkUtils.ZipSections mZipSections;
+ private ByteBuffer mApkSignatureSchemeV3Block;
+ private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private ApkSigningBlockUtils.Result mResult;
+ private int mMinSdkVersion;
+ private int mMaxSdkVersion;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private boolean mFullVerification = true;
+ private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * verify the V3 signing block of the provided {@code apk} with the specified {@code
+ * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
+ */
+ public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
+ int maxSdkVersion) {
+ mApk = apk;
+ mZipSections = zipSections;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
+ * apkSignatureSchemeV3Block}.
+ *
+ * <note>Full verification of the v3 signature is not possible when instantiating a new
+ * {@code V3SchemeVerifier} with this method.</note>
+ */
+ public Builder(ByteBuffer apkSignatureSchemeV3Block) {
+ mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the V3 {code blockId} to be verified in the provided APK.
+ *
+ * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
+ * attribute.
+ *
+ * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
+ * block of an APK; in the case of multiple signers targeting different SDK versions in the
+ * v3.1 signing block, the minimum SDK version from all the signers should be used.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets the {@code result} instance to be used when returning verification results.
+ *
+ * <p>This method can be used when the caller already has a {@link
+ * ApkSigningBlockUtils.Result} and wants to store the verification results in this
+ * instance.
+ */
+ public Builder setResult(ApkSigningBlockUtils.Result result) {
+ mResult = result;
+ return this;
+ }
+
+ /**
+ * Sets the instance to be used to store the {@code contentDigestsToVerify}.
+ *
+ * <p>This method can be used when the caller needs access to the {@code
+ * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
+ */
+ public Builder setContentDigestsToVerify(
+ Set<ContentDigestAlgorithm> contentDigestsToVerify) {
+ mContentDigestsToVerify = contentDigestsToVerify;
+ return this;
+ }
+
+ /**
+ * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
+ * from this instance.
+ *
+ * <note>{@link #verify()} will always verify the content digests for the APK, but this
+ * allows verification of the rotation minimum SDK version stripping attribute to be skipped
+ * for scenarios where this value may not have been parsed from a V3.1 signing block (such
+ * as when only {@link #parseSigners()} will be invoked.</note>
+ */
+ public Builder setFullVerification(boolean fullVerification) {
+ mFullVerification = fullVerification;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeVerifier build() {
+ int sigSchemeVersion;
+ switch (mBlockId) {
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+ mMinSdkVersion = Math.max(mMinSdkVersion,
+ V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
+ break;
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+ // V3.1 supports targeting an SDK version later than that of the initial release
+ // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
+ // rest of the range.
+ mMinSdkVersion = mMaxSdkVersion;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
+ mBlockId));
+ }
+ if (mResult == null) {
+ mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
+ }
+ if (mContentDigestsToVerify == null) {
+ mContentDigestsToVerify = new HashSet<>(1);
+ }
+
+ V3SchemeVerifier verifier = new V3SchemeVerifier(
+ mExecutor,
+ mApk,
+ mZipSections,
+ mContentDigestsToVerify,
+ mResult,
+ mMinSdkVersion,
+ mMaxSdkVersion,
+ mBlockId,
+ mOptionalRotationMinSdkVersion,
+ mFullVerification);
+ if (mApkSignatureSchemeV3Block != null) {
+ verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
+ }
+ return verifier;
+ }
}
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 73ba46f..2f9ecb3 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -17,17 +17,16 @@
package com.android.apksig.internal.apk.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
-import static com.android.apksig.internal.apk.v2.V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
-import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
-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.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.util.Pair;
@@ -44,6 +43,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
@@ -62,7 +62,6 @@ import java.util.Set;
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
* <p>
- * TODO(schfan): Add v4 unit tests
*/
public abstract class V4SchemeSigner {
/**
@@ -71,15 +70,33 @@ public abstract class V4SchemeSigner {
private V4SchemeSigner() {
}
+ public static class SignerConfig {
+ final public ApkSigningBlockUtils.SignerConfig v4Config;
+ final public ApkSigningBlockUtils.SignerConfig v41Config;
+
+ public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
+ List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
+ if (v4Configs == null || v4Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
+ }
+ if (v41Configs != null && v41Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
+ }
+ this.v4Config = v4Configs.get(0);
+ this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
+ }
+ }
+
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
- int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+ int minSdkVersion, boolean apkSigningBlockPaddingSupported,
+ boolean deterministicDsaSigning)
throws InvalidKeyException {
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
signingKey, minSdkVersion,
- apkSigningBlockPaddingSupported);
+ apkSigningBlockPaddingSupported, deterministicDsaSigning);
// Keeping only supported algorithms.
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
final SignatureAlgorithm algorithm = iter.next();
@@ -149,10 +166,10 @@ public abstract class V4SchemeSigner {
return Pair.of(signature, tree);
}
- private static V4Signature generateSignature(
- SignerConfig signerConfig,
+ private static V4Signature.SigningInfo generateSigningInfo(
+ ApkSigningBlockUtils.SignerConfig signerConfig,
V4Signature.HashingInfo hashingInfo,
- byte[] apkDigest, byte[] additionaData, long fileSize)
+ byte[] apkDigest, byte[] additionalData, long fileSize)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
CertificateEncodingException {
if (signerConfig.certificates.isEmpty()) {
@@ -169,9 +186,9 @@ public abstract class V4SchemeSigner {
final byte[] encodedCertificate = encodedCertificates.get(0);
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
- encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
+ encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
- final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+ final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
signingInfoNoSignature);
// Signing.
@@ -184,12 +201,33 @@ public abstract class V4SchemeSigner {
final int signatureAlgorithmId = signatures.get(0).getFirst();
final byte[] signature = signatures.get(0).getSecond();
- final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
- encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+ return new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
signature);
+ }
+
+ private static V4Signature generateSignature(
+ SignerConfig signerConfig,
+ V4Signature.HashingInfo hashingInfo,
+ byte[] apkDigest, byte[] additionalData, long fileSize)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+ CertificateEncodingException {
+ final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
+ hashingInfo, apkDigest, additionalData, fileSize);
+
+ final V4Signature.SigningInfos signingInfos;
+ if (signerConfig.v41Config != null) {
+ final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
+ generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
+ additionalData, fileSize).toByteArray());
+ signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
+ } else {
+ signingInfos = new V4Signature.SigningInfos(signingInfo);
+ }
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
- signingInfo.toByteArray());
+ signingInfos.toByteArray());
}
// Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
@@ -314,8 +352,6 @@ public abstract class V4SchemeSigner {
return bestDigest;
}
- // Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest
- // verification in framework works.
public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
switch (contentDigestAlgorithm) {
case CHUNKED_SHA256:
@@ -324,8 +360,9 @@ public abstract class V4SchemeSigner {
return 1;
case CHUNKED_SHA512:
return 2;
+ default:
+ return -1;
}
- return -1;
}
private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
index 0a8484b..c0a9013 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -90,20 +90,37 @@ public abstract class V4SchemeVerifier {
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
signature.hashingInfo);
- V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
- signature.signingInfo);
- final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+ V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
+ signature.signingInfos);
- // First, verify the signature over signedData.
- ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
- signingInfo, signedData);
- result.signers.add(signerInfo);
- if (result.containsErrors()) {
- return result;
+ final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
+
+ // Verify the primary signature over signedData.
+ {
+ V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
+ result.signers.add(signerInfo);
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Verify all subsequent signatures.
+ for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
+ V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+ signingInfoBlock.signingInfo);
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
+ if (result.containsErrors()) {
+ return result;
+ }
}
- // Second, check if the root hash and the tree are correct.
+ // Check if the root hash and the tree are correct.
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
if (!result.containsErrors()) {
result.verified = true;
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
index e36ed60..1eac5a2 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -22,6 +22,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
public class V4Signature {
public static final int CURRENT_VERSION = 2;
@@ -29,6 +31,8 @@ public class V4Signature {
public static final int HASHING_ALGORITHM_SHA256 = 1;
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+ public static final int MAX_SIGNING_INFOS_SIZE = 7168;
+
public static class HashingInfo {
public final int hashAlgorithm; // only 1 == SHA256 supported
public final byte log2BlockSize; // only 12 (block size 4096) supported now
@@ -82,7 +86,10 @@ public class V4Signature {
}
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
- ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
+ }
+
+ static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
byte[] apkDigest = readBytes(buffer);
byte[] certificate = readBytes(buffer);
byte[] additionalData = readBytes(buffer);
@@ -108,14 +115,93 @@ public class V4Signature {
}
}
- public final int version; // Always 2 for now.
+ public static class SigningInfoBlock {
+ public final int blockId;
+ public final byte[] signingInfo;
+
+ public SigningInfoBlock(int blockId, byte[] signingInfo) {
+ this.blockId = blockId;
+ this.signingInfo = signingInfo;
+ }
+
+ static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
+ int blockId = buffer.getInt();
+ byte[] signingInfo = readBytes(buffer);
+ return new SigningInfoBlock(blockId, signingInfo);
+ }
+
+ byte[] toByteArray() {
+ final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(this.blockId);
+ writeBytes(buffer, this.signingInfo);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfos {
+ public final SigningInfo signingInfo;
+ public final SigningInfoBlock[] signingInfoBlocks;
+
+ public SigningInfos(SigningInfo signingInfo) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = new SigningInfoBlock[0];
+ }
+
+ public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = signingInfoBlocks;
+ }
+
+ public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
+ if (!buffer.hasRemaining()) {
+ return new SigningInfos(signingInfo);
+ }
+ ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
+ while (buffer.hasRemaining()) {
+ signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
+ }
+ return new SigningInfos(signingInfo,
+ signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
+ }
+
+ byte[] toByteArray() {
+ byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
+ arrays[0] = this.signingInfo.toByteArray();
+ int size = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
+ size += arrays[i + 1].length;
+ }
+ if (size > MAX_SIGNING_INFOS_SIZE) {
+ throw new IllegalArgumentException(
+ "Combined SigningInfos length exceeded limit of 7K: " + size);
+ }
+
+ // Combine all arrays into one.
+ byte[] result = Arrays.copyOf(arrays[0], size);
+ int offset = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
+ offset += arrays[i + 1].length;
+ }
+ return result;
+ }
+ }
+
+ // Always 2 for now.
+ public final int version;
public final byte[] hashingInfo;
- public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
+ // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
+ // Passed as-is to the kernel. Can be retrieved later.
+ public final byte[] signingInfos;
- V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
+ V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
this.version = version;
this.hashingInfo = hashingInfo;
- this.signingInfo = signingInfo;
+ this.signingInfos = signingInfos;
}
static V4Signature readFrom(InputStream stream) throws IOException {
@@ -131,10 +217,10 @@ public class V4Signature {
public void writeTo(OutputStream stream) throws IOException {
writeIntLE(stream, this.version);
writeBytes(stream, this.hashingInfo);
- writeBytes(stream, this.signingInfo);
+ writeBytes(stream, this.signingInfos);
}
- static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+ static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
final int size =
4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
index d4a6fb6..160dc4e 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -507,23 +507,22 @@ public final class Asn1BerParser {
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
- try {
- return value.intValueExact();
- } catch (ArithmeticException e) {
+ if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
- String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e);
+ String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
}
+ return value.intValue();
}
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
- try {
- return value.longValueExact();
- } catch (ArithmeticException e) {
+ if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
- String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value),
- e);
+ String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
}
+ return value.longValue();
}
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
index c27c487..9712767 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
@@ -16,6 +16,7 @@
package com.android.apksig.internal.pkcs7;
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
@@ -77,7 +78,8 @@ public class AlgorithmIdentifier {
* when signing with the specified key and digest algorithm.
*/
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
- PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+ PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
String jcaDigestPrefixForSigAlg;
switch (digestAlgorithm) {
@@ -91,7 +93,7 @@ public class AlgorithmIdentifier {
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withRSA",
new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
@@ -115,7 +117,9 @@ public class AlgorithmIdentifier {
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
- return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
+ String signingAlgorithmName =
+ jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
+ return Pair.of(signingAlgorithmName, sigAlgId);
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withECDSA",
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 4ef67c7..bbead72 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -24,9 +24,15 @@ public abstract class AndroidSdkVersion {
/** Hidden constructor to prevent instantiation. */
private AndroidSdkVersion() {}
+ /** Android 1.0 */
+ public static final int INITIAL_RELEASE = 1;
+
/** Android 2.3. */
public static final int GINGERBREAD = 9;
+ /** Android 3.0 */
+ public static final int HONEYCOMB = 11;
+
/** Android 4.3. The revenge of the beans. */
public static final int JELLY_BEAN_MR2 = 18;
@@ -48,6 +54,18 @@ public abstract class AndroidSdkVersion {
/** Android P. */
public static final int P = 28;
+ /** Android Q. */
+ public static final int Q = 29;
+
/** Android R. */
public static final int R = 30;
+
+ /** Android S. */
+ public static final int S = 31;
+
+ /** Android Sv2. */
+ public static final int Sv2 = 32;
+
+ /** Android T. */
+ public static final int T = 33;
}
diff --git a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
index 8f9e1fd..2a890f6 100644
--- a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
+++ b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
@@ -34,6 +34,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
+
import javax.security.auth.x500.X500Principal;
/**
@@ -210,6 +211,7 @@ public class DelegatingX509Certificate extends X509Certificate {
}
@Override
+ @SuppressWarnings("AndroidJdkLibsChecker")
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
mDelegate.verify(key, sigProvider);
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 488df89..81026ba 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -16,6 +16,8 @@
package com.android.apksig.internal.util;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
@@ -24,9 +26,10 @@ import com.android.apksig.util.DataSources;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.util.ArrayList;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+
+import java.util.ArrayList;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Phaser;
@@ -76,7 +79,7 @@ public class VerityTreeBuilder implements AutoCloseable {
private final ExecutorService mExecutor =
new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
- 0L, TimeUnit.MILLISECONDS,
+ 0L, MILLISECONDS,
new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
new ThreadPoolExecutor.CallerRunsPolicy());
@@ -220,9 +223,6 @@ public class VerityTreeBuilder implements AutoCloseable {
final long size = dataSource.size();
final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
- /** Actual number of workers. */
- final int parallelism = Math.max(
- Math.min(chunks / MIN_CHUNKS_PER_WORKER, DIGEST_PARALLELISM), 1);
/** Single IO operation size, in chunks. */
final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
diff --git a/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/src/main/java/com/android/apksig/internal/zip/EocdRecord.java
index 9c531f4..d2000b4 100644
--- a/src/main/java/com/android/apksig/internal/zip/EocdRecord.java
+++ b/src/main/java/com/android/apksig/internal/zip/EocdRecord.java
@@ -45,4 +45,13 @@ public class EocdRecord {
ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset);
return result;
}
+
+ public static ByteBuffer createWithPaddedComment(ByteBuffer original, int padding) {
+ ByteBuffer result = ByteBuffer.allocate((int) original.remaining() + padding);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(original.slice());
+ result.rewind();
+ ZipUtils.updateZipEocdCommentLen(result);
+ return result;
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
index 272015a..1c2e82c 100644
--- a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
+++ b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
@@ -16,12 +16,18 @@
package com.android.apksig.internal.zip;
+import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
@@ -64,6 +70,20 @@ public abstract class ZipUtils {
}
/**
+ * Sets the length of EOCD comment.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE;
+ setUnsignedInt16(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET,
+ commentLen);
+ }
+
+ /**
* Returns the offset of the start of the ZIP Central Directory in the archive.
*
* <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
@@ -247,6 +267,46 @@ public abstract class ZipUtils {
return buffer.getShort() & 0xffff;
}
+ public static List<CentralDirectoryRecord> parseZipCentralDirectory(
+ DataSource apk,
+ ZipSections apkSections)
+ throws IOException, ApkFormatException {
+ // Read the ZIP Central Directory
+ long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+ if (cdSizeBytes > Integer.MAX_VALUE) {
+ throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+ }
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+ cd.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Parse the ZIP Central Directory
+ int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+ List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+ for (int i = 0; i < expectedCdRecordCount; i++) {
+ CentralDirectoryRecord cdRecord;
+ int offsetInsideCd = cd.position();
+ try {
+ cdRecord = CentralDirectoryRecord.getRecord(cd);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(
+ "Malformed ZIP Central Directory record #" + (i + 1)
+ + " at file offset " + (cdOffset + offsetInsideCd),
+ e);
+ }
+ String entryName = cdRecord.getName();
+ if (entryName.endsWith("/")) {
+ // Ignore directory entries
+ continue;
+ }
+ cdRecords.add(cdRecord);
+ }
+ // There may be more data in Central Directory, but we don't warn or throw because Android
+ // ignores unused CD data.
+
+ return cdRecords;
+ }
+
static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
if ((value < 0) || (value > 0xffff)) {
throw new IllegalArgumentException("uint16 value of out range: " + value);
diff --git a/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
index b0e69e4..74017f8 100644
--- a/src/main/java/com/android/apksig/util/RunnablesExecutor.java
+++ b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
@@ -16,16 +16,17 @@
package com.android.apksig.util;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Phaser;
import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
public interface RunnablesExecutor {
- RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+ static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
- RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
+ static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors());
private final int QUEUE_SIZE = 4;
@@ -33,7 +34,7 @@ public interface RunnablesExecutor {
public void execute(RunnablesProvider provider) {
final ExecutorService mExecutor =
new ThreadPoolExecutor(PARALLELISM, PARALLELISM,
- 0L, TimeUnit.MILLISECONDS,
+ 0L, MILLISECONDS,
new ArrayBlockingQueue<>(QUEUE_SIZE),
new ThreadPoolExecutor.CallerRunsPolicy());
diff --git a/src/main/java/com/android/apksig/zip/ZipSections.java b/src/main/java/com/android/apksig/zip/ZipSections.java
new file mode 100644
index 0000000..17bce05
--- /dev/null
+++ b/src/main/java/com/android/apksig/zip/ZipSections.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.zip;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base representation of an APK's zip sections containing the central directory's offset, the size
+ * of the central directory in bytes, the number of records in the central directory, the offset
+ * of the end of central directory, and a ByteBuffer containing the end of central directory
+ * contents.
+ */
+public class ZipSections {
+ private final long mCentralDirectoryOffset;
+ private final long mCentralDirectorySizeBytes;
+ private final int mCentralDirectoryRecordCount;
+ private final long mEocdOffset;
+ private final ByteBuffer mEocd;
+
+ public ZipSections(
+ long centralDirectoryOffset,
+ long centralDirectorySizeBytes,
+ int centralDirectoryRecordCount,
+ long eocdOffset,
+ ByteBuffer eocd) {
+ mCentralDirectoryOffset = centralDirectoryOffset;
+ mCentralDirectorySizeBytes = centralDirectorySizeBytes;
+ mCentralDirectoryRecordCount = centralDirectoryRecordCount;
+ mEocdOffset = eocdOffset;
+ mEocd = eocd;
+ }
+
+ /**
+ * Returns the start offset of the ZIP Central Directory. This value is taken from the
+ * ZIP End of Central Directory record.
+ */
+ public long getZipCentralDirectoryOffset() {
+ return mCentralDirectoryOffset;
+ }
+
+ /**
+ * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
+ * ZIP End of Central Directory record.
+ */
+ public long getZipCentralDirectorySizeBytes() {
+ return mCentralDirectorySizeBytes;
+ }
+
+ /**
+ * Returns the number of records in the ZIP Central Directory. This value is taken from the
+ * ZIP End of Central Directory record.
+ */
+ public int getZipCentralDirectoryRecordCount() {
+ return mCentralDirectoryRecordCount;
+ }
+
+ /**
+ * Returns the start offset of the ZIP End of Central Directory record. The record extends
+ * until the very end of the APK.
+ */
+ public long getZipEndOfCentralDirectoryOffset() {
+ return mEocdOffset;
+ }
+
+ /**
+ * Returns the contents of the ZIP End of Central Directory.
+ */
+ public ByteBuffer getZipEndOfCentralDirectory() {
+ return mEocd;
+ }
+} \ No newline at end of file
diff --git a/src/test/java/com/android/apksig/AllTests.java b/src/test/java/com/android/apksig/AllTests.java
index 4a9243d..3cb1052 100644
--- a/src/test/java/com/android/apksig/AllTests.java
+++ b/src/test/java/com/android/apksig/AllTests.java
@@ -24,6 +24,7 @@ import org.junit.runners.Suite;
ApkSignerTest.class,
ApkVerifierTest.class,
SigningCertificateLineageTest.class,
+ SourceStampVerifierTest.class,
com.android.apksig.apk.AllTests.class,
com.android.apksig.internal.AllTests.class,
com.android.apksig.util.AllTests.class,
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 4d45892..6473440 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -21,6 +21,7 @@ import static com.android.apksig.apk.ApkUtils.findZipSections;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -31,41 +32,52 @@ 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.SignatureInfo;
-import com.android.apksig.internal.apk.stamp.SourceStampSigner;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
-import com.android.apksig.internal.apk.v2.V2SchemeSigner;
-import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
-import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
-import com.android.apksig.util.ReadableDataSink;
import com.android.apksig.zip.ZipFormatException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.ByteBuffer;
-import java.nio.channels.ByteChannel;
import java.nio.file.Files;
-import java.nio.file.StandardOpenOption;
+import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
+import java.security.Security;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
@RunWith(JUnit4.class)
public class ApkSignerTest {
@@ -78,9 +90,11 @@ public class ApkSignerTest {
// All signers with the same prefix and an _X suffix were signed with the private key of the
// (X-1) signer.
- private static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
- private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
- private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
+ static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048";
+ static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
+ static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
+
+ private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
// 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 =
@@ -89,6 +103,14 @@ public class ApkSignerTest {
private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
"rsa-2048-lineage-2-signers";
+ // These are the ID and value of an extra signature block within the APK signing block that
+ // can be preserved through the setOtherSignersSignaturesPreserved API.
+ private final int EXTRA_BLOCK_ID = 0x7e57c0de;
+ private final byte[] EXTRA_BLOCK_VALUE = {0, 1, 2, 3, 4, 5, 6, 7};
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
public static void main(String[] params) throws Exception {
File outDir = (params.length > 0) ? new File(params[0]) : new File(".");
generateGoldenFiles(outDir);
@@ -100,8 +122,10 @@ public class ApkSignerTest {
+ ApkSignerTest.class.getSimpleName()
+ " into "
+ outDir);
- if (!outDir.mkdirs()) {
- throw new IOException("Failed to create directory: " + outDir);
+ if (!outDir.exists()) {
+ if (!outDir.mkdirs()) {
+ throw new IOException("Failed to create directory: " + outDir);
+ }
}
List<ApkSigner.SignerConfig> rsa2048SignerConfig =
Collections.singletonList(
@@ -133,21 +157,24 @@ public class ApkSignerTest {
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
signGolden(
"golden-legacy-aligned-in.apk",
new File(outDir, "golden-legacy-aligned-v1-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
signGolden(
"golden-aligned-in.apk",
new File(outDir, "golden-aligned-v1-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
signGolden(
"golden-unaligned-in.apk",
@@ -200,7 +227,9 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
+
signGolden(
"golden-legacy-aligned-in.apk",
new File(outDir, "golden-legacy-aligned-v3-lineage-out.apk"),
@@ -208,6 +237,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -216,6 +246,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -268,6 +299,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-legacy-aligned-in.apk",
@@ -276,6 +308,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -284,6 +317,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -314,6 +348,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-legacy-aligned-in.apk",
@@ -322,6 +357,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
"golden-aligned-in.apk",
@@ -330,6 +366,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
signGolden(
@@ -348,6 +385,27 @@ public class ApkSignerTest {
"original.apk",
new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(24));
+ signGolden(
+ "original.apk",
+ new File(outDir, "golden-rsa-verity-out.apk"),
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
+ signGolden(
+ "original.apk",
+ new File(outDir, "golden-file-size-aligned.apk"),
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setAlignFileSize(true));
+ signGolden(
+ "pinsapp-unsigned.apk",
+ new File(outDir, "golden-pinsapp-signed.apk"),
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
}
private static void signGolden(
@@ -356,7 +414,13 @@ public class ApkSignerTest {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName)));
- apkSignerBuilder.setInputApk(in).setOutputApk(outFile).build().sign();
+ apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
+
+ File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig");
+ apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig);
+ apkSignerBuilder.setV4ErrorReportingEnabled(true);
+
+ apkSignerBuilder.build().sign();
}
@Test
@@ -388,7 +452,8 @@ public class ApkSignerTest {
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
assertGolden(
"golden-unaligned-in.apk",
"golden-unaligned-v2-out.apk",
@@ -410,6 +475,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-unaligned-in.apk",
@@ -432,6 +498,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-unaligned-in.apk",
@@ -447,6 +514,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
// Uncompressed entries in this input file are aligned by zero-padding the "extra" field, as
@@ -463,7 +531,8 @@ public class ApkSignerTest {
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
assertGolden(
"golden-legacy-aligned-in.apk",
"golden-legacy-aligned-v2-out.apk",
@@ -485,6 +554,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-legacy-aligned-in.apk",
@@ -507,6 +577,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-legacy-aligned-in.apk",
@@ -522,6 +593,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
// Uncompressed entries in this input file are aligned by padding the "extra" field, as
@@ -537,7 +609,8 @@ public class ApkSignerTest {
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
- .setV3SigningEnabled(false));
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
assertGolden(
"golden-aligned-in.apk",
"golden-aligned-v2-out.apk",
@@ -559,6 +632,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-aligned-in.apk",
@@ -581,6 +655,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
assertGolden(
"golden-aligned-in.apk",
@@ -596,6 +671,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
}
@@ -627,6 +703,35 @@ public class ApkSignerTest {
}
@Test
+ public void testVerityEnabled_Golden() throws Exception {
+ List<ApkSigner.SignerConfig> rsaSignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertGolden(
+ "original.apk",
+ "golden-rsa-verity-out.apk",
+ new ApkSigner.Builder(rsaSignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
+ }
+
+ @Test
+ public void testAlignFileSize_Golden() throws Exception {
+ List<ApkSigner.SignerConfig> rsaSignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ String goldenOutput = "golden-file-size-aligned.apk";
+ assertGolden(
+ "original.apk",
+ goldenOutput,
+ new ApkSigner.Builder(rsaSignerConfig).setAlignFileSize(true));
+ assertTrue(Resources.toByteArray(getClass(), goldenOutput).length % 4096 == 0);
+ }
+
+ @Test
public void testRsaSignedVerifies() throws Exception {
List<ApkSigner.SignerConfig> signers =
Collections.singletonList(
@@ -634,7 +739,7 @@ public class ApkSignerTest {
String in = "original.apk";
// Sign so that the APK is guaranteed to verify on API Level 1+
- DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
assertVerified(verifyForMinSdkVersion(out, 1));
// Sign so that the APK is guaranteed to verify on API Level 18+
@@ -652,7 +757,7 @@ public class ApkSignerTest {
String in = "original.apk";
// Sign so that the APK is guaranteed to verify on API Level 1+
- DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
assertVerified(verifyForMinSdkVersion(out, 1));
// Sign so that the APK is guaranteed to verify on API Level 21+
@@ -663,15 +768,58 @@ public class ApkSignerTest {
verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
}
+
+ @Test
+ public void testDeterministicDsaSignedVerifies() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ // Sign so that the APK is guaranteed to verify on API Level 1+
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+ assertVerified(verifyForMinSdkVersion(out, 1));
+
+ // Sign so that the APK is guaranteed to verify on API Level 21+
+ out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(21));
+ assertVerified(verifyForMinSdkVersion(out, 21));
+ // Does not verify on API Level 20 because DSA with SHA-256 not supported
+ assertVerificationFailure(
+ verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
+ @Test
+ public void testDeterministicDsaSigningIsDeterministic() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signers).setMinSdkVersion(1);
+ File first = sign(in, apkSignerBuilder);
+ File second = sign(in, apkSignerBuilder);
+
+ assertFileContentsEqual(first, second);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
@Test
public void testEcSignedVerifies() throws Exception {
List<ApkSigner.SignerConfig> signers =
- Collections.singletonList(getDefaultSignerConfigFromResources("ec-p256"));
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
String in = "original.apk";
// NOTE: EC APK signatures are not supported prior to API Level 18
// Sign so that the APK is guaranteed to verify on API Level 18+
- DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18));
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18));
assertVerified(verifyForMinSdkVersion(out, 18));
// Does not verify on API Level 17 because EC not supported
assertVerificationFailure(
@@ -839,6 +987,7 @@ public class ApkSignerTest {
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
.setSigningCertificateLineage(lineage));
// Verifies that an intermediate signer in the lineage is not sufficient to satisfy the
@@ -903,14 +1052,13 @@ public class ApkSignerTest {
Arrays.asList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
- DataSource out =
+ File out =
sign(
"original.apk",
new ApkSigner.Builder(signerConfigs)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
- SigningCertificateLineage lineageFromApk =
- SigningCertificateLineage.readFromApkDataSource(out);
+ SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkFile(out);
assertTrue(
"The first signer was not in the lineage from the signed APK",
lineageFromApk.isSignerInLineage((firstSigner)));
@@ -933,7 +1081,7 @@ public class ApkSignerTest {
getDefaultSignerConfigFromResources(
FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
@@ -960,13 +1108,15 @@ public class ApkSignerTest {
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
assertThrows(
- IllegalStateException.class,
- () -> sign("original.apk",
- new ApkSigner.Builder(Collections.singletonList(signer))
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(false)
- .setV3SigningEnabled(false)
- .setV4SigningEnabled(true)));
+ IllegalStateException.class,
+ () ->
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(Collections.singletonList(signer))
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(true)));
}
@Test
@@ -980,28 +1130,106 @@ public class ApkSignerTest {
messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded());
byte[] expectedStampCertificateDigest = messageDigest.digest();
- DataSource signedApk =
+ File signedApkFile =
sign(
"original.apk",
new ApkSigner.Builder(signers)
.setV1SigningEnabled(true)
.setSourceStampSignerConfig(sourceStampSigner));
- ApkUtils.ZipSections zipSections = findZipSections(signedApk);
- List<CentralDirectoryRecord> cdRecords =
- V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections);
- CentralDirectoryRecord stampCdRecord = null;
- for (CentralDirectoryRecord cdRecord : cdRecords) {
- if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
- stampCdRecord = cdRecord;
- break;
+ try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) {
+ DataSource signedApk = DataSources.asDataSource(f, 0, f.length());
+
+ ApkUtils.ZipSections zipSections = findZipSections(signedApk);
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections);
+ CentralDirectoryRecord stampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ stampCdRecord = cdRecord;
+ break;
+ }
}
+ assertNotNull(stampCdRecord);
+ byte[] actualStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
+ assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
}
- assertNotNull(stampCdRecord);
- byte[] actualStampCertificateDigest =
- LocalFileRecord.getUncompressedData(
- signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
- assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_sameSourceStamp() throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ File signedApk =
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_differentSourceStamp() throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ Exception exception =
+ assertThrows(
+ ApkFormatException.class,
+ () ->
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner)));
+ assertEquals(
+ String.format(
+ "Cannot generate SourceStamp. APK contains an existing entry with the"
+ + " name: %s, and it is different than the provided source stamp"
+ + " certificate",
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME),
+ exception.getMessage());
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_differentSourceStamp_forceOverwrite()
+ throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ File signedApk =
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setForceSourceStampOverwrite(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
}
@Test
@@ -1010,7 +1238,7 @@ public class ApkSignerTest {
Collections.singletonList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- DataSource signedApk =
+ File signedApkFile =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
@@ -1018,45 +1246,44 @@ public class ApkSignerTest {
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
- ApkSigningBlockUtils.Result result =
- new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
- assertThrows(
- ApkSigningBlockUtils.SignatureNotFoundException.class,
- () ->
- ApkSigningBlockUtils.findSignature(
- signedApk,
- zipSections,
- ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
- result));
+ try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) {
+ DataSource signedApk = DataSources.asDataSource(f, 0, f.length());
+
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ assertThrows(
+ ApkSigningBlockUtils.SignatureNotFoundException.class,
+ () ->
+ ApkSigningBlockUtils.findSignature(
+ signedApk,
+ zipSections,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+ result));
+ }
}
@Test
- public void testSignApk_stampBlock_whenNoV2V3SignaturePresent() throws Exception {
+ public void testSignApk_stampBlock_whenV1SignaturePresent() throws Exception {
List<ApkSigner.SignerConfig> signersList =
Collections.singletonList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
.setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
.setSourceStampSignerConfig(sourceStampSigner));
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
- ApkSigningBlockUtils.Result result =
- new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
- assertThrows(
- ApkSigningBlockUtils.SignatureNotFoundException.class,
- () ->
- ApkSigningBlockUtils.findSignature(
- signedApk,
- zipSections,
- ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
- result));
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
}
@Test
@@ -1067,20 +1294,18 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
- .setV1SigningEnabled(true)
+ .setV1SigningEnabled(false)
.setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
.setSourceStampSignerConfig(sourceStampSigner));
- SignatureInfo signatureInfo =
- getSignatureInfoFromApk(
- signedApk,
- ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
- SourceStampSigner.SOURCE_STAMP_BLOCK_ID);
- assertNotNull(signatureInfo.signatureBlock);
+ ApkVerifier.Result sourceStampVerificationResult =
+ verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
}
@Test
@@ -1091,31 +1316,940 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
- .setV1SigningEnabled(true)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
.setV3SigningEnabled(true)
.setSourceStampSignerConfig(sourceStampSigner));
- SignatureInfo signatureInfo =
- getSignatureInfoFromApk(
- signedApk,
- ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
- SourceStampSigner.SOURCE_STAMP_BLOCK_ID);
- assertNotNull(signatureInfo.signatureBlock);
+ ApkVerifier.Result sourceStampVerificationResult =
+ verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_stampBlock_withStampLineage() throws Exception {
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ SigningCertificateLineage sourceStampLineage =
+ Resources.toSigningCertificateLineage(
+ getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner)
+ .setSourceStampSigningCertificateLineage(sourceStampLineage));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersion= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_Pinlist() throws Exception {
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ assertGolden(
+ "pinsapp-unsigned.apk",
+ "golden-pinsapp-signed.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
+ assertTrue("pinlist.meta file must be in the signed APK.",
+ resourceZipFileContains("golden-pinsapp-signed.apk", "pinlist.meta"));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_extraSigBlock_signatureAppended()
+ throws Exception {
+ // The DefaultApkSignerEngine contains support to append a signature to an existing
+ // signing block; any existing signature blocks within the APK signing block should be
+ // left intact except for the original verity padding block (since this is regenerated) and
+ // the source stamp. This test verifies that an extra signature block is still in
+ // the APK signing block after appending a V2 signature.
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v1Only_signatureAppended() throws Exception {
+ // This test verifies appending an additional V1 signature to an existing V1 signer behaves
+ // similar to jarsigner where the APK is then verified as signed by both signers.
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v1-only-with-rsa-2048.apk",
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v3OnlyDifferentSigner_throwsException()
+ throws Exception {
+ // The V3 Signature Scheme only supports a single signer; if an attempt is made to append
+ // a different signer to a V3 signature then an exception should be thrown.
+ // The APK used for this test is signed with the ec-p256 signer so use the rsa-2048 to
+ // attempt to append a different signature.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertThrows(IllegalStateException.class, () ->
+ sign("v3-only-with-stamp.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true))
+ );
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v2OnlyAppendV2V3SameSigner_signatureAppended()
+ throws Exception {
+ // A V2 and V3 signature can be appended to an existing V2 signature if the same signer is
+ // used to resign the APK; this could be used in a case where an APK was previously signed
+ // with just the V2 signature scheme along with additional non-APK signing scheme signature
+ // blocks and the signer wanted to preserve those existing blocks.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v2OnlyAppendV3SameSigner_throwsException()
+ throws Exception {
+ // A V3 only signature cannot be appended to an existing V2 signature, even when using the
+ // same signer, since the V2 signature would then not contain the stripping protection for
+ // the V3 signature. If the same signer is being used then the signer should be configured
+ // to resign using the V2 signature scheme as well as the V3 signature scheme.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertThrows(IllegalStateException.class, () ->
+ sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true)));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v1v2IndividuallySign_signaturesAppended()
+ throws Exception {
+ // One of the primary requirements for appending signatures is when an APK has already
+ // released with two signers; with the minimum signature scheme v2 requirement for target
+ // SDK version 30+ each signer must be able to append their signature to the existing
+ // signature block. This test verifies an APK with appended signatures verifies as expected
+ // after a series of appending V1 and V2 signatures.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ // When two parties are signing an APK the first must sign with both V1 and V2; this will
+ // write the stripping-protection attribute to the V1 signature.
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
+
+ // The second party can then append their signature with both the V1 and V2 signature; this
+ // will invalidate the V2 signature of the initial signer since the APK itself will be
+ // modified with this signers V1 / jar signature.
+ signedApk = sign(signedApk,
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ // The first party will then need to resign with just the V2 signature after its previous
+ // signature was invalidated by the V1 signature of the second signer; however since this
+ // signature is appended its previous V2 signature should be removed from the signature
+ // block and replaced with this new signature while preserving the V2 signature of the
+ // other signer.
+ signedApk = sign(signedApk,
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_lessThanT_noV31Block() throws Exception {
+ // The V3.1 signing block is intended to allow APK signing key rotation to target T+, but
+ // a minimum SDK version can be explicitly set for rotation; if it is less than T than
+ // the rotated key will be included in the V3.0 block.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkMinRotationP = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationP = verify(signedApkMinRotationP, null);
+ // The V3.1 signature scheme was introduced in T; specifying an older SDK version as the
+ // minimum for rotation should cause the APK to still be signed with rotation in the V3.0
+ // signing block.
+ File signedApkMinRotationS = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.S)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationS = verify(signedApkMinRotationS, null);
+
+ assertVerified(resultMinRotationP);
+ assertFalse(resultMinRotationP.isVerifiedUsingV31Scheme());
+ assertEquals(1, resultMinRotationP.getV3SchemeSigners().size());
+ assertResultContainsSigners(resultMinRotationP, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertVerified(resultMinRotationS);
+ assertFalse(resultMinRotationS.isVerifiedUsingV31Scheme());
+ // While rotation is targeting S, signer blocks targeting specific SDK versions have not
+ // been tested in previous platform releases; ensure only a single signer block with the
+ // rotated key is in the V3 block.
+ assertEquals(1, resultMinRotationS.getV3SchemeSigners().size());
+ assertResultContainsSigners(resultMinRotationS, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_TAndLater_v31Block() throws Exception {
+ // When T or later is specified as the minimum SDK version for rotation, then a new V3.1
+ // signing block should be created with the new rotated key, and the V3.0 signing block
+ // should still be signed with the original key.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkMinRotationT = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationT = verify(signedApkMinRotationT, null);
+ // The API level for a release after T is not yet defined, so for now treat it as T + 1.
+ File signedApkMinRotationU = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T + 1)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultMinRotationU = verify(signedApkMinRotationU, null);
+
+ assertVerified(resultMinRotationT);
+ assertTrue(resultMinRotationT.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultMinRotationT, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(resultMinRotationT, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T);
+ assertVerified(resultMinRotationU);
+ assertTrue(resultMinRotationU.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultMinRotationU, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(resultMinRotationU, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T + 1);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTNoOriginalSigner_fails() throws Exception {
+ // Similar to the V1 and V2 signatures schemes, if an app is targeting P or later with
+ // rotation targeting T, the original signer must be provided so that it can be used in the
+ // V3.0 signing block; if it is not provided the signer should throw an Exception.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+ .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(28)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage)));
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTAndApkMinSdkT_onlySignsV3Block()
+ throws Exception {
+ // A V3.1 signing block should only exist alongside a V3.0 signing block; if an APK's
+ // min SDK version is greater than or equal to the SDK version for rotation then the
+ // original signer should not be required, and the rotated signing key should be in
+ // a V3.0 signing block.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List
+ .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+ .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verifyForMinSdkVersion(signedApk,
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT);
+
+ assertVerified(result);
+ assertFalse(result.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testSetMinSdkVersionForRotation_targetTWithSourceStamp_noWarnings()
+ throws Exception {
+ // Source stamp verification will report a warning if a stamp signature is not found for any
+ // of the APK Signature Schemes used to sign the APK. This test verifies an APK signed with
+ // a rotated key in the v3.1 block and a source stamp successfully verifies, including the
+ // source stamp, without any warnings.
+ ApkSigner.SignerConfig rsa2048OriginalSignerConfig = getDefaultSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ rsa2048OriginalSignerConfig,
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage)
+ .setSourceStampSignerConfig(rsa2048OriginalSignerConfig));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T);
+ assertSourceStampVerified(signedApk, result);
+ }
+
+ @Test
+ public void testSetRotationTargetsDevRelease_target34_v30SignerTargetsAtLeast34()
+ throws Exception {
+ // During development of a new platform release the new platform will use the SDK version
+ // of the previously released platform, so in order to test rotation on a new platform
+ // release it must target the SDK version of the previous platform. However an APK signed
+ // with the v3.1 signature scheme and targeting rotation on the previous platform release X
+ // would still use rotation if that APK were installed on a device running release version
+ // X. To support targeting rotation on the main branch, the v3.1 signature scheme supports
+ // a rotation-targets-dev-release attribute; this allows the APK to use the v3.1 signer
+ // block on a development platform with SDK version X while a release platform X will
+ // skip this signer block when it sees this additional attribute. To ensure that the APK
+ // will still target the released platform X, the v3.0 signer must have a maxSdkVersion
+ // of at least X for the signer.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+ int rotationMinSdkVersion = V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + 1;
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(rotationMinSdkVersion)
+ .setSigningCertificateLineage(lineage)
+ .setRotationTargetsDevRelease(true));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease());
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertTrue(result.getV3SchemeSigners().get(0).getMaxSdkVersion() >= rotationMinSdkVersion);
+ }
+
+ @Test
+ public void testV3_rotationMinSdkVersionLessThanTV3Only_origSignerNotRequired()
+ throws Exception {
+ // The v3.1 signature scheme allows a rotation-min-sdk-version be specified to target T+
+ // for rotation; however if this value is less than the expected SDK version of T, then
+ // apksig should just use the rotated signing key in the v3.0 block. An APK that targets
+ // P+ that wants to use rotation in the v3.0 signing block should only need to provide
+ // the rotated signing key and lineage; this test ensures this behavior when the
+ // rotation-min-sdk-version is set to a value > P and < T.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApkRotationOnQ = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersion(AndroidSdkVersion.P)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.Q)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result resultRotationOnQ = verify(signedApkRotationOnQ, AndroidSdkVersion.P);
+
+ assertVerified(resultRotationOnQ);
+ assertEquals(1, resultRotationOnQ.getV3SchemeSigners().size());
+ assertFalse(resultRotationOnQ.isVerifiedUsingV31Scheme());
+ assertResultContainsSigners(resultRotationOnQ, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionEqualsMinSdkVersion_v3SignerPresent()
+ throws Exception {
+ // The SDK version for Sv2 (32) is used as the minSdkVersion for the V3.1 signature
+ // scheme to allow rotation to target the T development platform; this will be updated
+ // to the real SDK version of T once its SDK is finalized. This test verifies if a
+ // package has Sv2 as its minSdkVersion, the signing can complete as expected with the
+ // v3 block signed by the original signer and targeting just Sv2, and the v3.1 block
+ // signed by the rotated signer and targeting the dev release of Sv2 and all later releases.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original-minSdk32.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionTWithoutLineage_v30VerificationSucceeds()
+ throws Exception {
+ // apksig allows setting a rotation-min-sdk-version without providing a rotated signing
+ // key / lineage; however in the absence of rotation, the rotation-min-sdk-version should
+ // be a no-op, and the stripping protection attribute should not be written to the v3.0
+ // signer.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertFalse(result.isVerifiedUsingV31Scheme());
+ assertTrue(result.isVerifiedUsingV3Scheme());
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionDefault_rotationTargetsT() throws Exception {
+ // The v3.1 signature scheme was introduced in T to allow developers to target T+ for
+ // rotation due to known issues with rotation on previous platform releases. This test
+ // verifies an APK signed with a rotated signing key defaults to the original signing
+ // key used in the v3 signing block for pre-T devices, and the rotated signing key used
+ // in the v3.1 signing block for T+ devices.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV3Scheme());
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion());
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME,
+ AndroidSdkVersion.T);
+ }
+
+ @Test
+ public void testV31_rotationMinSdkVersionP_rotationTargetsP() throws Exception {
+ // While the V3.1 signature scheme will target T by default, a package that has
+ // previously rotated can provide a rotation-min-sdk-version less than T to continue
+ // using the rotated signing key in the v3.0 block.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV3Scheme());
+ assertFalse(result.isVerifiedUsingV31Scheme());
+ }
+
+ @Test
+ public void testV4_rotationMinSdkVersionLessThanT_signatureOnlyHasRotatedSigner()
+ throws Exception {
+ // To support SDK version targeting in the v3.1 signature scheme, apksig added a
+ // rotation-min-sdk-version option to allow the caller to specify the level from which
+ // the rotated signer should be used. A value less than T should result in a single
+ // rotated signer in the V3 block (along with the corresponding lineage), and the V4
+ // signature should use this signer.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.P)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsV4Signers(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testV4_rotationMinSdkVersionT_signatureHasOrigAndRotatedKey() throws Exception {
+ // When an APK is signed with a rotated key and the rotation-min-sdk-version X is set to T+,
+ // a V3.1 block will be signed with the rotated signing key targeting X and later, and
+ // a V3.0 block will be signed with the original signing key targeting P - X-1. The
+ // V4 signature should contain both the original signing key and the rotated signing
+ // key; this ensures if an APK is installed on a device running an SDK version less than X,
+ // the V4 signature will be verified using the original signing key which will be the only
+ // signing key visible to the platform.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(true)
+ .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+ .setSigningCertificateLineage(lineage));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertResultContainsV4Signers(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void
+ testSourceStampTimestamp_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 by default.
+ ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setSourceStampSignerConfig(rsa2048SignerConfig));
+ ApkVerifier.Result result = verify(signedApk, null);
+
+ assertSourceStampVerified(signedApk, result);
+ long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds();
+ assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0);
+ }
+
+ @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}.
+ */
+ private static void assertSigningBlockContains(File signedApk,
+ Pair<byte[], Integer> expectedBlock) throws Exception {
+ try (RandomAccessFile apkFile = new RandomAccessFile(signedApk, "r")) {
+ ApkUtils.ApkSigningBlock apkSigningBlock = ApkUtils.findApkSigningBlock(
+ DataSources.asDataSource(apkFile));
+ List<Pair<byte[], Integer>> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock.getContents());
+ for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond().equals(expectedBlock.getSecond())) {
+ if (Arrays.equals(signatureBlock.getFirst(), expectedBlock.getFirst())) {
+ return;
+ }
+ }
+ }
+ fail(String.format(
+ "The APK signing block did not contain the expected block with ID %08x",
+ expectedBlock.getSecond()));
+ }
+ }
+
+ /**
+ * Asserts the provided verification {@code result} contains the expected {@code signers} for
+ * each scheme that was used to verify the APK's signature.
+ */
+ static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
+ throws Exception {
+ assertResultContainsSigners(result, false, signers);
+ }
+
+ /**
+ * Asserts the provided verification {@code result} contains the expected {@code signers} for
+ * each scheme that was used to verify the APK's signature; if {@code rotationExpected} is set
+ * to {@code true}, then the first element in {@code signers} is treated as the expected
+ * original signer for any V1, V2, and V3 (where applicable) signatures, and the last element
+ * is the rotated expected signer for V3+.
+ */
+ static void assertResultContainsSigners(ApkVerifier.Result result,
+ boolean rotationExpected, String... signers) throws Exception {
+ // A result must be successfully verified before verifying any of the result's signers.
+ assertTrue(result.isVerified());
+
+ List<X509Certificate> expectedSigners = new ArrayList<>();
+ for (String signer : signers) {
+ ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
+ expectedSigners.addAll(signerConfig.getCertificates());
+ }
+ // If rotation is expected then the V1 and V2 signature should only be signed by the
+ // original signer.
+ List<X509Certificate> expectedV1Signers =
+ rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+ List<X509Certificate> expectedV2Signers =
+ rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners;
+ // V3 only supports a single signer; if rotation is not expected or the V3.1 block contains
+ // the rotated signing key then the expected V3.0 signer should be the original signer.
+ List<X509Certificate> expectedV3Signers =
+ !rotationExpected || result.isVerifiedUsingV31Scheme()
+ ? List.of(expectedSigners.get(0))
+ : List.of(expectedSigners.get(expectedSigners.size() - 1));
+
+ if (result.isVerifiedUsingV1Scheme()) {
+ Set<X509Certificate> v1Signers = new HashSet<>();
+ for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
+ v1Signers.add(signer.getCertificate());
+ }
+ assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedV1Signers)
+ + ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers),
+ v1Signers.containsAll(expectedV1Signers));
+ }
+
+ if (result.isVerifiedUsingV2Scheme()) {
+ Set<X509Certificate> v2Signers = new HashSet<>();
+ for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
+ v2Signers.add(signer.getCertificate());
+ }
+ assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedV2Signers)
+ + ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers),
+ v2Signers.containsAll(expectedV2Signers));
+ }
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ Set<X509Certificate> v3Signers = new HashSet<>();
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ v3Signers.add(signer.getCertificate());
+ }
+ assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedV3Signers)
+ + ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers),
+ v3Signers.containsAll(expectedV3Signers));
+ }
+
+ if (result.isVerifiedUsingV31Scheme()) {
+ Set<X509Certificate> v31Signers = new HashSet<>();
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ v31Signers.add(signer.getCertificate());
+ }
+ // V3.1 only supports specifying signatures with a rotated signing key; if a V3.1
+ // signing block was verified then ensure it contains the expected rotated signer.
+ List<X509Certificate> expectedV31Signers = List
+ .of(expectedSigners.get(expectedSigners.size() - 1));
+ assertTrue("Expected V3.1 signers: " + getAllSubjectNamesFrom(expectedV31Signers)
+ + ", actual V3.1 signers: " + getAllSubjectNamesFrom(v31Signers),
+ v31Signers.containsAll(expectedV31Signers));
+ }
}
- private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId)
+ /**
+ * Asserts the provided verification {@code result} contains the expected V4 {@code signers}.
+ */
+ private static void assertResultContainsV4Signers(ApkVerifier.Result result, String... signers)
+ throws Exception {
+ assertTrue(result.isVerified());
+ assertTrue(result.isVerifiedUsingV4Scheme());
+ List<X509Certificate> expectedSigners = new ArrayList<>();
+ for (String signer : signers) {
+ ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
+ expectedSigners.addAll(signerConfig.getCertificates());
+ }
+ List<X509Certificate> v4Signers = new ArrayList<>();
+ for (ApkVerifier.Result.V4SchemeSignerInfo signer : result.getV4SchemeSigners()) {
+ v4Signers.addAll(signer.getCertificates());
+ }
+ assertTrue("Expected V4 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V4 signers: " + getAllSubjectNamesFrom(v4Signers),
+ v4Signers.containsAll(expectedSigners));
+ }
+
+ /**
+ * Asserts the provided {@code result} contains the expected {@code signer} targeting
+ * {@code minSdkVersion} as the minimum version for rotation.
+ */
+ static void assertV31SignerTargetsMinApiLevel(ApkVerifier.Result result, String signer,
+ int minSdkVersion) throws Exception {
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ ApkSigner.SignerConfig expectedSignerConfig = getDefaultSignerConfigFromResources(signer);
+
+ for (ApkVerifier.Result.V3SchemeSignerInfo signerConfig : result.getV31SchemeSigners()) {
+ if (signerConfig.getCertificates()
+ .containsAll(expectedSignerConfig.getCertificates())) {
+ assertEquals("The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates())
+ + ", is expected to target SDK version " + minSdkVersion
+ + ", instead it is targeting " + signerConfig.getMinSdkVersion(),
+ minSdkVersion, signerConfig.getMinSdkVersion());
+ return;
+ }
+ }
+ fail("Did not find the expected signer, " + getAllSubjectNamesFrom(
+ expectedSignerConfig.getCertificates()));
+ }
+
+ /**
+ * Returns a comma delimited {@code String} containing all of the Subject Names from the
+ * provided {@code certificates}.
+ */
+ private static String getAllSubjectNamesFrom(Collection<X509Certificate> certificates) {
+ StringBuilder result = new StringBuilder();
+ for (X509Certificate certificate : certificates) {
+ if (result.length() > 0) {
+ result.append(", ");
+ }
+ result.append(certificate.getSubjectDN().getName());
+ }
+ return result.toString();
+ }
+
+ private static boolean resourceZipFileContains(String resourceName, String zipEntryName)
+ throws IOException {
+ ZipInputStream zip = new ZipInputStream(
+ Resources.toInputStream(ApkSignerTest.class, resourceName));
+ while (true) {
+ ZipEntry entry = zip.getNextEntry();
+ if (entry == null) {
+ break;
+ }
+
+ if (entry.getName().equals(zipEntryName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private RSAPublicKey getRSAPublicKeyFromSigningBlock(File apk, int signatureVersionId)
throws Exception {
int signatureVersionBlockId;
switch (signatureVersionId) {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
- signatureVersionBlockId = V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+ signatureVersionBlockId = V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
- signatureVersionBlockId = V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ signatureVersionBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
break;
default:
throw new Exception(
@@ -1154,13 +2288,17 @@ public class ApkSignerTest {
}
private static SignatureInfo getSignatureInfoFromApk(
- DataSource apk, int signatureVersionId, int signatureVersionBlockId)
+ File apkFile, int signatureVersionId, int signatureVersionBlockId)
throws IOException, ZipFormatException,
- ApkSigningBlockUtils.SignatureNotFoundException {
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
- ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(signatureVersionId);
- return ApkSigningBlockUtils.findSignature(
- apk, zipSections, signatureVersionBlockId, result);
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) {
+ DataSource apk = DataSources.asDataSource(f, 0, f.length());
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ signatureVersionId);
+ return ApkSigningBlockUtils.findSignature(apk, zipSections, signatureVersionBlockId,
+ result);
+ }
}
/**
@@ -1173,18 +2311,22 @@ public class ApkSignerTest {
ApkSigner.Builder apkSignerBuilder)
throws Exception {
// Sign the provided golden input
- DataSource out = sign(inResourceName, apkSignerBuilder);
+ File out = sign(inResourceName, apkSignerBuilder);
+ assertVerified(verify(out, AndroidSdkVersion.P));
// Assert that the output is identical to the provided golden output
- if (out.size() > Integer.MAX_VALUE) {
- throw new RuntimeException("Output too large: " + out.size() + " bytes");
+ if (out.length() > Integer.MAX_VALUE) {
+ throw new RuntimeException("Output too large: " + out.length() + " bytes");
}
- ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size());
+ byte[] outData = new byte[(int) out.length()];
+ try (FileInputStream fis = new FileInputStream(out)) {
+ fis.read(outData);
+ }
+ ByteBuffer actualOutBuf = ByteBuffer.wrap(outData);
ByteBuffer expectedOutBuf =
ByteBuffer.wrap(Resources.toByteArray(getClass(), expectedOutResourceName));
- int actualStartPos = actualOutBuf.position();
boolean identical = false;
if (actualOutBuf.remaining() == expectedOutBuf.remaining()) {
while (actualOutBuf.hasRemaining()) {
@@ -1198,47 +2340,57 @@ public class ApkSignerTest {
if (identical) {
return;
}
- actualOutBuf.position(actualStartPos);
if (KEEP_FAILING_OUTPUT_AS_FILES) {
File tmp = File.createTempFile(getClass().getSimpleName(), ".apk");
- try (ByteChannel outChannel =
- Files.newByteChannel(
- tmp.toPath(),
- StandardOpenOption.WRITE,
- StandardOpenOption.CREATE,
- StandardOpenOption.TRUNCATE_EXISTING)) {
- while (actualOutBuf.hasRemaining()) {
- outChannel.write(actualOutBuf);
- }
- }
+ Files.copy(out.toPath(), tmp.toPath());
fail(tmp + " differs from " + expectedOutResourceName);
} else {
fail("Output differs from " + expectedOutResourceName);
}
}
- private DataSource sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
- throws Exception {
+ private File sign(File inApkFile, ApkSigner.Builder apkSignerBuilder) throws Exception {
+ try (RandomAccessFile apkFile = new RandomAccessFile(inApkFile, "r")) {
+ DataSource in = DataSources.asDataSource(apkFile);
+ return sign(in, apkSignerBuilder);
+ }
+ }
+
+ private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName)));
- ReadableDataSink out = DataSinks.newInMemoryDataSink();
- apkSignerBuilder.setInputApk(in).setOutputApk(out).build().sign();
- return out;
+ return sign(in, apkSignerBuilder);
}
- private static ApkVerifier.Result verifyForMinSdkVersion(DataSource apk, int minSdkVersion)
+ private File sign(DataSource in, ApkSigner.Builder apkSignerBuilder) throws Exception {
+ File outFile = mTemporaryFolder.newFile();
+ apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
+
+ File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig");
+ apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig);
+ apkSignerBuilder.setV4ErrorReportingEnabled(true);
+
+ apkSignerBuilder.build().sign();
+ return outFile;
+ }
+
+ private static ApkVerifier.Result verifyForMinSdkVersion(File apk, int minSdkVersion)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apk, minSdkVersion);
}
- private static ApkVerifier.Result verify(DataSource apk, Integer minSdkVersionOverride)
+ private static ApkVerifier.Result verify(File apk, Integer minSdkVersionOverride)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
ApkVerifier.Builder builder = new ApkVerifier.Builder(apk);
if (minSdkVersionOverride != null) {
builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
}
+ File idSig = new File(apk.getCanonicalPath() + ".idsig");
+ if (idSig.exists()) {
+ builder.setV4SignatureFile(idSig);
+ }
return builder.build().verify();
}
@@ -1246,17 +2398,40 @@ public class ApkSignerTest {
ApkVerifierTest.assertVerified(result);
}
+ private static void assertSourceStampVerified(File signedApk, ApkVerifier.Result result)
+ throws ApkSigningBlockUtils.SignatureNotFoundException, IOException,
+ ZipFormatException {
+ SignatureInfo signatureInfo =
+ getSignatureInfoFromApk(
+ signedApk,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ assertNotNull(signatureInfo.signatureBlock);
+ assertTrue(result.isSourceStampVerified());
+ }
+
private static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
ApkVerifierTest.assertVerificationFailure(result, expectedIssue);
}
+ private void assertFileContentsEqual(File first, File second) throws IOException {
+ assertArrayEquals(Files.readAllBytes(Paths.get(first.getPath())),
+ Files.readAllBytes(Paths.get(second.getPath())));
+ }
+
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, false);
+ }
+
+ private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
+ String keyNameInResources, boolean deterministicDsaSigning) 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).build();
+ return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs,
+ deterministicDsaSigning).build();
}
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
@@ -1267,4 +2442,9 @@ public class ApkSignerTest {
Resources.toCertificateChain(ApkSignerTest.class, certNameInResources);
return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
}
+
+ private static ApkSigner.SignerConfig getDeterministicDsaSignerConfigFromResources(
+ String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, true);
+ }
}
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 9369333..9de2b59 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -16,25 +16,35 @@
package com.android.apksig;
+import static com.android.apksig.ApkSignerTest.FIRST_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.ApkSignerTest.assertResultContainsSigners;
+import static com.android.apksig.ApkSignerTest.assertV31SignerTargetsMinApiLevel;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.ApkVerifier.IssueWithParams;
+import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus;
import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.HexEncoding;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.util.DataSources;
+import java.security.Provider;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -60,9 +70,14 @@ public class ApkVerifierTest {
private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"};
private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"};
private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = {
- "2048", "3072", "4096", "8192", "16384"
+ "2048", "3072", "4096", "8192", "16384"
};
+ private static final String RSA_2048_CERT_SHA256_DIGEST =
+ "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+ private static final String EC_P256_CERT_SHA256_DIGEST =
+ "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+
@Test
public void testOriginalAccepted() throws Exception {
// APK signed with v1 and v2 schemes. Obtained by building
@@ -781,6 +796,47 @@ public class ApkVerifierTest {
}
@Test
+ public void testTargetSdkMinSchemeVersionNotMet() throws Exception {
+ // Android 11 / SDK version 30 requires apps targeting this SDK version or higher must be
+ // signed with at least the V2 signature scheme. This test verifies if an app is targeting
+ // this SDK version and is only signed with a V1 signature then the verifier reports the
+ // platform will not accept it.
+ assertVerificationFailure(verify("v1-ec-p256-targetSdk-30.apk"),
+ Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET);
+ }
+
+ @Test
+ public void testTargetSdkMinSchemeVersionMet() throws Exception {
+ // This test verifies if an app is signed with the minimum required signature scheme version
+ // for the target SDK version then the verifier reports the platform will accept it.
+ assertVerified(verify("v2-ec-p256-targetSdk-30.apk"));
+
+ // If an app is only signed with a signature scheme higher than the required version for the
+ // target SDK the verifier should also report that the platform will accept it.
+ assertVerified(verify("v3-ec-p256-targetSdk-30.apk"));
+ }
+
+ @Test
+ public void testTargetSdkMinSchemeVersionNotMetMaxLessThanTarget() throws Exception {
+ // If the minimum signature scheme for the target SDK version is not met but the maximum
+ // SDK version is less than the target then the verifier should report that the platform
+ // will accept it since the specified max SDK version does not know about the minimum
+ // signature scheme requirement.
+ verifyForMaxSdkVersion("v1-ec-p256-targetSdk-30.apk", 29);
+ }
+
+ @Test
+ public void testTargetSdkNoUsesSdkElement() throws Exception {
+ // The target SDK minimum signature scheme version check will attempt to obtain the
+ // targetSdkVersion attribute value from the uses-sdk element in the AndroidManifest. If
+ // the targetSdkVersion is not specified then the verifier should behave the same as the
+ // platform; the minSdkVersion should be used when available and when neither the minimum or
+ // target SDK are specified a default value of 1 should be used. This test verifies that the
+ // verifier does not fail when the uses-sdk element is not specified.
+ verify("v1-only-no-uses-sdk.apk");
+ }
+
+ @Test
public void testV1MultipleDigestAlgsInManifestAndSignatureFile() throws Exception {
// MANIFEST.MF contains SHA-1 and SHA-256 digests for each entry, .SF contains only SHA-1
// digests. This file was obtained by:
@@ -1023,6 +1079,33 @@ public class ApkVerifierTest {
}
@Test
+ public void verifySourceStamp_correctSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("valid-stamp.apk");
+ // Since the API is only verifying the source stamp the result itself should be marked as
+ // verified.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ // The source stamp can also be verified by platform version; confirm the verification works
+ // using just the max signature scheme version supported by that platform version.
+ verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
public void testSourceStampBlock_signatureMissing() throws Exception {
ApkVerifier.Result verificationResult = verify("stamp-without-block.apk");
// A broken stamp should not block a signing scheme verified APK.
@@ -1031,6 +1114,14 @@ public class ApkVerifierTest {
}
@Test
+ public void verifySourceStamp_signatureMissing() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-without-block.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_NOT_VERIFIED);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+
+ @Test
public void testSourceStampBlock_certificateMismatch() throws Exception {
ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk");
// A broken stamp should not block a signing scheme verified APK.
@@ -1041,14 +1132,128 @@ public class ApkVerifierTest {
}
@Test
- public void testSourceStampBlock_apkHashMismatch() throws Exception {
- ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch.apk");
+ public void verifySourceStamp_certificateMismatch() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-certificate-mismatch.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(
+ verificationResult,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ }
+
+ @Test
+ public void testSourceStampBlock_v1OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verify("v1-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertTrue(verificationResult.isSourceStampVerified());
+ }
+
+ @Test
+ public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ // Confirm that the source stamp verification succeeds when specifying platform versions
+ // that supported later signature scheme versions.
+ verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
+ public void testSourceStampBlock_v2OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verify("v2-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertTrue(verificationResult.isSourceStampVerified());
+ }
+
+ @Test
+ public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+
+ // Confirm that the source stamp verification succeeds when specifying a platform version
+ // that supports a later signature scheme version.
+ verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
+ public void testSourceStampBlock_v3OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verify("v3-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertTrue(verificationResult.isSourceStampVerified());
+ }
+
+ @Test
+ public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v1SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v1.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v1.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v2SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v2.apk");
// A broken stamp should not block a signing scheme verified APK.
assertVerified(verificationResult);
assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
}
@Test
+ public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v2.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v3SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v3.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v3.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
public void testSourceStampBlock_malformedSignature() throws Exception {
ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk");
// A broken stamp should not block a signing scheme verified APK.
@@ -1057,6 +1262,340 @@ public class ApkVerifierTest {
verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
}
+ @Test
+ public void verifySourceStamp_malformedSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-malformed-signature.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(
+ verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+
+ @Test
+ public void verifySourceStamp_expectedDigestMatchesActual() throws Exception {
+ // The ApkVerifier provides an API to specify the expected certificate digest; this test
+ // verifies that the test runs through to completion when the actual digest matches the
+ // provided value.
+ ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ RSA_2048_CERT_SHA256_DIGEST);
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
+ public void verifySourceStamp_expectedDigestMismatch() throws Exception {
+ // If the caller requests source stamp verification with an expected cert digest that does
+ // not match the actual digest in the APK the verifier should report the mismatch.
+ ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ EC_P256_CERT_SHA256_DIGEST);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.CERT_DIGEST_MISMATCH);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+ }
+
+ @Test
+ public void verifySourceStamp_validStampLineage() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-valid.apk");
+ assertVerified(verificationResult);
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFIED);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidStampLineage() throws Exception {
+ ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-invalid.apk");
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ }
+
+ @Test
+ public void verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception {
+ // A timestamp attribute was added to the source stamp, but verification of APKs that were
+ // generated prior to the addition of the timestamp should still complete successfully,
+ // returning a default value of 0 for the timestamp.
+ ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk");
+
+ assertTrue(verificationResult.isSourceStampVerified());
+ assertEquals(
+ "A value of 0 should be returned for the timestamp when the attribute is not "
+ + "present",
+ 0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception {
+ // Once an APK is signed with a source stamp that contains a valid value for the timestamp
+ // attribute, verification of the source stamp should result in the same value for the
+ // timestamp returned to the verifier.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-valid-timestamp-value.apk");
+
+ assertTrue(verificationResult.isSourceStampVerified());
+ assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue()
+ throws Exception {
+ // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer
+ // as a little-endian long; while a larger buffer will not result in an error, any
+ // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a
+ // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read
+ // successfully.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-valid-timestamp-16-byte-buffer.apk");
+
+ assertTrue(verificationResult.isSourceStampVerified());
+ assertEquals(1645126786,
+ verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails()
+ throws Exception {
+ // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+ // reported to notify the caller to the invalid attribute value. This test verifies a
+ // a warning is reported when the timestamp attribute value is 0.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-invalid-timestamp-value-zero.apk");
+
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails()
+ throws Exception {
+ // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+ // reported to notify the caller to the invalid attribute value. This test verifies a
+ // a warning is reported when the timestamp attribute value is < 0.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-invalid-timestamp-value-less-than-zero.apk");
+
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails()
+ throws Exception {
+ // The source stamp's timestamp attribute value is expected to be written to the first 8
+ // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp
+ // value is not written as a little-endian long to the first 8 bytes of the buffer, then
+ // an error should be reported for the timestamp attribute since the rest of the buffer will
+ // be ignored.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-timestamp-in-last-8-of-16-byte-buffer.apk");
+
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+
+ @Test
+ public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception {
+ // Since the source stamp timestamp attribute value is a long, an attribute value with
+ // insufficient space to hold a long value should result in a warning reported to the user.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-int-timestamp-value.apk");
+
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+ }
+
+ @Test
+ public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception {
+ // The source stamp timestamp attribute is part of the block's signed data; this test
+ // verifies if the value of the timestamp in the stamp block is modified then verification
+ // of the source stamp should fail.
+ ApkVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-valid-timestamp-value-modified.apk");
+
+ assertSourceStampVerificationStatus(verificationResult,
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED);
+ assertSourceStampVerificationFailure(verificationResult,
+ Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void apkVerificationIssueAdapter_verifyAllBaseIssuesMapped() throws Exception {
+ Field[] fields = ApkVerificationIssue.class.getFields();
+ StringBuilder msg = new StringBuilder();
+ for (Field field : fields) {
+ // All public static int fields in the ApkVerificationIssue class should be issue IDs;
+ // if any are added that are not intended as IDs a filter set should be applied to this
+ // test.
+ if (Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) {
+ if (!ApkVerifier.ApkVerificationIssueAdapter
+ .sVerificationIssueIdToIssue.containsKey(field.get(null))) {
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(
+ "A mapping is required from ApkVerificationIssue." + field.getName()
+ + " to an ApkVerifier.Issue in ApkVerificationIssueAdapter");
+ }
+ }
+ }
+ if (msg.length() > 0) {
+ fail(msg.toString());
+ }
+ }
+
+ @Test
+ public void verifySignature_negativeModulusConscryptProvider() throws Exception {
+ Provider conscryptProvider = null;
+ try {
+ conscryptProvider = new org.conscrypt.OpenSSLProvider();
+ Security.insertProviderAt(conscryptProvider, 1);
+ assertVerified(verify("v1v2v3-rsa-2048-negmod-in-cert.apk"));
+ } catch (UnsatisfiedLinkError e) {
+ // If the library for conscrypt is not available then skip this test.
+ return;
+ } finally {
+ if (conscryptProvider != null) {
+ Security.removeProvider(conscryptProvider.getName());
+ }
+ }
+ }
+
+ @Test
+ public void verifyV31_rotationTarget34_containsExpectedSigners() throws Exception {
+ // This test verifies an APK targeting a specific SDK version for rotation properly reports
+ // that version for the rotated signer in the v3.1 block, and all other signing blocks
+ // use the original signing key.
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+
+ assertVerified(result);
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+ }
+
+ @Test
+ public void verifyV31_missingStrippingAttr_warningReported() throws Exception {
+ // The v3.1 signing block supports targeting SDK versions; to protect against these target
+ // versions being modified the v3 signer contains a stripping protection attribute with the
+ // SDK version on which rotation should be applied. This test verifies a warning is reported
+ // when this attribute is not present in the v3 signer.
+ ApkVerifier.Result result = verify("v31-tgt-33-no-v3-attr.apk");
+
+ assertVerificationWarning(result, Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING);
+ }
+
+ @Test
+ public void verifyV31_strippingAttrMismatch_errorReportedOnSupportedVersions()
+ throws Exception {
+ // This test verifies if the stripping protection attribute does not properly match the
+ // minimum SDK version on which rotation is supported then the APK should fail verification.
+ ApkVerifier.Result result = verify("v31-tgt-34-v3-attr-value-33.apk");
+ assertVerificationFailure(result, Issue.V31_ROTATION_MIN_SDK_MISMATCH);
+
+ // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+ // and the v3.1 signing block.
+ result = verifyForMaxSdkVersion("v31-tgt-34-v3-attr-value-33.apk",
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+ assertVerified(result);
+ }
+
+ @Test
+ public void verifyV31_missingV31Block_errorReportedOnSupportedVersions() throws Exception {
+ // This test verifies if the stripping protection attribute contains a value for rotation
+ // but a v3.1 signing block was not found then the APK should fail verification.
+ ApkVerifier.Result result = verify("v31-block-stripped-v3-attr-value-33.apk");
+ assertVerificationFailure(result, Issue.V31_BLOCK_MISSING);
+
+ // SDK versions that do not support v3.1 should ignore the stripping protection attribute
+ // and the v3.1 signing block.
+ result = verifyForMaxSdkVersion("v31-block-stripped-v3-attr-value-33.apk",
+ V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1);
+ assertVerified(result);
+ }
+
+ @Test
+ public void verifyV31_v31BlockWithoutV3Block_reportsError() throws Exception {
+ // A v3.1 block must always exist alongside a v3.0 block; if an APK's minSdkVersion is the
+ // same as the version supporting rotation then it should be written to a v3.0 block.
+ ApkVerifier.Result result = verify("v31-tgt-33-no-v3-block.apk");
+ assertVerificationFailure(result, Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
+
+ @Test
+ public void verifyV31_rotationTargetsDevRelease_resultReportsDevReleaseFlag() throws Exception {
+ // Development releases use the SDK version of the previous release until the SDK is
+ // finalized. In order to only target the development release and later, the v3.1 signature
+ // scheme supports targeting development releases such that the SDK version X will install
+ // on a device running X with the system property ro.build.version.codename set to a new
+ // development codename (eg T); a release platform will have this set to "REL", and the
+ // platform will ignore the v3.1 signer if the minSdkVersion is X and the codename is "REL".
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-dev-release.apk");
+
+ assertVerified(result);
+ assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34);
+ assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void verifyV3_v3RotatedSignerTargetsDevRelease_warningReported() throws Exception {
+ // While a v3.1 signer can target a development release, v3.0 does not support the same
+ // attribute since it is only intended for v3.1 with v3.0 using the original signer. This
+ // test verifies a warning is reported if an APK has this flag set on a v3.0 signer since it
+ // will be ignored by the platform.
+ ApkVerifier.Result result = verify("v3-rsa-2048_2-tgt-dev-release.apk");
+
+ assertVerificationWarning(result, Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+ }
+
+ @Test
+ public void verifyV31_rotationTargets34_resultContainsExpectedLineage() throws Exception {
+ // During verification of the v3.1 and v3.0 signing blocks, ApkVerifier will set the
+ // signing certificate lineage in the Result object; this test verifies a null lineage from
+ // a v3.0 signer does not overwrite a valid lineage from a v3.1 signer.
+ ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+
+ assertNotNull(result.getSigningCertificateLineage());
+ SigningCertificateLineageTest.assertLineageContainsExpectedSigners(
+ result.getSigningCertificateLineage(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void verify31_minSdkVersionT_resultSuccessfullyVerified() throws Exception {
+ // When a min-sdk-version of 33 is explicitly specified, apksig will behave the same as a
+ // device running this API level and only verify a v3.1 signature if it exists. This test
+ // verifies this v3.1 signature is sufficient to report the APK as verified.
+ ApkVerifier.Result result = verifyForMinSdkVersion("v31-rsa-2048_2-tgt-33-1-tgt-28.apk",
+ 33);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ }
+
+ @Test
+ public void verify31_minSdkVersionTTargetSdk30_resultSuccessfullyVerified() throws Exception {
+ // This test verifies when a min-sdk-version of 33 is specified and the APK targets API
+ // level 30 or later, the v3.1 signature is sufficient to report the APK meets the
+ // requirement of a minimum v2 signature.
+ ApkVerifier.Result result = verifyForMinSdkVersion(
+ "v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk", 33);
+
+ assertVerified(result);
+ assertTrue(result.isVerifiedUsingV31Scheme());
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
@@ -1092,6 +1631,36 @@ public class ApkVerifierTest {
return builder.build().verify();
}
+ private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, null, null, null);
+ }
+
+ private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ String expectedCertDigest) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
+ }
+
+ private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
+ maxSdkVersionOverride);
+ }
+
+ private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride)
+ throws Exception {
+ byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
+ ApkVerifier.Builder builder = new ApkVerifier.Builder(
+ DataSources.asDataSource(ByteBuffer.wrap(apkBytes)));
+ if (minSdkVersionOverride != null) {
+ builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
+ }
+ if (maxSdkVersionOverride != null) {
+ builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride);
+ }
+ return builder.build().verifySourceStamp(expectedCertDigest);
+ }
+
static void assertVerified(ApkVerifier.Result result) {
assertVerified(result, "APK");
}
@@ -1165,13 +1734,27 @@ public class ApkVerifierTest {
}
static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
- if (result.isVerified()) {
+ assertVerificationIssue(result, expectedIssue, true);
+ }
+
+ static void assertVerificationWarning(ApkVerifier.Result result, Issue expectedIssue) {
+ assertVerificationIssue(result, expectedIssue, false);
+ }
+
+ /**
+ * Asserts the provided {@code result} contains the {@code expectedIssue}; if {@code
+ * verifyError} is set to {@code true} then the specified {@link Issue} will be expected as an
+ * error, otherwise it will be expected as a warning.
+ */
+ private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue,
+ boolean verifyError) {
+ if (result.isVerified() && verifyError) {
fail("APK verification succeeded instead of failing with " + expectedIssue);
return;
}
StringBuilder msg = new StringBuilder();
- for (IssueWithParams issue : result.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1182,7 +1765,8 @@ public class ApkVerifierTest {
}
for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
String signerName = signer.getName();
- for (ApkVerifier.IssueWithParams issue : signer.getErrors()) {
+ for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1199,7 +1783,8 @@ public class ApkVerifierTest {
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
String signerName = "signer #" + (signer.getIndex() + 1);
- for (IssueWithParams issue : signer.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1214,7 +1799,8 @@ public class ApkVerifierTest {
}
for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
String signerName = "signer #" + (signer.getIndex() + 1);
- for (IssueWithParams issue : signer.getErrors()) {
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
if (expectedIssue.equals(issue.getIssue())) {
return;
}
@@ -1227,6 +1813,22 @@ public class ApkVerifierTest {
.append(issue);
}
}
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1);
+ for (IssueWithParams issue : (verifyError ? signer.getErrors()
+ : signer.getWarnings())) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK Signature Scheme v3.1 signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue);
+ }
+ }
fail(
"APK failed verification for the wrong reason"
@@ -1287,6 +1889,12 @@ public class ApkVerifierTest {
+ msg);
}
+ private static void assertSourceStampVerificationStatus(ApkVerifier.Result result,
+ SourceStampVerificationStatus verificationStatus) throws Exception {
+ assertEquals(verificationStatus,
+ result.getSourceStampInfo().getSourceStampVerificationStatus());
+ }
+
private void assertVerificationFailure(
String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index 2038421..07a48f1 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -21,27 +21,23 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
+import com.android.apksig.SigningCertificateLineage.SignerConfig;
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.ByteBufferDataSource;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.Resources;
-
-import com.android.apksig.SigningCertificateLineage.SignerConfig;
-import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
-
import com.android.apksig.util.DataSource;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
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;
@@ -94,6 +90,23 @@ public class SigningCertificateLineageTest {
}
@Test
+ public void testLineageFromBytesContainsExpectedSigners() throws Exception {
+ // This file contains the lineage with the three rsa-2048 signers
+ DataSource lineageDataSource = Resources.toDataSource(getClass(),
+ "rsa-2048-lineage-3-signers");
+ SigningCertificateLineage lineage = SigningCertificateLineage.readFromBytes(
+ lineageDataSource.getByteBuffer(0, (int) lineageDataSource.size()).array());
+ List<SignerConfig> signers = new ArrayList<>(3);
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+ assertLineageContainsExpectedSigners(lineage, signers);
+ }
+
+ @Test
public void testLineageFromFileContainsExpectedSigners() throws Exception {
// This file contains the lineage with the three rsa-2048 signers
DataSource lineageDataSource = Resources.toDataSource(getClass(),
@@ -135,6 +148,17 @@ public class SigningCertificateLineageTest {
}
@Test
+ public void testLineageWrittenToBytesContainsExpectedSigners() throws Exception {
+ SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ lineage = updateLineageWithSignerFromResources(lineage,
+ THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+ byte[] lineageBytes = lineage.getBytes();
+ lineage = SigningCertificateLineage.readFromBytes(lineageBytes);
+ assertLineageContainsExpectedSigners(lineage, mSigners);
+ }
+
+ @Test
public void testLineageWrittenToFileContainsExpectedSigners() throws Exception {
SigningCertificateLineage lineage = createLineageWithSignersFromResources(
FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
@@ -249,7 +273,8 @@ public class SigningCertificateLineageTest {
// * length-prefixed bytes: attribute pair
// * uint32: ID
// * bytes: value - encoded V3 SigningCertificateLineage
- ByteBuffer v3SignerAttribute = ByteBuffer.wrap(lineage.generateV3SignerAttribute());
+ ByteBuffer v3SignerAttribute = ByteBuffer.wrap(
+ V3SchemeSigner.generateV3SignerAttribute(lineage));
v3SignerAttribute.order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer attribute = ApkSigningBlockUtils.getLengthPrefixedSlice(v3SignerAttribute);
// The generateV3SignerAttribute method should only use the PROOF_OF_ROTATION_ATTR_ID
@@ -258,7 +283,7 @@ public class SigningCertificateLineageTest {
assertEquals(
"The ID of the v3SignerAttribute ByteBuffer is not the expected "
+ "PROOF_OF_ROTATION_ATTR_ID",
- V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID, id);
+ V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID, id);
lineage = SigningCertificateLineage.readFromV3AttributeValue(
ByteBufferUtils.toByteArray(attribute));
assertLineageContainsExpectedSigners(lineage, mSigners);
@@ -372,6 +397,21 @@ public class SigningCertificateLineageTest {
assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
}
+ @Test
+ public void testLineageFromAPKWithV31BlockContainsExpectedSigners() throws Exception {
+ SignerConfig firstSigner = getSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ SignerConfig secondSigner = getSignerConfigFromResources(
+ SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ List<SignerConfig> expectedSigners = Arrays.asList(firstSigner, secondSigner);
+ DataSource apkDataSource = Resources.toDataSource(getClass(),
+ "v31-rsa-2048_2-tgt-34-1-tgt-28.apk");
+ SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
+ apkDataSource);
+ assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners);
+
+ }
+
@Test(expected = ApkFormatException.class)
public void testLineageFromAPKWithInvalidZipCDSizeFails() throws Exception {
// This test verifies that attempting to read the lineage from an APK where the zip
@@ -510,7 +550,20 @@ public class SigningCertificateLineageTest {
return lineage.spawnDescendant(oldSignerConfig, newSignerConfig);
}
- private void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
+ /**
+ * Asserts the provided {@code lineage} contains the {@code expectedSigners} from the test's
+ * resources.
+ */
+ static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
+ String... expectedSigners) throws Exception {
+ List<SignerConfig> signers = new ArrayList<>();
+ for (String expectedSigner : expectedSigners) {
+ signers.add(getSignerConfigFromResources(expectedSigner));
+ }
+ assertLineageContainsExpectedSigners(lineage, signers);
+ }
+
+ private static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage,
List<SignerConfig> signers) {
assertEquals("The lineage does not contain the expected number of signers",
signers.size(), lineage.size());
diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
new file mode 100644
index 0000000..2186744
--- /dev/null
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.apksig.SourceStampVerifier.Result;
+import com.android.apksig.SourceStampVerifier.Result.SignerInfo;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Resources;
+import com.android.apksig.util.DataSources;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.nio.ByteBuffer;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class SourceStampVerifierTest {
+ private static final String RSA_2048_CERT_SHA256_DIGEST =
+ "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+ private static final String RSA_2048_2_CERT_SHA256_DIGEST =
+ "681b0e56a796350c08647352a4db800cc44b2adc8f4c72fa350bd05d4d50264d";
+ private static final String RSA_2048_3_CERT_SHA256_DIGEST =
+ "bb77a72efc60e66501ab75953af735874f82cfe52a70d035186a01b3482180f3";
+ private static final String EC_P256_CERT_SHA256_DIGEST =
+ "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+ private static final String EC_P256_2_CERT_SHA256_DIGEST =
+ "d78405f761ff6236cc9b570347a570aba0c62a129a3ac30c831c64d09ad95469";
+
+ @Test
+ public void verifySourceStamp_correctSignature() throws Exception {
+ Result verificationResult = verifySourceStamp("valid-stamp.apk");
+ // Since the API is only verifying the source stamp the result itself should be marked as
+ // verified.
+ assertVerified(verificationResult);
+
+ // The source stamp can also be verified by platform version; confirm the verification works
+ // using just the max signature scheme version supported by that platform version.
+ verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18);
+ assertVerified(verificationResult);
+
+ verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+
+ verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ }
+
+ @Test
+ public void verifySourceStamp_rotatedV3Key_signingCertDigestsMatch() throws Exception {
+ // The SourceStampVerifier should return a result that includes all of the latest signing
+ // certificates for each of the signature schemes that are applicable to the specified
+ // min / max SDK versions.
+
+ // Verify when platform versions that support the V1 - V3 signature schemes are specified
+ // that an APK signed with all signature schemes has its expected signers returned in the
+ // result.
+ Result verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 23,
+ 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST,
+ EC_P256_CERT_SHA256_DIGEST, EC_P256_2_CERT_SHA256_DIGEST);
+
+ // Verify when the specified platform versions only support a single signature scheme that
+ // scheme's signer is the only one in the result.
+ verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 18, 18);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+ verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+
+ verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, EC_P256_2_CERT_SHA256_DIGEST);
+ }
+
+ @Test
+ public void verifySourceStamp_signatureMissing() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-without-block.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+ }
+
+ @Test
+ public void verifySourceStamp_certificateMismatch() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-certificate-mismatch.apk");
+ assertSourceStampVerificationFailure(
+ verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ }
+
+ @Test
+ public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception {
+ Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+ // Confirm that the source stamp verification succeeds when specifying platform versions
+ // that supported later signature scheme versions.
+ verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+ verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+ }
+
+ @Test
+ public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception {
+ // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
+ // set the min / max versions to prevent failure due to a missing V1 signature.
+ Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk",
+ 24, 24);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+
+ // Confirm that the source stamp verification succeeds when specifying a platform version
+ // that supports a later signature scheme version.
+ verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+ }
+
+ @Test
+ public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception {
+ // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
+ // set the min / max versions to prevent failure due to a missing V1 signature.
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ 28, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, EC_P256_CERT_SHA256_DIGEST);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-apk-hash-mismatch-v1.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-apk-hash-mismatch-v2.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-apk-hash-mismatch-v3.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_malformedSignature() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-malformed-signature.apk");
+ assertSourceStampVerificationFailure(
+ verificationResult, ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+
+ @Test
+ public void verifySourceStamp_expectedDigestMatchesActual() throws Exception {
+ // The ApkVerifier provides an API to specify the expected certificate digest; this test
+ // verifies that the test runs through to completion when the actual digest matches the
+ // provided value.
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ RSA_2048_CERT_SHA256_DIGEST, 28, 28);
+ assertVerified(verificationResult);
+ }
+
+ @Test
+ public void verifySourceStamp_expectedDigestMismatch() throws Exception {
+ // If the caller requests source stamp verification with an expected cert digest that does
+ // not match the actual digest in the APK the verifier should report the mismatch.
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ EC_P256_CERT_SHA256_DIGEST);
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+ }
+
+ @Test
+ public void verifySourceStamp_noStampCertDigestNorSignatureBlock() throws Exception {
+ // The caller of this API expects that the provided APK should be signed with a source
+ // stamp; if no artifacts of the stamp are present ensure that the API fails indicating the
+ // missing stamp.
+ Result verificationResult = verifySourceStamp("original.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ }
+
+ @Test
+ public void verifySourceStamp_validStampLineage() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-lineage-valid.apk");
+ assertVerified(verificationResult);
+ assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST,
+ RSA_2048_2_CERT_SHA256_DIGEST);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidStampLineage() throws Exception {
+ Result verificationResult = verifySourceStamp(
+ "stamp-lineage-invalid.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ }
+
+ @Test
+ public void verifySourceStamp_multipleSignersInLineage() throws Exception {
+ Result verificationResult = verifySourceStamp("stamp-lineage-with-3-signers.apk", 18, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST,
+ RSA_2048_2_CERT_SHA256_DIGEST, RSA_2048_3_CERT_SHA256_DIGEST);
+ }
+
+ @Test
+ public void verifySourceStamp_noSignersInLineage_returnsEmptyLineage() throws Exception {
+ // If the source stamp's signer has not yet been rotated then an empty lineage should be
+ // returned.
+ Result verificationResult = verifySourceStamp("valid-stamp.apk");
+ assertSigningCertificatesInLineage(verificationResult);
+ }
+
+ @Test
+ public void verifySourceStamp_noApkSignature_succeeds()
+ throws Exception {
+ // The SourceStampVerifier is designed to verify an APK's source stamp with minimal
+ // verification of the APK signature schemes. This test verifies if just the MANIFEST.MF
+ // is present without any other APK signatures the stamp signature can still be successfully
+ // verified.
+ Result verificationResult = verifySourceStamp("stamp-without-apk-signature.apk", 18, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, null);
+ // While the source stamp verification should succeed a warning should still be logged to
+ // notify the caller that there were no signers.
+ assertSourceStampVerificationWarning(verificationResult,
+ ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+ }
+
+ @Test
+ public void verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception {
+ // A timestamp attribute was added to the source stamp, but verification of APKs that were
+ // generated prior to the addition of the timestamp should still complete successfully,
+ // returning a default value of 0 for the timestamp.
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", AndroidSdkVersion.P,
+ AndroidSdkVersion.P);
+
+ assertVerified(verificationResult);
+ assertEquals(
+ "A value of 0 should be returned for the timestamp when the attribute is not "
+ + "present",
+ 0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception {
+ // Once an APK is signed with a source stamp that contains a valid value for the timestamp
+ // attribute, verification of the source stamp should result in the same value for the
+ // timestamp returned to the verifier.
+ Result verificationResult = verifySourceStamp("stamp-valid-timestamp-value.apk");
+
+ assertVerified(verificationResult);
+ assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue()
+ throws Exception {
+ // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer
+ // as a little-endian long; while a larger buffer will not result in an error, any
+ // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a
+ // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read
+ // successfully.
+ Result verificationResult = verifySourceStamp("stamp-valid-timestamp-16-byte-buffer.apk");
+
+ assertEquals(1645126786,
+ verificationResult.getSourceStampInfo().getTimestampEpochSeconds());
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails()
+ throws Exception {
+ // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+ // reported to notify the caller to the invalid attribute value. This test verifies a
+ // a warning is reported when the timestamp attribute value is 0.
+ Result verificationResult = verifySourceStamp("stamp-invalid-timestamp-value-zero.apk");
+
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails()
+ throws Exception {
+ // If the source stamp timestamp attribute exists and is <= 0, then a warning should be
+ // reported to notify the caller to the invalid attribute value. This test verifies a
+ // a warning is reported when the timestamp attribute value is < 0.
+ Result verificationResult = verifySourceStamp(
+ "stamp-invalid-timestamp-value-less-than-zero.apk");
+
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails()
+ throws Exception {
+ // The source stamp's timestamp attribute value is expected to be written to the first 8
+ // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp
+ // value is not written as a little-endian long to the first 8 bytes of the buffer, then
+ // an error should be reported for the timestamp attribute since the rest of the buffer will
+ // be ignored.
+ Result verificationResult = verifySourceStamp(
+ "stamp-timestamp-in-last-8-of-16-byte-buffer.apk");
+
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ @Test
+ public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception {
+ // Since the source stamp timestamp attribute value is a long, an attribute value with
+ // insufficient space to hold a long value should result in a warning reported to the user.
+ Result verificationResult = verifySourceStamp(
+ "stamp-int-timestamp-value.apk");
+
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+ }
+
+ @Test
+ public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception {
+ // The source stamp timestamp attribute is part of the block's signed data; this test
+ // verifies if the value of the timestamp in the stamp block is modified then verification
+ // of the source stamp should fail.
+ Result verificationResult = verifySourceStamp(
+ "stamp-valid-timestamp-value-modified.apk");
+
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @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 {
+ return verifySourceStamp(apkFilenameInResources, null, null, null);
+ }
+
+ private Result verifySourceStamp(String apkFilenameInResources,
+ String expectedCertDigest) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
+ }
+
+ private Result verifySourceStamp(String apkFilenameInResources,
+ Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
+ maxSdkVersionOverride);
+ }
+
+ private Result verifySourceStamp(String apkFilenameInResources,
+ String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride)
+ throws Exception {
+ byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
+ SourceStampVerifier.Builder builder = new SourceStampVerifier.Builder(
+ DataSources.asDataSource(ByteBuffer.wrap(apkBytes)));
+ if (minSdkVersionOverride != null) {
+ builder.setMinCheckedPlatformVersion(minSdkVersionOverride);
+ }
+ if (maxSdkVersionOverride != null) {
+ builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride);
+ }
+ return builder.build().verifySourceStamp(expectedCertDigest);
+ }
+
+ private static void assertVerified(Result result) {
+ if (result.isVerified()) {
+ return;
+ }
+ StringBuilder msg = new StringBuilder();
+ for (ApkVerificationIssue error : result.getAllErrors()) {
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(error.toString());
+ }
+ fail("APK failed source stamp verification: " + msg.toString());
+ }
+
+ private static void assertSourceStampVerificationFailure(Result result, int expectedIssueId) {
+ if (result.isVerified()) {
+ fail(
+ "APK source stamp verification succeeded instead of failing with "
+ + expectedIssueId);
+ return;
+ }
+ assertSourceStampVerificationIssue(result.getAllErrors(), expectedIssueId);
+ }
+
+ private static void assertSourceStampVerificationWarning(Result result, int expectedIssueId) {
+ assertSourceStampVerificationIssue(result.getAllWarnings(), expectedIssueId);
+ }
+
+ private static void assertSourceStampVerificationIssue(List<ApkVerificationIssue> issues,
+ int expectedIssueId) {
+ StringBuilder msg = new StringBuilder();
+ for (ApkVerificationIssue issue : issues) {
+ if (issue.getIssueId() == expectedIssueId) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(issue.toString());
+ }
+
+ fail(
+ "APK source stamp verification did not report the expected issue. "
+ + "Expected error ID: "
+ + expectedIssueId
+ + ", actual: "
+ + (msg.length() > 0 ? msg.toString() : "No reported issues"));
+ }
+
+ /**
+ * Asserts that the provided {@code expectedCertDigests} match their respective signing
+ * certificate digest in the specified {@code result}.
+ *
+ * <p>{@code expectedCertDigests} should be provided in order of the signature schemes with V1
+ * being the first element, V2 the second, etc. If a signer is not expected to be present for
+ * a signature scheme version a {@code null} value should be provided; for instance if only a V3
+ * signing certificate is expected the following should be provided: {@code null, null,
+ * v3ExpectedCertDigest}.
+ *
+ * <p>Note, this method only supports a single signer per signature scheme; if an expected
+ * certificate digest is provided for a signature scheme and multiple signers are found an
+ * assertion exception will be thrown.
+ */
+ private static void assertSigningCertificates(Result result, String... expectedCertDigests)
+ throws Exception {
+ for (int i = 0; i < expectedCertDigests.length; i++) {
+ List<SignerInfo> signers = null;
+ switch (i) {
+ case 0:
+ signers = result.getV1SchemeSigners();
+ break;
+ case 1:
+ signers = result.getV2SchemeSigners();
+ break;
+ case 2:
+ signers = result.getV3SchemeSigners();
+ break;
+ default:
+ fail("This method only supports verification of the signing certificates up "
+ + "through the V3 Signature Scheme");
+ }
+ if (expectedCertDigests[i] == null) {
+ assertEquals(
+ "Did not expect any V" + (i + 1) + " signers, found " + signers.size(), 0,
+ signers.size());
+ continue;
+ }
+ if (signers.size() != 1) {
+ fail("Expected one V" + (i + 1) + " signer with certificate digest "
+ + expectedCertDigests[i] + ", found " + signers.size() + " V" + (i + 1)
+ + " signers");
+ }
+ X509Certificate signingCertificate = signers.get(0).getSigningCertificate();
+ assertNotNull(signingCertificate);
+ assertEquals(expectedCertDigests[i],
+ toHex(computeSha256DigestBytes(signingCertificate.getEncoded())));
+ }
+ }
+
+ /**
+ * Asserts that the provided {@code expectedCertDigests} match their respective certificate in
+ * the source stamp's lineage with the oldest signer at element 0.
+ *
+ * <p>If no values are provided for the expectedCertDigests, the source stamp's lineage will
+ * be checked for an empty {@code List} indicating the source stamp has not been rotated.
+ */
+ private static void assertSigningCertificatesInLineage(Result result,
+ String... expectedCertDigests) throws Exception {
+ List<X509Certificate> lineageCertificates =
+ result.getSourceStampInfo().getCertificatesInLineage();
+ assertEquals("Unexpected number of lineage certificates", expectedCertDigests.length,
+ lineageCertificates.size());
+ for (int i = 0; i < expectedCertDigests.length; i++) {
+ assertEquals("Stamp lineage mismatch at signer " + i, expectedCertDigests[i],
+ toHex(computeSha256DigestBytes(lineageCertificates.get(i).getEncoded())));
+ }
+ }
+}
diff --git a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
index 480dc1a..e8234c9 100644
--- a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
+++ b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java
@@ -90,6 +90,49 @@ public class ApkUtilsTest {
}
@Test
+ public void testGetTargetSdkVersionFromBinaryAndroidManifest() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("v3-ec-p256-targetSdk-30.apk");
+ assertEquals(30, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetTargetSdkVersion_noUsesSdkElement_returnsDefault() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("v1-only-no-uses-sdk.apk");
+ assertEquals(1, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetTargetSandboxVersionFromBinaryAndroidManifest() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("targetSandboxVersion-2.apk");
+ assertEquals(2, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetTargetSandboxVersion_noTargetSandboxAttribute_returnsDefault()
+ throws Exception {
+ ByteBuffer manifest = getAndroidManifest("original.apk");
+ assertEquals(1, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetVersionCodeFromBinaryAndroidManifest() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("original.apk");
+ assertEquals(10, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetVersionCode_withVersionCodeMajor_returnsOnlyVersionCode() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk");
+ assertEquals(25, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
+ public void testGetLongVersionCodeFromBinaryAndroidManifest() throws Exception {
+ ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk");
+ assertEquals(4294967321L, ApkUtils.getLongVersionCodeFromBinaryAndroidManifest(manifest));
+ }
+
+ @Test
public void testGetAndroidManifest() throws Exception {
ByteBuffer manifest = getAndroidManifest("original.apk");
MessageDigest md = MessageDigest.getInstance("SHA-256");
diff --git a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
index 85e9e90..8396d76 100644
--- a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
+++ b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
@@ -79,7 +79,7 @@ public final class VerityTreeBuilderTest {
return DataSources.asDataSource(ByteBuffer.wrap(data.getBytes(UTF_8)));
}
- @Test public void generateVerityTreeRootHashFromDummyDataSource() throws Exception {
+ @Test public void generateVerityTreeRootHashFromPlaceholderDataSource() throws Exception {
// This sample was taken from src/test/resources/com/android/apksig/original.apk.
byte[] sampleEoCDFromDisk = new byte[] {
0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x79, 0x01,
diff --git a/src/test/resources/com/android/apksig/golden-aligned-out.apk b/src/test/resources/com/android/apksig/golden-aligned-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
index 5133049..1c0edeb 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
index b9dc782..a273b0c 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
index d947e3c..69f5e64 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
index 88c571b..2a0d383 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
index 25f35cc..19931ed 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
index 30e1f72..4a4cda9 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
index f97cbeb..48f9aa3 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-file-size-aligned.apk b/src/test/resources/com/android/apksig/golden-file-size-aligned.apk
new file mode 100644
index 0000000..8dd95fc
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-file-size-aligned.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
index cc03744..cf04aa2 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
index e359da7..376b0e5 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
index 68f07ed..f34a8e5 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
index 4b51e4f..25ffa5b 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
index 7177862..9940c4f 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
index bd3e668..341d7ba 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
index 67a7d3f..80a70b9 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk
new file mode 100644
index 0000000..43c39f1
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
index 7289853..5ae642a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
new file mode 100644
index 0000000..5ce588a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-out.apk b/src/test/resources/com/android/apksig/golden-rsa-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
new file mode 100644
index 0000000..232db96
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
index c708211..5194496 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
index dd6324b..5636723 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
index 4fdc18c..92e5099 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
index 4e523ca..4f21b4c 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
index 74e7dbc..c18c445 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
index 831c756..7514d20 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
index 3196267..4db4579 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-minSdk32.apk b/src/test/resources/com/android/apksig/original-minSdk32.apk
new file mode 100644
index 0000000..1d50f27
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-minSdk32.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-with-stamp-file.apk b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
new file mode 100644
index 0000000..604fe6f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk
new file mode 100644
index 0000000..315254d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/pinsapp-unsigned.apk b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk
new file mode 100755
index 0000000..b6a6e8f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
new file mode 100644
index 0000000..add4aa0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
new file mode 100644
index 0000000..e55eb90
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
new file mode 100644
index 0000000..de23558
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
index 562805c..f1105f9 100644
--- a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
+++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk
new file mode 100644
index 0000000..0cd740c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk
new file mode 100644
index 0000000..f4a189e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk
new file mode 100644
index 0000000..6cfd082
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk
new file mode 100644
index 0000000..f9777c3
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-valid.apk b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk
new file mode 100644
index 0000000..955652e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk
new file mode 100644
index 0000000..c24fa98
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
index 2723cc8..d28774a 100644
--- a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
+++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk
new file mode 100644
index 0000000..7ec82eb
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk
new file mode 100644
index 0000000..260b0ca
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-unknown-attr.apk b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk
new file mode 100644
index 0000000..68771a5
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-unknown-sig.apk b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk
new file mode 100644
index 0000000..1c1557e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk
new file mode 100644
index 0000000..da9c34d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk
new file mode 100644
index 0000000..eefc148
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk
new file mode 100644
index 0000000..3c6a501
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
new file mode 100644
index 0000000..c2e6826
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk
index 9dec2f5..604fe6f 100644
--- a/src/test/resources/com/android/apksig/stamp-without-block.apk
+++ b/src/test/resources/com/android/apksig/stamp-without-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..6a561c0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
new file mode 100644
index 0000000..714f9ff
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk
new file mode 100644
index 0000000..61f4122
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-stamp.apk b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk
new file mode 100644
index 0000000..745a7aa
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
new file mode 100644
index 0000000..5f1103a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apk b/src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apk
new file mode 100644
index 0000000..6f73b92
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..a29dd6c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-only-with-stamp.apk b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk
new file mode 100644
index 0000000..ebd4021
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
new file mode 100644
index 0000000..94b54c9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..c58ec8b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-only-with-stamp.apk b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk
new file mode 100644
index 0000000..5f65214
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk
new file mode 100644
index 0000000..d3b2c14
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
new file mode 100644
index 0000000..d091075
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk
new file mode 100644
index 0000000..ad14731
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
new file mode 100644
index 0000000..aeaec33
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
new file mode 100644
index 0000000..ccf59d4
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
new file mode 100644
index 0000000..784f47e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
new file mode 100644
index 0000000..284cd7a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
new file mode 100644
index 0000000..1c797a7
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
new file mode 100644
index 0000000..ab87fcc
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk
index 8056e0b..2f2a592 100644
--- a/src/test/resources/com/android/apksig/valid-stamp.apk
+++ b/src/test/resources/com/android/apksig/valid-stamp.apk
Binary files differ