/* * 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. * *

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. * *

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: *

*/ 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. * *

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 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> 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 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 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 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}. * *

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 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}. * *

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. * *

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 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. * *

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 getApkContentDigestFromV1SigningScheme( List cdRecords, DataSource apk, ZipSections zipSections, Result result) throws IOException, ApkFormatException { CentralDirectoryRecord manifestCdRecord = null; List signatureBlockRecords = new ArrayList<>(1); Map 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 mV1SchemeSigners = new ArrayList<>(); private final List mV2SchemeSigners = new ArrayList<>(); private final List mV3SchemeSigners = new ArrayList<>(); private final List> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners, mV2SchemeSigners, mV3SchemeSigners); private SourceStampInfo mSourceStampInfo; private final List mErrors = new ArrayList<>(); private final List 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 getV1SchemeSigners() { return mV1SchemeSigners; } /** * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the * provided APK. */ public List getV2SchemeSigners() { return mV2SchemeSigners; } /** * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the * provided APK. */ public List 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. * *

Any error prevents the APK from being considered verified. */ public boolean containsErrors() { if (!mErrors.isEmpty()) { return true; } for (List 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 getErrors() { return mErrors; } /** * Returns the warnings encountered while verifying the APK's source stamp. */ public List getWarnings() { return mWarnings; } /** * Returns all errors for this result, including any errors from signature scheme signers * and the source stamp. */ public List getAllErrors() { List errors = new ArrayList<>(); errors.addAll(mErrors); for (List 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 getAllWarnings() { List warnings = new ArrayList<>(); warnings.addAll(mWarnings); for (List 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 mErrors = new ArrayList<>(); private final List 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 getErrors() { return mErrors; } /** * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings * encountered during processing of this signer's signature block. */ public List 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 mCertificates; private final List mCertificateLineage; private final List mErrors = new ArrayList<>(); private final List 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()}. * *

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 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 getErrors() { if (!mWarningsAsErrors) { return mErrors; } List 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 getWarnings() { return mWarnings; } } } /** * Builder of {@link SourceStampVerifier} instances. * *

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. * *

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. * *

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); } } }