aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Groover <mpgroover@google.com>2020-09-25 22:01:50 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-09-25 22:01:50 +0000
commitb8af28a8640f04d653977bb312fc1e11ceb2c5d0 (patch)
tree5e6b360e019392d829cda7e8fffa593751d6f0eb
parent437ee601265d94505426eeafc2f87debcaa50c3e (diff)
parent29ff0d339577cc74e196454f16157c18f7c5f206 (diff)
downloadapksig-b8af28a8640f04d653977bb312fc1e11ceb2c5d0.tar.gz
Create lightweight utils and source stamp verifier am: c088f66022 am: 29ff0d3395
Original change: https://googleplex-android-review.googlesource.com/c/platform/tools/apksig/+/12691621 Change-Id: I87f958e1067d4269c5332930bb24c06807fe662f
-rw-r--r--src/main/java/com/android/apksig/ApkVerificationIssue.java148
-rw-r--r--src/main/java/com/android/apksig/ApkVerifier.java134
-rw-r--r--src/main/java/com/android/apksig/Constants.java6
-rw-r--r--src/main/java/com/android/apksig/SourceStampVerifier.java762
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtils.java191
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtilsLite.java197
-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.java78
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java260
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java391
-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/SignatureNotFoundException.java30
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java1
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java70
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java46
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java36
-rw-r--r--src/main/java/com/android/apksig/internal/zip/ZipUtils.java47
-rw-r--r--src/main/java/com/android/apksig/zip/ZipSections.java85
-rw-r--r--src/test/java/com/android/apksig/SourceStampVerifierTest.java266
20 files changed, 2420 insertions, 494 deletions
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..bb3cb65
--- /dev/null
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+ 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 7d4f753..e323ed9 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -27,10 +27,13 @@ import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTR
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.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;
@@ -323,7 +326,7 @@ public class ApkVerifier {
apk,
sourceStampCdRecord,
zipSections.getZipCentralDirectoryOffset());
- ApkSigningBlockUtils.Result sourceStampResult =
+ ApkSigResult sourceStampResult =
V2SourceStampVerifier.verify(
apk,
zipSections,
@@ -333,7 +336,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);
@@ -773,7 +776,7 @@ public class ApkVerifier {
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
}
- ApkSigningBlockUtils.Result sourceStampResult =
+ ApkSigResult sourceStampResult =
V2SourceStampVerifier.verify(
apk,
zipSections,
@@ -801,7 +804,7 @@ public class ApkVerifier {
return createSourceStampResultWithError(
Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
Issue.UNEXPECTED_EXCEPTION, e);
- } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ } catch (SignatureNotFoundException e) {
return createSourceStampResultWithError(
Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
Issue.SOURCE_STAMP_SIG_MISSING);
@@ -1176,6 +1179,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:
@@ -1623,10 +1641,12 @@ public class ApkVerifier {
private final SourceStampVerificationStatus mSourceStampVerificationStatus;
- private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ private SourceStampInfo(ApkSignerInfo result) {
mCertificates = result.certs;
- mErrors = result.getErrors();
- mWarnings = result.getWarnings();
+ mErrors = ApkVerificationErrorAdapter.getIssuesFromVerificationIssues(
+ result.getErrors());
+ mWarnings = ApkVerificationErrorAdapter.getIssuesFromVerificationIssues(
+ result.getWarnings());
if (mErrors.isEmpty() && mWarnings.isEmpty()) {
mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
} else {
@@ -2788,7 +2808,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;
@@ -2797,6 +2817,7 @@ public class ApkVerifier {
* parameters.
*/
public IssueWithParams(Issue issue, Object[] params) {
+ super(issue.mFormat, params);
mIssue = issue;
mParams = params;
}
@@ -2953,4 +2974,101 @@ public class ApkVerifier {
mMaxSdkVersion);
}
}
+
+ /**
+ * Adapter for converting base ApkVerificationError instances to their IssueWithParams
+ * equivalent.
+ */
+ public static class ApkVerificationErrorAdapter {
+ private 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);
+ }
+
+ /**
+ * 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
index 3f67c1a..680c5c3 100644
--- a/src/main/java/com/android/apksig/Constants.java
+++ b/src/main/java/com/android/apksig/Constants.java
@@ -28,6 +28,12 @@ import com.android.apksig.internal.apk.v3.V3SchemeConstants;
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 =
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..0dad8e2
--- /dev/null
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,762 @@
+/*
+ * 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.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.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_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.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,
+ Integer 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);
+ 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) throws NoSuchAlgorithmException {
+ 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.addVerificationError(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+ return;
+ }
+ if (!signers.hasRemaining()) {
+ result.addVerificationError(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.addVerificationError(
+ 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, NoSuchAlgorithmException {
+ 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.addVerificationError(
+ 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.addVerificationError(
+ 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.addVerificationError(isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+ : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+ return;
+ }
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ manifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ /**
+ * 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> mV2SchemeSigners = new ArrayList<>();
+ private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV2SchemeSigners,
+ mV3SchemeSigners);
+ private SourceStampInfo mSourceStampInfo;
+
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+ private boolean mVerified;
+
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ 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 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 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;
+ }
+
+ /**
+ * 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<>();
+
+ void setSigningCertificate(X509Certificate signingCertificate) {
+ mSigningCertificate = signingCertificate;
+ }
+
+ void addVerificationError(int error, Object... params) {
+ mErrors.add(new ApkVerificationIssue(error, 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 {@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<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 final boolean mWarningsAsErrors = true;
+
+ private SourceStampInfo(ApkSignerInfo result) {
+ mCertificates = result.certs;
+ 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 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 df84342..896ce3f 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -17,6 +17,7 @@
package com.android.apksig.apk;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -24,6 +25,8 @@ 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 com.android.apksig.zip.ZipSections;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -44,7 +47,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 +60,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);
}
}
@@ -180,74 +110,20 @@ public abstract class ApkUtils {
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
- * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
- // FORMAT (see https://source.android.com/security/apksigning/v2.html):
- // OFFSET DATA TYPE DESCRIPTION
- // * @+0 bytes uint64: size in bytes (excluding this field)
- // * @+8 bytes payload
- // * @-24 bytes uint64: size in bytes (same as the one above)
- // * @-16 bytes uint128: magic
-
- long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
- long centralDirEndOffset =
- centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
- long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
- if (centralDirEndOffset != eocdStartOffset) {
- throw new ApkSigningBlockNotFoundException(
- "ZIP Central Directory is not immediately followed by End of Central Directory"
- + ". CD end: " + centralDirEndOffset
- + ", EoCD start: " + eocdStartOffset);
- }
-
- if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
- throw new ApkSigningBlockNotFoundException(
- "APK too small for APK Signing Block. ZIP Central Directory offset: "
- + centralDirStartOffset);
- }
- // Read the magic and offset in file from the footer section of the block:
- // * uint64: size of block
- // * 16 bytes: magic
- ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
- footer.order(ByteOrder.LITTLE_ENDIAN);
- if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
- || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
- throw new ApkSigningBlockNotFoundException(
- "No APK Signing Block before ZIP Central Directory");
- }
- // Read and compare size fields
- long apkSigBlockSizeInFooter = footer.getLong(0);
- if ((apkSigBlockSizeInFooter < footer.capacity())
- || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
- }
- int totalSize = (int) (apkSigBlockSizeInFooter + 8);
- long apkSigBlockOffset = centralDirStartOffset - totalSize;
- if (apkSigBlockOffset < 0) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block offset out of range: " + apkSigBlockOffset);
- }
- ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
- apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
- long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
- if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
- throw new ApkSigningBlockNotFoundException(
- "APK Signing Block sizes in header and footer do not match: "
- + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
- }
- return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+ ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
+ zipSections);
+ return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
- public static class ApkSigningBlock {
- private final long mStartOffsetInApk;
- private final DataSource mContents;
-
+ public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
/**
* Constructs a new {@code ApkSigningBlock}.
*
@@ -256,23 +132,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);
}
}
@@ -781,13 +641,6 @@ public abstract class ApkUtils {
}
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();
+ 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..992d6c6
--- /dev/null
+++ b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
@@ -0,0 +1,197 @@
+/*
+ * 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 {
+ /**
+ * 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..2465c02
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -0,0 +1,78 @@
+/*
+ * 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<>();
+
+ 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 8274c16..261f696 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;
@@ -110,58 +109,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,50 +230,15 @@ 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);
+ try {
+ return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
}
-
- 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");
- }
+ ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
}
/**
@@ -389,45 +305,15 @@ public class ApkSigningBlockUtils {
}
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 +832,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 +850,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);
- }
- 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());
+ return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
+ }
}
/**
@@ -1173,8 +1028,8 @@ 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 {
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
}
@@ -1194,58 +1049,18 @@ 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,
boolean onlyRequireJcaSupport) 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 = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
- : 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);
- }
- }
-
- // 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");
+ 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);
}
@@ -1408,19 +1223,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() {
@@ -1459,17 +1269,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<>();
@@ -1563,13 +1373,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..be14af8
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
@@ -0,0 +1,391 @@
+/*
+ * 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 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/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/SourceStampConstants.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
index 9502911..afb6bce 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
@@ -22,4 +22,5 @@ public class 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";
}
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 399c8b3..8f59780 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,13 +15,16 @@
*/
package com.android.apksig.internal.apk.stamp;
-import com.android.apksig.ApkVerifier;
+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.ApkSigningBlockUtilsLite;
+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.util.GuaranteedEncodedFormX509Certificate;
-import com.android.apksig.internal.util.X509CertificateUtils;
+import java.io.ByteArrayInputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
@@ -67,7 +70,7 @@ class SourceStampVerifier {
public static void verifyV1SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
- ApkSigningBlockUtils.Result.SignerInfo result,
+ ApkSignerInfo result,
byte[] apkDigest,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
@@ -101,7 +104,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,
@@ -116,11 +119,11 @@ class SourceStampVerifier {
// Parse signed signature schemes block.
ByteBuffer signedSignatureSchemes =
- ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+ ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlockData);
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
while (signedSignatureSchemes.hasRemaining()) {
ByteBuffer signedSignatureScheme =
- ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
+ ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedSignatureSchemes);
int signatureSchemeId = signedSignatureScheme.getInt();
signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
}
@@ -128,7 +131,7 @@ class SourceStampVerifier {
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(
@@ -148,18 +151,17 @@ 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);
+ ApkSigningBlockUtilsLite.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,10 +179,10 @@ 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));
+ ApkSigningBlockUtilsLite.toHex(sourceStampBlockCertificateDigest),
+ ApkSigningBlockUtilsLite.toHex(sourceStampCertificateDigest));
return null;
}
return sourceStampCertificate;
@@ -192,57 +194,57 @@ class SourceStampVerifier {
int maxSdkVersion,
X509Certificate sourceStampCertificate,
ByteBuffer signedData,
- ApkSigningBlockUtils.Result.SignerInfo result)
+ ApkSignerInfo result)
throws ApkFormatException {
// Parse the signatures block and identify supported signatures
- ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+ ByteBuffer signatures = ApkSigningBlockUtilsLite.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 = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
- byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+ byte[] sigBytes = ApkSigningBlockUtilsLite.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(
+ ApkSigningBlockUtilsLite.getSignaturesToVerify(
supportedSignatures, minSdkVersion, maxSdkVersion, true);
- } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+ } catch (NoApkSupportedSignaturesException e) {
// To facilitate debugging capture the signature algorithms and resulting exception in
// the warning.
StringBuilder signatureAlgorithms = new StringBuilder();
- for (ApkSigningBlockUtils.SupportedSignature supportedSignature : supportedSignatures) {
+ for (ApkSupportedSignature supportedSignature : supportedSignatures) {
if (signatureAlgorithms.length() > 0) {
signatureAlgorithms.append(", ");
}
signatureAlgorithms.append(supportedSignature.algorithm);
}
- result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ 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();
@@ -259,7 +261,7 @@ class SourceStampVerifier {
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
@@ -267,7 +269,7 @@ class SourceStampVerifier {
| SignatureException
| NoSuchAlgorithmException e) {
result.addWarning(
- ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
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 0a130b1..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.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/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index 69d4798..2453f36 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,6 +46,7 @@ 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;
@@ -1237,40 +1238,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);
}
/**
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..c6f074d 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,19 @@
package com.android.apksig.internal.zip;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
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 +254,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/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
new file mode 100644
index 0000000..0ff5a81
--- /dev/null
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNoException;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.HexEncoding;
+import com.android.apksig.internal.util.Resources;
+import com.android.apksig.util.DataSources;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(JUnit4.class)
+public class SourceStampVerifierTest {
+ private static final String RSA_2048_CERT_SHA256_DIGEST =
+ "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+ private static final String EC_P256_CERT_SHA256_DIGEST =
+ "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+
+ @Test
+ public void verifySourceStamp_correctSignature() throws Exception {
+ SourceStampVerifier.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_signatureMissing() throws Exception {
+ SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-without-block.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+ }
+
+ @Test
+ public void verifySourceStamp_certificateMismatch() throws Exception {
+ SourceStampVerifier.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 {
+ SourceStampVerifier.Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+ assertVerified(verificationResult);
+
+ // 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);
+
+ verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ }
+
+ @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.
+ SourceStampVerifier.Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk",
+ 24, 24);
+ assertVerified(verificationResult);
+
+ // 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);
+ }
+
+ @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.
+ SourceStampVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ 28, 28);
+ assertVerified(verificationResult);
+ }
+
+ @Test
+ public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
+ SourceStampVerifier.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 {
+ SourceStampVerifier.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 {
+ SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ "stamp-apk-hash-mismatch-v3.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void verifySourceStamp_malformedSignature() throws Exception {
+ SourceStampVerifier.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.
+ SourceStampVerifier.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.
+ SourceStampVerifier.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.
+ SourceStampVerifier.Result verificationResult = verifySourceStamp("original.apk");
+ assertSourceStampVerificationFailure(verificationResult,
+ ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ }
+
+ private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources)
+ throws Exception {
+ return verifySourceStamp(apkFilenameInResources, null, null, null);
+ }
+
+ private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ String expectedCertDigest) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
+ }
+
+ private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
+ return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
+ maxSdkVersionOverride);
+ }
+
+ private SourceStampVerifier.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 void assertVerified(SourceStampVerifier.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(SourceStampVerifier.Result result,
+ int expectedIssueId) {
+ if (result.isVerified()) {
+ fail(
+ "APK source stamp verification succeeded instead of failing with "
+ + expectedIssueId);
+ return;
+ }
+
+ StringBuilder msg = new StringBuilder();
+ for (ApkVerificationIssue issue : result.getAllErrors()) {
+ if (issue.getIssueId() == expectedIssueId) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(issue.toString());
+ }
+
+ fail(
+ "APK source stamp failed verification for the wrong reason"
+ + ". Expected error ID: "
+ + expectedIssueId
+ + ", actual: "
+ + msg);
+ }
+} \ No newline at end of file