883 lines
38 KiB
Java
883 lines
38 KiB
Java
/*
|
|
* Copyright (C) 2020 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.apksig;
|
|
|
|
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
|
|
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
|
|
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
|
|
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
|
|
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
|
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
|
|
|
import com.android.apksig.apk.ApkFormatException;
|
|
import com.android.apksig.apk.ApkUtilsLite;
|
|
import com.android.apksig.internal.apk.ApkSigResult;
|
|
import com.android.apksig.internal.apk.ApkSignerInfo;
|
|
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
|
import com.android.apksig.internal.apk.SignatureInfo;
|
|
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
|
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
|
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
|
|
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
|
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
|
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
|
import com.android.apksig.internal.zip.LocalFileRecord;
|
|
import com.android.apksig.internal.zip.ZipUtils;
|
|
import com.android.apksig.util.DataSource;
|
|
import com.android.apksig.util.DataSources;
|
|
import com.android.apksig.zip.ZipFormatException;
|
|
import com.android.apksig.zip.ZipSections;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.RandomAccessFile;
|
|
import java.nio.BufferUnderflowException;
|
|
import java.nio.ByteBuffer;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.CertificateFactory;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.EnumMap;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* APK source stamp verifier intended only to verify the validity of the stamp signature.
|
|
*
|
|
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
|
|
* when obtaining the digests for verification. This verifier should only be used in cases where
|
|
* another mechanism has already been used to verify the APK signatures.
|
|
*/
|
|
public class SourceStampVerifier {
|
|
private final File mApkFile;
|
|
private final DataSource mApkDataSource;
|
|
|
|
private final int mMinSdkVersion;
|
|
private final int mMaxSdkVersion;
|
|
|
|
private SourceStampVerifier(
|
|
File apkFile,
|
|
DataSource apkDataSource,
|
|
int minSdkVersion,
|
|
int maxSdkVersion) {
|
|
mApkFile = apkFile;
|
|
mApkDataSource = apkDataSource;
|
|
mMinSdkVersion = minSdkVersion;
|
|
mMaxSdkVersion = maxSdkVersion;
|
|
}
|
|
|
|
/**
|
|
* Verifies the APK's source stamp signature and returns the result of the verification.
|
|
*
|
|
* <p>The APK's source stamp can be considered verified if the result's {@link
|
|
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
|
|
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
|
|
* can be obtained as follows:
|
|
* <ul>
|
|
* <li>Obtain the generic errors via {@link Result#getErrors()}
|
|
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
|
|
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
|
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
|
|
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
|
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
|
|
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
|
|
* </ul>
|
|
*/
|
|
public SourceStampVerifier.Result verifySourceStamp() {
|
|
return verifySourceStamp(null);
|
|
}
|
|
|
|
/**
|
|
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
|
|
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
|
|
* of the verification.
|
|
*
|
|
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
|
|
* if present, without verifying the actual source stamp certificate used to sign the source
|
|
* stamp. This can be used to verify an APK contains a properly signed source stamp without
|
|
* verifying a particular signer.
|
|
*
|
|
* @see #verifySourceStamp()
|
|
*/
|
|
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
|
|
Closeable in = null;
|
|
try {
|
|
DataSource apk;
|
|
if (mApkDataSource != null) {
|
|
apk = mApkDataSource;
|
|
} else if (mApkFile != null) {
|
|
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
|
|
in = f;
|
|
apk = DataSources.asDataSource(f, 0, f.length());
|
|
} else {
|
|
throw new IllegalStateException("APK not provided");
|
|
}
|
|
return verifySourceStamp(apk, expectedCertDigest);
|
|
} catch (IOException e) {
|
|
Result result = new Result();
|
|
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
|
return result;
|
|
} finally {
|
|
if (in != null) {
|
|
try {
|
|
in.close();
|
|
} catch (IOException ignored) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
|
|
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
|
|
* returns the result of the verification.
|
|
*
|
|
* @see #verifySourceStamp(String)
|
|
*/
|
|
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
|
|
String expectedCertDigest) {
|
|
Result result = new Result();
|
|
try {
|
|
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
|
// Attempt to obtain the source stamp's certificate digest from the APK.
|
|
List<CentralDirectoryRecord> cdRecords =
|
|
ZipUtils.parseZipCentralDirectory(apk, zipSections);
|
|
CentralDirectoryRecord sourceStampCdRecord = null;
|
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
|
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
|
sourceStampCdRecord = cdRecord;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the source stamp's certificate digest is not available within the APK then the
|
|
// source stamp cannot be verified; check if a source stamp signing block is in the
|
|
// APK's signature block to determine the appropriate status to return.
|
|
if (sourceStampCdRecord == null) {
|
|
boolean stampSigningBlockFound;
|
|
try {
|
|
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
|
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
|
stampSigningBlockFound = true;
|
|
} catch (SignatureNotFoundException e) {
|
|
stampSigningBlockFound = false;
|
|
}
|
|
result.addVerificationError(stampSigningBlockFound
|
|
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
|
|
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
|
|
return result;
|
|
}
|
|
|
|
// Verify that the contents of the source stamp certificate digest match the expected
|
|
// value, if provided.
|
|
byte[] sourceStampCertificateDigest =
|
|
LocalFileRecord.getUncompressedData(
|
|
apk,
|
|
sourceStampCdRecord,
|
|
zipSections.getZipCentralDirectoryOffset());
|
|
if (expectedCertDigest != null) {
|
|
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
|
|
sourceStampCertificateDigest);
|
|
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
|
|
result.addVerificationError(
|
|
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
|
|
actualCertDigest, expectedCertDigest);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
|
|
new HashMap<>();
|
|
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
|
|
SignatureInfo signatureInfo;
|
|
try {
|
|
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
|
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
|
} catch (SignatureNotFoundException e) {
|
|
signatureInfo = null;
|
|
}
|
|
if (signatureInfo != null) {
|
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
|
ContentDigestAlgorithm.class);
|
|
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
|
|
apkContentDigests, result);
|
|
signatureSchemeApkContentDigests.put(
|
|
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
|
|
}
|
|
}
|
|
|
|
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
|
|
signatureSchemeApkContentDigests.isEmpty())) {
|
|
SignatureInfo signatureInfo;
|
|
try {
|
|
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
|
} catch (SignatureNotFoundException e) {
|
|
signatureInfo = null;
|
|
}
|
|
if (signatureInfo != null) {
|
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
|
ContentDigestAlgorithm.class);
|
|
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
|
|
apkContentDigests, result);
|
|
signatureSchemeApkContentDigests.put(
|
|
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
|
|
}
|
|
}
|
|
|
|
if (mMinSdkVersion < AndroidSdkVersion.N
|
|
|| signatureSchemeApkContentDigests.isEmpty()) {
|
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
|
|
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
|
|
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
|
|
apkContentDigests);
|
|
}
|
|
|
|
ApkSigResult sourceStampResult =
|
|
V2SourceStampVerifier.verify(
|
|
apk,
|
|
zipSections,
|
|
sourceStampCertificateDigest,
|
|
signatureSchemeApkContentDigests,
|
|
mMinSdkVersion,
|
|
mMaxSdkVersion);
|
|
result.mergeFrom(sourceStampResult);
|
|
return result;
|
|
} catch (ApkFormatException | IOException | ZipFormatException e) {
|
|
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
|
} catch (SignatureNotFoundException e) {
|
|
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
|
|
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
|
|
*
|
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
|
* expected to be encountered on an Android platform version in the
|
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
|
*/
|
|
public static void parseSigners(
|
|
ByteBuffer apkSignatureSchemeBlock,
|
|
int apkSigSchemeVersion,
|
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
|
Result result) {
|
|
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
|
// Both the V2 and V3 signature blocks contain the following:
|
|
// * length-prefixed sequence of length-prefixed signers
|
|
ByteBuffer signers;
|
|
try {
|
|
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
|
|
} catch (ApkFormatException e) {
|
|
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
|
|
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
|
|
return;
|
|
}
|
|
if (!signers.hasRemaining()) {
|
|
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
|
|
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
|
|
return;
|
|
}
|
|
|
|
CertificateFactory certFactory;
|
|
try {
|
|
certFactory = CertificateFactory.getInstance("X.509");
|
|
} catch (CertificateException e) {
|
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
|
}
|
|
while (signers.hasRemaining()) {
|
|
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
|
if (isV2Block) {
|
|
result.addV2Signer(signerInfo);
|
|
} else {
|
|
result.addV3Signer(signerInfo);
|
|
}
|
|
try {
|
|
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
|
|
parseSigner(
|
|
signer,
|
|
apkSigSchemeVersion,
|
|
certFactory,
|
|
apkContentDigests,
|
|
signerInfo);
|
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
|
signerInfo.addVerificationWarning(
|
|
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
|
|
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the provided signer block and populates the {@code result}.
|
|
*
|
|
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
|
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
|
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
|
* integrity of the APK.
|
|
*
|
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
|
* expected to be encountered on an Android platform version in the
|
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
|
*/
|
|
private static void parseSigner(
|
|
ByteBuffer signerBlock,
|
|
int apkSigSchemeVersion,
|
|
CertificateFactory certFactory,
|
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
|
Result.SignerInfo signerInfo)
|
|
throws ApkFormatException {
|
|
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
|
// Both the V2 and V3 signer blocks contain the following:
|
|
// * length-prefixed signed data
|
|
// * length-prefixed sequence of length-prefixed digests:
|
|
// * uint32: signature algorithm ID
|
|
// * length-prefixed bytes: digest of contents
|
|
// * length-prefixed sequence of certificates:
|
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
|
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
|
|
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
|
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
|
|
|
// Parse the digests block
|
|
while (digests.hasRemaining()) {
|
|
try {
|
|
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
|
|
int sigAlgorithmId = digest.getInt();
|
|
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
|
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
|
if (signatureAlgorithm == null) {
|
|
continue;
|
|
}
|
|
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
|
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
|
signerInfo.addVerificationWarning(
|
|
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
|
|
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Parse the certificates block
|
|
if (certificates.hasRemaining()) {
|
|
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
|
|
X509Certificate certificate;
|
|
try {
|
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
|
new ByteArrayInputStream(encodedCert));
|
|
} catch (CertificateException e) {
|
|
signerInfo.addVerificationWarning(
|
|
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
|
|
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
|
|
return;
|
|
}
|
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
|
// form. Without this, getEncoded may return a different form from what was stored in
|
|
// the signature. This is because some X509Certificate(Factory) implementations
|
|
// re-encode certificates.
|
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
|
signerInfo.setSigningCertificate(certificate);
|
|
}
|
|
|
|
if (signerInfo.getSigningCertificate() == null) {
|
|
signerInfo.addVerificationWarning(
|
|
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
|
|
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
|
|
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
|
|
* returned.
|
|
*
|
|
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
|
|
* will be updated to include a warning, but the source stamp verification can still proceed.
|
|
*/
|
|
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
|
|
List<CentralDirectoryRecord> cdRecords,
|
|
DataSource apk,
|
|
ZipSections zipSections,
|
|
Result result)
|
|
throws IOException, ApkFormatException {
|
|
CentralDirectoryRecord manifestCdRecord = null;
|
|
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
|
|
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
|
|
ContentDigestAlgorithm.class);
|
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
|
String cdRecordName = cdRecord.getName();
|
|
if (cdRecordName == null) {
|
|
continue;
|
|
}
|
|
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
|
|
manifestCdRecord = cdRecord;
|
|
continue;
|
|
}
|
|
if (cdRecordName.startsWith("META-INF/")
|
|
&& (cdRecordName.endsWith(".RSA")
|
|
|| cdRecordName.endsWith(".DSA")
|
|
|| cdRecordName.endsWith(".EC"))) {
|
|
signatureBlockRecords.add(cdRecord);
|
|
}
|
|
}
|
|
if (manifestCdRecord == null) {
|
|
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
|
|
// digest is enough since this would affect the final digest signed by the stamp, and
|
|
// thus an empty digest will invalidate that signature.
|
|
return v1ContentDigest;
|
|
}
|
|
if (signatureBlockRecords.isEmpty()) {
|
|
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
|
|
} else {
|
|
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
|
|
try {
|
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
|
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
|
|
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
|
|
for (Certificate certificate : certFactory.generateCertificates(
|
|
new ByteArrayInputStream(signatureBlockBytes))) {
|
|
// If multiple certificates are found within the signature block only the
|
|
// first is used as the signer of this block.
|
|
if (certificate instanceof X509Certificate) {
|
|
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
|
signerInfo.setSigningCertificate((X509Certificate) certificate);
|
|
result.addV1Signer(signerInfo);
|
|
break;
|
|
}
|
|
}
|
|
} catch (CertificateException e) {
|
|
// Log a warning for the parsing exception but still proceed with the stamp
|
|
// verification.
|
|
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
|
|
signatureBlockRecord.getName(), e);
|
|
break;
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException("Failed to read APK", e);
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
byte[] manifestBytes =
|
|
LocalFileRecord.getUncompressedData(
|
|
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
|
|
v1ContentDigest.put(
|
|
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
|
|
return v1ContentDigest;
|
|
} catch (ZipFormatException e) {
|
|
throw new ApkFormatException("Failed to read APK", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Result of verifying the APK's source stamp signature; this signature can only be considered
|
|
* verified if {@link #isVerified()} returns true.
|
|
*/
|
|
public static class Result {
|
|
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
|
|
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
|
|
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
|
|
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
|
|
mV2SchemeSigners, mV3SchemeSigners);
|
|
private SourceStampInfo mSourceStampInfo;
|
|
|
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
|
|
|
private boolean mVerified;
|
|
|
|
void addVerificationError(int errorId, Object... params) {
|
|
mErrors.add(new ApkVerificationIssue(errorId, params));
|
|
}
|
|
|
|
void addVerificationWarning(int warningId, Object... params) {
|
|
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
|
}
|
|
|
|
private void addV1Signer(SignerInfo signerInfo) {
|
|
mV1SchemeSigners.add(signerInfo);
|
|
}
|
|
|
|
private void addV2Signer(SignerInfo signerInfo) {
|
|
mV2SchemeSigners.add(signerInfo);
|
|
}
|
|
|
|
private void addV3Signer(SignerInfo signerInfo) {
|
|
mV3SchemeSigners.add(signerInfo);
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if the APK's source stamp signature
|
|
*/
|
|
public boolean isVerified() {
|
|
return mVerified;
|
|
}
|
|
|
|
private void mergeFrom(ApkSigResult source) {
|
|
switch (source.signatureSchemeVersion) {
|
|
case Constants.VERSION_SOURCE_STAMP:
|
|
mVerified = source.verified;
|
|
if (!source.mSigners.isEmpty()) {
|
|
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown ApkSigResult Signing Block Scheme Id "
|
|
+ source.signatureSchemeVersion);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
|
|
* provided APK.
|
|
*/
|
|
public List<SignerInfo> getV1SchemeSigners() {
|
|
return mV1SchemeSigners;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
|
|
* provided APK.
|
|
*/
|
|
public List<SignerInfo> getV2SchemeSigners() {
|
|
return mV2SchemeSigners;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
|
|
* provided APK.
|
|
*/
|
|
public List<SignerInfo> getV3SchemeSigners() {
|
|
return mV3SchemeSigners;
|
|
}
|
|
|
|
/**
|
|
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
|
|
* APK, or null if the source stamp signature verification failed before the stamp signature
|
|
* block could be fully parsed.
|
|
*/
|
|
public SourceStampInfo getSourceStampInfo() {
|
|
return mSourceStampInfo;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if an error was encountered while verifying the APK.
|
|
*
|
|
* <p>Any error prevents the APK from being considered verified.
|
|
*/
|
|
public boolean containsErrors() {
|
|
if (!mErrors.isEmpty()) {
|
|
return true;
|
|
}
|
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
|
for (SignerInfo signer : signers) {
|
|
if (signer.containsErrors()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (mSourceStampInfo != null) {
|
|
if (mSourceStampInfo.containsErrors()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the errors encountered while verifying the APK's source stamp.
|
|
*/
|
|
public List<ApkVerificationIssue> getErrors() {
|
|
return mErrors;
|
|
}
|
|
|
|
/**
|
|
* Returns the warnings encountered while verifying the APK's source stamp.
|
|
*/
|
|
public List<ApkVerificationIssue> getWarnings() {
|
|
return mWarnings;
|
|
}
|
|
|
|
/**
|
|
* Returns all errors for this result, including any errors from signature scheme signers
|
|
* and the source stamp.
|
|
*/
|
|
public List<ApkVerificationIssue> getAllErrors() {
|
|
List<ApkVerificationIssue> errors = new ArrayList<>();
|
|
errors.addAll(mErrors);
|
|
|
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
|
for (SignerInfo signer : signers) {
|
|
errors.addAll(signer.getErrors());
|
|
}
|
|
}
|
|
if (mSourceStampInfo != null) {
|
|
errors.addAll(mSourceStampInfo.getErrors());
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Returns all warnings for this result, including any warnings from signature scheme
|
|
* signers and the source stamp.
|
|
*/
|
|
public List<ApkVerificationIssue> getAllWarnings() {
|
|
List<ApkVerificationIssue> warnings = new ArrayList<>();
|
|
warnings.addAll(mWarnings);
|
|
|
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
|
for (SignerInfo signer : signers) {
|
|
warnings.addAll(signer.getWarnings());
|
|
}
|
|
}
|
|
if (mSourceStampInfo != null) {
|
|
warnings.addAll(mSourceStampInfo.getWarnings());
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
/**
|
|
* Contains information about an APK's signer and any errors encountered while parsing the
|
|
* corresponding signature block.
|
|
*/
|
|
public static class SignerInfo {
|
|
private X509Certificate mSigningCertificate;
|
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
|
|
|
void setSigningCertificate(X509Certificate signingCertificate) {
|
|
mSigningCertificate = signingCertificate;
|
|
}
|
|
|
|
void addVerificationError(int errorId, Object... params) {
|
|
mErrors.add(new ApkVerificationIssue(errorId, params));
|
|
}
|
|
|
|
void addVerificationWarning(int warningId, Object... params) {
|
|
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
|
}
|
|
|
|
/**
|
|
* Returns the current signing certificate used by this signer.
|
|
*/
|
|
public X509Certificate getSigningCertificate() {
|
|
return mSigningCertificate;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
|
|
* encountered during processing of this signer's signature block.
|
|
*/
|
|
public List<ApkVerificationIssue> getErrors() {
|
|
return mErrors;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
|
|
* encountered during processing of this signer's signature block.
|
|
*/
|
|
public List<ApkVerificationIssue> getWarnings() {
|
|
return mWarnings;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if any errors were encountered while parsing this signer's
|
|
* signature block.
|
|
*/
|
|
public boolean containsErrors() {
|
|
return !mErrors.isEmpty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Contains information about an APK's source stamp and any errors encountered while
|
|
* parsing the stamp signature block.
|
|
*/
|
|
public static class SourceStampInfo {
|
|
private final List<X509Certificate> mCertificates;
|
|
private final List<X509Certificate> mCertificateLineage;
|
|
|
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
|
|
|
/*
|
|
* Since this utility is intended just to verify the source stamp, and the source stamp
|
|
* currently only logs warnings to prevent failing the APK signature verification, treat
|
|
* all warnings as errors. If the stamp verification is updated to log errors this
|
|
* should be set to false to ensure only errors trigger a failure verifying the source
|
|
* stamp.
|
|
*/
|
|
private static final boolean mWarningsAsErrors = true;
|
|
|
|
private SourceStampInfo(ApkSignerInfo result) {
|
|
mCertificates = result.certs;
|
|
mCertificateLineage = result.certificateLineage;
|
|
mErrors.addAll(result.getErrors());
|
|
mWarnings.addAll(result.getWarnings());
|
|
}
|
|
|
|
/**
|
|
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
|
|
* certificate is guaranteed to be available if no errors were encountered during
|
|
* verification (see {@link #containsErrors()}.
|
|
*
|
|
* <p>This certificate contains the SourceStamp's public key.
|
|
*/
|
|
public X509Certificate getCertificate() {
|
|
return mCertificates.isEmpty() ? null : mCertificates.get(0);
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link X509Certificate} instances representing the source
|
|
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
|
|
* if the stamp's signing certificate has not been rotated.
|
|
*/
|
|
public List<X509Certificate> getCertificatesInLineage() {
|
|
return mCertificateLineage;
|
|
}
|
|
|
|
/**
|
|
* Returns whether any errors were encountered during the source stamp verification.
|
|
*/
|
|
public boolean containsErrors() {
|
|
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
|
|
* encountered during source stamp verification.
|
|
*/
|
|
public List<ApkVerificationIssue> getErrors() {
|
|
if (!mWarningsAsErrors) {
|
|
return mErrors;
|
|
}
|
|
List<ApkVerificationIssue> result = new ArrayList<>();
|
|
result.addAll(mErrors);
|
|
result.addAll(mWarnings);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
|
|
* were encountered during source stamp verification.
|
|
*/
|
|
public List<ApkVerificationIssue> getWarnings() {
|
|
return mWarnings;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builder of {@link SourceStampVerifier} instances.
|
|
*
|
|
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
|
|
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
|
|
* queried to determine the APK's minimum supported level, so the caller should specify a lower
|
|
* bound with {@link #setMinCheckedPlatformVersion(int)}.
|
|
*/
|
|
public static class Builder {
|
|
private final File mApkFile;
|
|
private final DataSource mApkDataSource;
|
|
|
|
private int mMinSdkVersion = 1;
|
|
private int mMaxSdkVersion = Integer.MAX_VALUE;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
|
* apk}.
|
|
*/
|
|
public Builder(File apk) {
|
|
if (apk == null) {
|
|
throw new NullPointerException("apk == null");
|
|
}
|
|
mApkFile = apk;
|
|
mApkDataSource = null;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
|
* apk}.
|
|
*/
|
|
public Builder(DataSource apk) {
|
|
if (apk == null) {
|
|
throw new NullPointerException("apk == null");
|
|
}
|
|
mApkDataSource = apk;
|
|
mApkFile = null;
|
|
}
|
|
|
|
/**
|
|
* Sets the oldest Android platform version for which the APK's source stamp is verified.
|
|
*
|
|
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
|
* on all Android platforms starting from the platform version with the provided {@code
|
|
* minSdkVersion}. The upper end of the platform versions range can be modified via
|
|
* {@link #setMaxCheckedPlatformVersion(int)}.
|
|
*
|
|
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
|
*/
|
|
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
|
mMinSdkVersion = minSdkVersion;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the newest Android platform version for which the APK's source stamp is verified.
|
|
*
|
|
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
|
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
|
|
* lower end of the platform versions range can be modified via {@link
|
|
* #setMinCheckedPlatformVersion(int)}.
|
|
*
|
|
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
|
* @see #setMinCheckedPlatformVersion(int)
|
|
*/
|
|
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
|
|
mMaxSdkVersion = maxSdkVersion;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
|
|
* builder.
|
|
*/
|
|
public SourceStampVerifier build() {
|
|
return new SourceStampVerifier(
|
|
mApkFile,
|
|
mApkDataSource,
|
|
mMinSdkVersion,
|
|
mMaxSdkVersion);
|
|
}
|
|
}
|
|
}
|