aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2020-11-03 18:05:42 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2020-11-03 18:05:42 +0000
commitc73e6d6d85cfe88508f93ebf18161e74e5602fc0 (patch)
treebc77c65ac96177d0ea7105734008db689a906a5c
parentfdd4d6e88a25b47687a54818e05e7a42ea80afbd (diff)
parent1c02e15450fc0c6e9e481685c0fdf5cd3a6418c7 (diff)
downloadapksig-android-platform-11.0.0_r4.tar.gz
Change-Id: I2a2de9e0b10434d652896a097bcd1c5bbbf2d057
-rw-r--r--Android.bp2
-rw-r--r--src/apksigner/java/com/android/apksigner/ApkSignerTool.java44
-rw-r--r--src/main/java/com/android/apksig/ApkSigner.java19
-rw-r--r--src/main/java/com/android/apksig/ApkVerificationIssue.java171
-rw-r--r--src/main/java/com/android/apksig/ApkVerifier.java972
-rw-r--r--src/main/java/com/android/apksig/Constants.java50
-rw-r--r--src/main/java/com/android/apksig/DefaultApkSignerEngine.java59
-rw-r--r--src/main/java/com/android/apksig/SigningCertificateLineage.java21
-rw-r--r--src/main/java/com/android/apksig/SourceStampVerifier.java882
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtils.java368
-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.java79
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java337
-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/NoApkSupportedSignaturesException.java27
-rw-r--r--src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java42
-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.java27
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java173
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java8
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java2
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java76
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java48
-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.java10
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java49
-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.java10
-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.java25
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java27
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java11
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java8
-rw-r--r--src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java6
-rw-r--r--src/main/java/com/android/apksig/internal/zip/ZipUtils.java46
-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.java229
-rw-r--r--src/test/java/com/android/apksig/ApkVerifierTest.java254
-rw-r--r--src/test/java/com/android/apksig/SigningCertificateLineageTest.java17
-rw-r--r--src/test/java/com/android/apksig/SourceStampVerifierTest.java420
-rw-r--r--src/test/java/com/android/apksig/apk/ApkUtilsTest.java43
-rw-r--r--src/test/resources/com/android/apksig/original-with-versionCodeMajor.apkbin0 -> 12703 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-without-apk-signature.apkbin0 -> 12633 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/v2-only-with-stamp.apkbin0 -> 12567 bytes
-rw-r--r--src/test/resources/com/android/apksig/v3-only-with-stamp.apkbin0 -> 12567 bytes
54 files changed, 4718 insertions, 987 deletions
diff --git a/Android.bp b/Android.bp
index c46629f..ec57fb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -21,6 +21,7 @@ java_library_host {
srcs: [
"src/main/java/**/*.java",
],
+ java_version: "1.8",
}
// apksigner command-line tool for signing APKs and verifying their signatures
@@ -36,4 +37,5 @@ java_binary_host {
"conscrypt-unbundled",
],
required: ["libconscrypt_openjdk_jni"],
+ java_version: "1.8",
}
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 2f4e680..c7cb660 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -147,8 +147,9 @@ public class ApkSignerTool {
int maxSdkVersion = Integer.MAX_VALUE;
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);
@@ -252,6 +253,10 @@ 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 {
throw new ParameterException(
"Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -358,7 +363,8 @@ public class ApkSignerTool {
apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
}
if (sourceStampSignerConfig != null) {
- apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
+ apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig)
+ .setSourceStampSigningCertificateLineage(sourceStampLineage);
}
ApkSigner apkSigner = apkSignerBuilder.build();
try {
@@ -435,10 +441,12 @@ public class ApkSignerTool {
boolean printCerts = 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)) {
@@ -461,6 +469,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"
@@ -513,7 +526,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(".")) {
@@ -524,8 +539,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();
@@ -544,7 +560,9 @@ public class ApkSignerTool {
"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;
@@ -552,6 +570,10 @@ public class ApkSignerTool {
signerNumber++;
printCertificate(signerCert, "Signer #" + signerNumber, verbose);
}
+ if (sourceStampInfo != null) {
+ printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer",
+ verbose);
+ }
}
} else {
System.err.println("DOES NOT VERIFY");
@@ -562,7 +584,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);
@@ -602,7 +624,6 @@ public class ApkSignerTool {
}
}
- ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
if (sourceStampInfo != null) {
for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
System.err.println("ERROR: SourceStamp: " + error);
@@ -961,10 +982,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);
@@ -981,7 +1002,6 @@ public class ApkSignerTool {
* @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.
- *
* @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
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 154e917..d4da569 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -86,6 +86,7 @@ public class ApkSigner {
private final List<SignerConfig> mSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
private final boolean mForceSourceStampOverwrite;
private final Integer mMinSdkVersion;
private final boolean mV1SigningEnabled;
@@ -114,6 +115,7 @@ public class ApkSigner {
private ApkSigner(
List<SignerConfig> signerConfigs,
SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
boolean forceSourceStampOverwrite,
Integer minSdkVersion,
boolean v1SigningEnabled,
@@ -136,6 +138,7 @@ public class ApkSigner {
mSignerConfigs = signerConfigs;
mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
mForceSourceStampOverwrite = forceSourceStampOverwrite;
mMinSdkVersion = minSdkVersion;
mV1SigningEnabled = v1SigningEnabled;
@@ -304,6 +307,10 @@ public class ApkSigner {
mSourceStampSignerConfig.getCertificates())
.build());
}
+ if (mSourceStampSigningCertificateLineage != null) {
+ signerEngineBuilder.setSourceStampSigningCertificateLineage(
+ mSourceStampSigningCertificateLineage);
+ }
signerEngine = signerEngineBuilder.build();
}
@@ -1022,6 +1029,7 @@ public class ApkSigner {
public static class Builder {
private final List<SignerConfig> mSignerConfigs;
private SignerConfig mSourceStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
private boolean mForceSourceStampOverwrite = false;
private boolean mV1SigningEnabled = true;
private boolean mV2SigningEnabled = true;
@@ -1101,6 +1109,16 @@ 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
@@ -1465,6 +1483,7 @@ public class ApkSigner {
return new ApkSigner(
mSignerConfigs,
mSourceStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
mForceSourceStampOverwrite,
mMinSdkVersion,
mV1SigningEnabled,
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..2aa9d0b
--- /dev/null
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+ 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 f2d0fbc..354dfbd 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -18,20 +18,28 @@ 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.V1SchemeSigner.MANIFEST_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.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.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;
@@ -53,6 +61,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;
@@ -75,7 +84,7 @@ 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");
@@ -116,12 +125,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 {
@@ -151,25 +160,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;
@@ -181,23 +178,7 @@ 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<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
@@ -212,17 +193,8 @@ 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.
@@ -353,7 +325,7 @@ public class ApkVerifier {
apk,
sourceStampCdRecord,
zipSections.getZipCentralDirectoryOffset());
- ApkSigningBlockUtils.Result sourceStampResult =
+ ApkSigResult sourceStampResult =
V2SourceStampVerifier.verify(
apk,
zipSections,
@@ -363,7 +335,7 @@ public class ApkVerifier {
maxSdkVersion);
result.mergeFrom(sourceStampResult);
}
- } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ } catch (SignatureNotFoundException ignored) {
result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
} catch (ZipFormatException e) {
throw new ApkFormatException("Failed to read APK", e);
@@ -453,7 +425,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) {
@@ -530,29 +502,38 @@ 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.
- if (androidManifest == null) {
- androidManifest = getAndroidManifestFromApk(apk, zipSections);
- }
- 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()) {
- break;
- }
- result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion,
- minSchemeVersion);
+ 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()) {
+ break;
+ }
+ result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+ targetSdkVersion,
+ minSchemeVersion);
+ }
}
}
@@ -581,7 +562,341 @@ 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 checkV4Certificate(List<X509Certificate> v4Certs,
+ List<X509Certificate> v2v3Certs, Result result) {
try {
byte[] v4Cert = v4Certs.get(0).getEncoded();
byte[] cert = v2v3Certs.get(0).getEncoded();
@@ -593,7 +908,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);
@@ -614,7 +930,8 @@ public class ApkVerifier {
ApkUtils.ZipSections zipSections)
throws IOException, ApkFormatException {
CentralDirectoryRecord manifestCdRecord = null;
- Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new HashMap<>();
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
for (CentralDirectoryRecord cdRecord : cdRecords) {
if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
manifestCdRecord = cdRecord;
@@ -639,7 +956,9 @@ public class ApkVerifier {
}
}
- private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ 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());
@@ -655,7 +974,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 {
@@ -667,120 +986,6 @@ 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;
- private static final String TARGET_SANDBOX_VERSION_ELEMENT_NAME = "manifest";
-
- /**
- * Android resource ID of the {@code android:targetSdkVersion} attribute in
- * AndroidManifest.xml.
- */
- private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
- private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
- private static final String USES_SDK_ELEMENT_NAME = "uses-sdk";
-
- /**
- * 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 getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
- TARGET_SANDBOX_VERSION_ELEMENT_NAME, TARGET_SANDBOX_VERSION_ATTR_ID);
- }
-
- /**
- * Returns the SDK 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 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_NAME, 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 = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
- USES_SDK_ELEMENT_NAME, MIN_SDK_VERSION_ATTR_ID);
- } 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 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
- */
- private static int getAttributeValueFromBinaryAndroidManifest(
- ByteBuffer androidManifestContents, String elementName, int attributeId)
- throws ApkFormatException {
- // Return the value of the requested attribute from the specified element.
- 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()))
- && (parser.getNamespace().isEmpty())) {
- int result = 1;
- 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:
- result = parser.getAttributeIntValue(i);
- break;
- default:
- throw new ApkFormatException(
- "Failed to determine APK's "
- + elementName + " attribute"
- + ": unsupported value type of"
- + " AndroidManifest.xml "
- + String.format("0x%08X", attributeId)
- + ". Only integer values supported.");
- }
- break;
- }
- }
- return result;
- }
- eventType = parser.next();
- }
- throw new ApkFormatException(
- "Failed to determine APK's " + elementName + " attribute "
- + String.format("0x%08X", attributeId)
- + " : no " + elementName + " element in AndroidManifest.xml");
- } catch (AndroidBinXmlParser.XmlParserException e) {
- throw new ApkFormatException(
- "Failed to determine APK's " + elementName + " attribute "
- + String.format("0x%08X", attributeId)
- + ": malformed AndroidManifest.xml", e);
- }
- }
-
private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
if (targetSdkVersion >= AndroidSdkVersion.R) {
return VERSION_APK_SIGNATURE_SCHEME_V2;
@@ -809,6 +1014,7 @@ public class ApkVerifier {
private boolean mVerifiedUsingV3Scheme;
private boolean mVerifiedUsingV4Scheme;
private boolean mSourceStampVerified;
+ private boolean mWarningsAsErrors;
private SigningCertificateLineage mSigningCertificateLineage;
/**
@@ -937,10 +1143,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;
+ }
}
/**
@@ -962,6 +1182,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:
@@ -992,8 +1227,6 @@ public class ApkVerifier {
default:
throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
}
- mErrors.addAll(source.getErrors());
- mWarnings.addAll(source.getWarnings());
}
/**
@@ -1004,11 +1237,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()) {
@@ -1016,6 +1255,9 @@ public class ApkVerifier {
if (signer.containsErrors()) {
return true;
}
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
}
}
if (!mV3SchemeSigners.isEmpty()) {
@@ -1023,16 +1265,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 {
@@ -1328,15 +1621,50 @@ 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 SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+
+ 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());
+ if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+ mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+ } else {
+ mSourceStampVerificationStatus =
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+ }
+ }
+
+ SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+ mCertificates = Collections.emptyList();
+ mCertificateLineage = Collections.emptyList();
+ mErrors = Collections.emptyList();
+ mWarnings = Collections.emptyList();
+ mSourceStampVerificationStatus = sourceStampVerificationStatus;
}
/**
@@ -1350,6 +1678,13 @@ 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();
}
@@ -1361,6 +1696,14 @@ public class ApkVerifier {
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
+
+ /**
+ * 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;
+ }
}
}
@@ -2338,6 +2681,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"),
@@ -2384,8 +2735,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
@@ -2400,7 +2759,87 @@ 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 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;
@@ -2421,7 +2860,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;
@@ -2430,6 +2869,7 @@ public class ApkVerifier {
* parameters.
*/
public IssueWithParams(Issue issue, Object[] params) {
+ super(issue.mFormat, params);
mIssue = issue;
mParams = params;
}
@@ -2544,7 +2984,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) {
@@ -2560,7 +2999,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) {
@@ -2586,4 +3024,118 @@ 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);
+ }
+
+ /**
+ * 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..680c5c3
--- /dev/null
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -0,0 +1,50 @@
+/*
+ * 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_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 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;
+}
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index f0796fb..90f2a6d 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -29,6 +29,7 @@ import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
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;
@@ -98,6 +99,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private final String mCreatedBy;
private final List<SignerConfig> mSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
@@ -160,6 +162,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private DefaultApkSignerEngine(
List<SignerConfig> signerConfigs,
SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
int minSdkVersion,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
@@ -190,6 +193,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
mCreatedBy = createdBy;
mSignerConfigs = signerConfigs;
mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
mMinSdkVersion = minSdkVersion;
mSigningCertificateLineage = signingCertificateLineage;
@@ -307,13 +311,8 @@ 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 List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
+ List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
// we have our configs, now touch them up to appropriately cover all SDK levels since APK
@@ -361,26 +360,40 @@ 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);
+ private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+ 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.");
+ // V4 only uses signer config to connect back to v3. Use the same filtering logic.
+ configs = processV3Configs(configs);
+ }
+ if (configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
}
return configs.get(0);
}
private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
throws InvalidKeyException {
- return createSigningBlockSignerConfig(
+ ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
mSourceStampSignerConfig,
/* 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) {
@@ -549,7 +562,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);
@@ -617,7 +630,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 {
@@ -752,7 +765,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 {
@@ -1473,6 +1486,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
public static class Builder {
private List<SignerConfig> mSignerConfigs;
private SignerConfig mStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
private final int mMinSdkVersion;
private boolean mV1SigningEnabled = true;
@@ -1564,6 +1578,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
return new DefaultApkSignerEngine(
mSignerConfigs,
mStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
mMinSdkVersion,
mV1SigningEnabled,
mV2SigningEnabled,
@@ -1582,6 +1597,16 @@ 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 APK should be signed using JAR signing (aka v1 signature scheme).
*
* <p>By default, the APK will be signed using this scheme.
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 54340d7..b8f1f8b 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;
@@ -192,7 +193,7 @@ public class SigningCertificateLineage {
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
- V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
} catch (ZipFormatException e) {
throw new ApkFormatException(e.getMessage());
} catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
@@ -263,7 +264,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);
@@ -491,20 +492,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..587cbd3
--- /dev/null
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,882 @@
+/*
+ * 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<>();
+
+ /*
+ * 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());
+ }
+
+ /**
+ * 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 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;
+ }
+ }
+ }
+
+ /**
+ * 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 c6cbf5f..69399a7 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,11 +25,10 @@ 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;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -44,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() {}
@@ -56,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);
}
}
@@ -169,85 +96,26 @@ 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;
-
/**
* 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>
+ * @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}.
*
@@ -256,23 +124,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);
}
}
@@ -324,6 +176,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}.
*
@@ -607,14 +483,156 @@ public abstract class ApkUtils {
}
}
- public static byte[] computeSha256DigestBytes(byte[] data) {
- MessageDigest messageDigest;
+ /**
+ * 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 {
- messageDigest = MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("SHA-256 is not found", e);
+ 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);
}
- messageDigest.update(data);
- return messageDigest.digest();
+ }
+
+ 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..e0ea365
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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 List<X509Certificate> certs = new ArrayList<>();
+ public List<X509Certificate> certificateLineage = 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));
+ }
+
+ /**
+ * 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 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;
+ }
+}
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 f027525..e8f6fc0 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -23,7 +23,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;
@@ -53,7 +52,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;
@@ -86,7 +84,6 @@ 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;
private static final byte[] APK_SIGNING_BLOCK_MAGIC =
@@ -110,58 +107,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 +228,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(
@@ -946,20 +767,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);
}
/**
@@ -976,30 +785,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());
}
/**
@@ -1173,57 +963,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);
}
@@ -1386,19 +1158,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() {
@@ -1437,17 +1204,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<>();
@@ -1541,13 +1308,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/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..d54f1e0 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,8 @@ public enum SignatureAlgorithm {
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
- AndroidSdkVersion.N),
+ 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 +111,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 +124,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 +137,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 +194,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..465fbb0
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.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.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;
+}
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 2f4c3ba..b4ae71a 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
@@ -15,15 +15,25 @@
*/
package com.android.apksig.internal.apk.stamp;
-import com.android.apksig.ApkVerifier;
+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.ApkVerificationIssue;
import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+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.v3.V3SigningCertificateLineage;
+import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
-import com.android.apksig.internal.util.X509CertificateUtils;
+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;
@@ -53,7 +63,8 @@ import java.util.Map;
*/
class SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
- private SourceStampVerifier() {}
+ private SourceStampVerifier() {
+ }
/**
* Parses the SourceStamp block and populates the {@code result}.
@@ -67,7 +78,7 @@ class SourceStampVerifier {
public static void verifyV1SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
+ ApkSignerInfo result,
byte[] apkDigest,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
@@ -80,12 +91,13 @@ class SourceStampVerifier {
return;
}
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
verifySourceStampSignature(
apkDigest,
minSdkVersion,
maxSdkVersion,
sourceStampCertificate,
- sourceStampBlockData,
+ apkDigestSignatures,
result);
}
@@ -101,7 +113,7 @@ class SourceStampVerifier {
public static void verifyV2SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
+ ApkSignerInfo result,
Map<Integer, byte[]> signatureSchemeApkDigests,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
@@ -115,20 +127,19 @@ class SourceStampVerifier {
}
// Parse signed signature schemes block.
- ByteBuffer signedSignatureSchemes =
- ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+ ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
while (signedSignatureSchemes.hasRemaining()) {
- ByteBuffer signedSignatureScheme =
- ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
+ ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
int signatureSchemeId = signedSignatureScheme.getInt();
- signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
+ signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
}
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
signatureSchemeApkDigests.entrySet()) {
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
return;
}
verifySourceStampSignature(
@@ -138,9 +149,26 @@ class SourceStampVerifier {
sourceStampCertificate,
signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
result);
- if (result.containsWarnings() || result.containsWarnings()) {
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+ }
+
+ 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);
}
}
@@ -148,18 +176,16 @@ class SourceStampVerifier {
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
byte[] sourceStampCertificateDigest,
- ApkSigningBlockUtils.Result.SignerInfo result)
+ 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);
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
return null;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
@@ -177,62 +203,71 @@ 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));
+ toHex(sourceStampBlockCertificateDigest),
+ toHex(sourceStampCertificateDigest));
return null;
}
return sourceStampCertificate;
}
private static void verifySourceStampSignature(
- byte[] apkDigest,
+ byte[] data,
int minSdkVersion,
int maxSdkVersion,
X509Certificate sourceStampCertificate,
- ByteBuffer signedData,
- ApkSigningBlockUtils.Result.SignerInfo result)
- throws ApkFormatException {
+ ByteBuffer signatures,
+ ApkSignerInfo result) {
// Parse the signatures block and identify supported signatures
- ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
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);
+ byte[] sigBytes = readLengthPrefixedByteArray(signature);
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(
- ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ 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);
+ ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (supportedSignatures.isEmpty()) {
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ 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();
@@ -245,11 +280,11 @@ class SourceStampVerifier {
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
- sig.update(apkDigest);
+ 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;
}
} catch (InvalidKeyException
@@ -257,9 +292,57 @@ class SourceStampVerifier {
| 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 {
+ result.addWarning(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/V1SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
index dacd0be..dee24bd 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -48,8 +48,8 @@ import java.util.Map;
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
*/
public abstract class V1SourceStampSigner {
-
- public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V1SourceStampSigner() {}
@@ -98,8 +98,8 @@ public abstract class V1SourceStampSigner {
// FORMAT:
// * length-prefixed stamp block.
- return Pair.of(
- encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_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
index 8a3e776..c3fdeec 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -16,7 +16,7 @@
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
-import static com.android.apksig.internal.apk.stamp.V1SourceStampSigner.V1_SOURCE_STAMP_BLOCK_ID;
+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;
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
index 16062bf..1c1570a 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -23,17 +23,22 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengt
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.util.ArrayList;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -50,11 +55,12 @@ import java.util.Map;
* <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
*/
public abstract class V2SourceStampSigner {
-
- public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
- private V2SourceStampSigner() {}
+ private V2SourceStampSigner() {
+ }
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig,
@@ -81,7 +87,7 @@ public abstract class V2SourceStampSigner {
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
- signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst));
+ Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
SourceStampBlock sourceStampBlock = new SourceStampBlock();
@@ -95,23 +101,36 @@ public abstract class V2SourceStampSigner {
sourceStampBlock.signedDigests = signatureSchemeDigests;
+ sourceStampBlock.stampAttributes = encodeStampAttributes(
+ generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage));
+ sourceStampBlock.signedStampAttributes =
+ ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig,
+ 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),
+ new byte[][]{
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ sourceStampBlock.stampAttributes,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedStampAttributes),
});
// FORMAT:
// * length-prefixed stamp block.
- return Pair.of(
- encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID);
+ return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
}
private static void getSignedDigestsFor(
@@ -130,7 +149,7 @@ public abstract class V2SourceStampSigner {
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
}
- digests.sort(Comparator.comparing(Pair::getFirst));
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
// FORMAT:
// * length-prefixed sequence of length-prefixed digests:
@@ -158,8 +177,43 @@ public abstract class V2SourceStampSigner {
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 static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+ HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
+ 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;
}
}
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
index 8a776fc..5ba3618 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -16,17 +16,21 @@
package com.android.apksig.internal.apk.stamp;
-import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
-import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID;
+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.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.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;
@@ -53,30 +57,29 @@ public abstract class V2SourceStampVerifier {
/**
* 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
+ * The APK must be considered verified only if {@link ApkSigResult#verified} is
* {@code true}. If verification fails, the result will contain errors -- see {@link
- * ApkSigningBlockUtils.Result#getErrors()}.
+ * ApkSigResult#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
+ * @throws SignatureNotFoundException if no SourceStamp signatures are
* found
* @throws IOException if an I/O error occurs when reading the APK
*/
- public static ApkSigningBlockUtils.Result verify(
+ public static ApkSigResult verify(
DataSource apk,
- ApkUtils.ZipSections zipSections,
+ ZipSections zipSections,
byte[] sourceStampCertificateDigest,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
int minSdkVersion,
int maxSdkVersion)
- throws IOException, NoSuchAlgorithmException,
- ApkSigningBlockUtils.SignatureNotFoundException {
- ApkSigningBlockUtils.Result result =
- new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ ApkSigResult result =
+ new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
SignatureInfo signatureInfo =
- ApkSigningBlockUtils.findSignature(
- apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result);
+ ApkSigningBlockUtilsLite.findSignature(
+ apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
verify(
signatureInfo.signatureBlock,
@@ -91,7 +94,7 @@ public abstract class V2SourceStampVerifier {
/**
* 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
+ * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
* more information about the contract of this method.
*/
private static void verify(
@@ -100,15 +103,14 @@ public abstract class V2SourceStampVerifier {
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
int minSdkVersion,
int maxSdkVersion,
- ApkSigningBlockUtils.Result result)
+ ApkSigResult result)
throws NoSuchAlgorithmException {
- ApkSigningBlockUtils.Result.SignerInfo signerInfo =
- new ApkSigningBlockUtils.Result.SignerInfo();
- result.signers.add(signerInfo);
+ ApkSignerInfo signerInfo = new ApkSignerInfo();
+ result.mSigners.add(signerInfo);
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteBuffer sourceStampBlockData =
- ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+ ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
SourceStampVerifier.verifyV2SourceStamp(
sourceStampBlockData,
certFactory,
@@ -121,7 +123,7 @@ public abstract class V2SourceStampVerifier {
} 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);
+ signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
}
}
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..6e9e0c3 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
@@ -59,17 +59,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.
@@ -303,7 +301,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 +319,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;
}
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..6d7e997 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
@@ -46,13 +46,13 @@ 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.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -82,9 +82,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 +228,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;
}
@@ -939,7 +937,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 +1047,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 +1237,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 +1341,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 e812c3f..c870a9e 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() {}
@@ -183,7 +184,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(
@@ -263,9 +264,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):
@@ -276,7 +274,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..3b70aa0
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.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.v3;
+
+/** 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 PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+}
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 56ab60e..cab2a47 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
@@ -57,8 +57,9 @@ import java.util.Map;
* it can prove the new siging certificate was signed by the old.
*/
public abstract class V3SchemeSigner {
-
- public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
/** Hidden constructor to prevent instantiation. */
private V3SchemeSigner() {}
@@ -141,6 +142,22 @@ public abstract class V3SchemeSigner {
digestInfo.getSecond());
}
+ 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 Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
@@ -166,7 +183,7 @@ public abstract class V3SchemeSigner {
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
- APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
private static byte[] generateSignerBlock(
@@ -284,13 +301,11 @@ 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];
}
- return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute();
+ return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
}
private static final class V3SignatureSchemeBlock {
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..ea93194 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
@@ -30,10 +30,11 @@ 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;
@@ -53,7 +54,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -68,9 +68,6 @@ 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() {}
@@ -105,7 +102,7 @@ public abstract class V3SchemeVerifier {
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
- APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
@@ -494,7 +491,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 =
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..1a1ad93 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,10 +17,9 @@
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;
@@ -324,8 +323,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/util/AndroidSdkVersion.java b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
index 4ef67c7..87eae48 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;
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..9d9da15 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;
@@ -247,6 +253,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/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 560202c..40255a4 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -31,10 +31,10 @@ 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.V2SourceStampSigner;
+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.Resources;
@@ -42,23 +42,23 @@ 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 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.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -90,6 +90,9 @@ public class ApkSignerTest {
private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
"rsa-2048-lineage-2-signers";
+ @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);
@@ -136,21 +139,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",
@@ -367,7 +373,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
@@ -399,7 +411,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",
@@ -474,7 +487,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",
@@ -548,7 +562,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",
@@ -661,7 +676,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+
@@ -679,7 +694,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+
@@ -698,7 +713,7 @@ public class ApkSignerTest {
// 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(
@@ -930,14 +945,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)));
@@ -960,7 +974,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)
@@ -1009,28 +1023,32 @@ 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
@@ -1041,7 +1059,7 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original-with-stamp-file.apk",
new ApkSigner.Builder(signers)
@@ -1092,7 +1110,7 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original-with-stamp-file.apk",
new ApkSigner.Builder(signers)
@@ -1113,7 +1131,7 @@ public class ApkSignerTest {
Collections.singletonList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- DataSource signedApk =
+ File signedApkFile =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
@@ -1121,17 +1139,21 @@ 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
@@ -1142,13 +1164,14 @@ 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)
.setV2SigningEnabled(false)
.setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
.setSourceStampSignerConfig(sourceStampSigner));
ApkVerifier.Result sourceStampVerificationResult =
@@ -1164,7 +1187,7 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
@@ -1186,7 +1209,7 @@ public class ApkSignerTest {
ApkSigner.SignerConfig sourceStampSigner =
getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- DataSource signedApk =
+ File signedApk =
sign(
"original.apk",
new ApkSigner.Builder(signersList)
@@ -1200,15 +1223,41 @@ public class ApkSignerTest {
assertSourceStampVerified(signedApk, sourceStampVerificationResult);
}
- private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId)
+ @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);
+ }
+
+ 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(
@@ -1247,13 +1296,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);
+ }
}
/**
@@ -1266,18 +1319,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");
+ }
+ byte[] outData = new byte[(int) out.length()];
+ try (FileInputStream fis = new FileInputStream(out)) {
+ fis.read(outData);
}
- ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size());
+ 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()) {
@@ -1291,47 +1348,47 @@ 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)
+ 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;
+ 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(DataSource apk, int minSdkVersion)
+ 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();
}
@@ -1339,14 +1396,14 @@ public class ApkSignerTest {
ApkVerifierTest.assertVerified(result);
}
- private static void assertSourceStampVerified(DataSource signedApk, ApkVerifier.Result result)
+ private static void assertSourceStampVerified(File signedApk, ApkVerifier.Result result)
throws ApkSigningBlockUtils.SignatureNotFoundException, IOException,
- ZipFormatException {
+ ZipFormatException {
SignatureInfo signatureInfo =
getSignatureInfoFromApk(
signedApk,
ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
- V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID);
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
assertNotNull(signatureInfo.signatureBlock);
assertTrue(result.isSourceStampVerified());
}
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index ed154c5..9e1a75e 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -23,6 +23,7 @@ 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.util.AndroidSdkVersion;
import com.android.apksig.internal.util.HexEncoding;
@@ -35,6 +36,8 @@ 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 +63,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
@@ -1064,6 +1072,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.
@@ -1072,6 +1107,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.
@@ -1082,6 +1125,80 @@ public class ApkVerifierTest {
}
@Test
+ 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.
@@ -1090,6 +1207,14 @@ public class ApkVerifierTest {
}
@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.
@@ -1098,6 +1223,14 @@ public class ApkVerifierTest {
}
@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.
@@ -1106,6 +1239,14 @@ public class ApkVerifierTest {
}
@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.
@@ -1114,6 +1255,81 @@ 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 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());
+ }
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
@@ -1149,6 +1365,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");
}
@@ -1344,6 +1590,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..14cab83 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;
@@ -249,7 +245,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 +255,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);
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..f5020cc
--- /dev/null
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -0,0 +1,420 @@
+/*
+ * 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.fail;
+
+import com.android.apksig.SourceStampVerifier.Result;
+import com.android.apksig.SourceStampVerifier.Result.SignerInfo;
+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);
+ }
+
+ 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/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/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-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/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/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/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