diff options
author | Michael Groover <mpgroover@google.com> | 2020-09-25 22:01:50 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-09-25 22:01:50 +0000 |
commit | b8af28a8640f04d653977bb312fc1e11ceb2c5d0 (patch) | |
tree | 5e6b360e019392d829cda7e8fffa593751d6f0eb | |
parent | 437ee601265d94505426eeafc2f87debcaa50c3e (diff) | |
parent | 29ff0d339577cc74e196454f16157c18f7c5f206 (diff) | |
download | apksig-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
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 |