/* * Copyright (C) 2016 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.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest; import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; 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; import com.android.apksig.internal.apk.v2.V2SchemeConstants; import com.android.apksig.internal.apk.v2.V2SchemeVerifier; import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.apk.v4.V4SchemeVerifier; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; import com.android.apksig.util.RunnablesExecutor; import com.android.apksig.zip.ZipFormatException; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * APK signature verifier which mimics the behavior of the Android platform. * *

The verifier is designed to closely mimic the behavior of Android platforms. This is to enable * the verifier to be used for checking whether an APK's signatures are expected to verify on * Android. * *

Use {@link Builder} to obtain instances of this verifier. * * @see Application Signing */ public class ApkVerifier { private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES = loadSupportedApkSigSchemeNames(); private static Map loadSupportedApkSigSchemeNames() { Map supportedMap = new HashMap<>(2); supportedMap.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); supportedMap.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3"); return supportedMap; } private final File mApkFile; private final DataSource mApkDataSource; private final File mV4SignatureFile; private final Integer mMinSdkVersion; private final int mMaxSdkVersion; private ApkVerifier( File apkFile, DataSource apkDataSource, File v4SignatureFile, Integer minSdkVersion, int maxSdkVersion) { mApkFile = apkFile; mApkDataSource = apkDataSource; mV4SignatureFile = v4SignatureFile; mMinSdkVersion = minSdkVersion; mMaxSdkVersion = maxSdkVersion; } /** * Verifies the APK's signatures and returns the result of verification. The APK can be * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. * The verification result also includes errors, warnings, and information about signers such * as their signing certificates. * *

Verification succeeds iff the APK's signature is expected to verify on all Android * platform versions specified via the {@link Builder}. If the APK's signature is expected to * not verify on any of the specified platform versions, this method returns a result with one * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method * throws an exception. * * @throws IOException if an I/O error is encountered while reading the APK * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing * @throws IllegalStateException if this verifier's configuration is missing required * information. */ public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException, IllegalStateException { 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 verify(apk); } finally { if (in != null) { in.close(); } } } /** * Simply retrieve result that contains V2Signature only by ignoring all the verification process. * * @throws IOException if an I/O error is encountered while reading the APK * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing * @throws IllegalStateException if this verifier's configuration is missing required * information. */ public Result retrieveV2Signature() throws IOException, ApkFormatException, NoSuchAlgorithmException { 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 retrieveV2Signature(apk); } finally { if (in != null) { in.close(); } } } /** * Simply retrieve result that contains V2Signature only by ignoring all the verification process. * * @param apk APK file contents * @throws IOException if an I/O error is encountered while reading the APK * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing */ private Result retrieveV2Signature(DataSource apk) throws IOException, ApkFormatException, NoSuchAlgorithmException { int maxSdkVersion = mMaxSdkVersion; ApkUtils.ZipSections zipSections; try { zipSections = ApkUtils.findZipSections(apk); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed APK: not a ZIP archive", e); } int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); Result result = new Result(); Map> signatureSchemeApkContentDigests = new HashMap<>(); // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme // name, but the verifiers use this parameter as the schemes supported by the target SDK // range. Since the code below skips signature verification based on max SDK the mapping of // supported schemes needs to be modified to ensure the verifiers do not report a stripped // signature for an SDK range that does not support that signature version. For instance an // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 // verification is performed it would see the stripping protection attribute, see that V3 // is in the list of supported signatures, and report a stripped signature. Map supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion); // Android N and newer attempts to verify APKs using the APK Signing Block, which can // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // verification. If the signature is found but does not verify, the APK is rejected. Set foundApkSigSchemeIds = new HashSet<>(2); if (maxSdkVersion >= AndroidSdkVersion.N) { RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if // no APK Signature Scheme v3 (or newer scheme) signatures were found. try { ApkSigningBlockUtils.Result v2Result = V2SchemeVerifier.verify( executor, apk, zipSections, supportedSchemeNames, foundApkSigSchemeIds, Math.max(minSdkVersion, AndroidSdkVersion.N), maxSdkVersion); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); result.mergeFrom(v2Result); signatureSchemeApkContentDigests.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, getApkContentDigestsFromSigningSchemeResult(v2Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { // v2 signature not required } if (result.containsErrors()) { return result; } } return result; } /** * Verifies the APK's signatures and returns the result of verification. The APK can be * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. * The verification result also includes errors, warnings, and information about signers. * * @param apk APK file contents * @throws IOException if an I/O error is encountered while reading the APK * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing */ private Result verify(DataSource apk) throws IOException, ApkFormatException, NoSuchAlgorithmException { int maxSdkVersion = mMaxSdkVersion; ApkUtils.ZipSections zipSections; try { zipSections = ApkUtils.findZipSections(apk); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed APK: not a ZIP archive", e); } ByteBuffer androidManifest = null; int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); Result result = new Result(); Map> signatureSchemeApkContentDigests = new HashMap<>(); // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme // name, but the verifiers use this parameter as the schemes supported by the target SDK // range. Since the code below skips signature verification based on max SDK the mapping of // supported schemes needs to be modified to ensure the verifiers do not report a stripped // signature for an SDK range that does not support that signature version. For instance an // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 // verification is performed it would see the stripping protection attribute, see that V3 // is in the list of supported signatures, and report a stripped signature. Map supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion); // Android N and newer attempts to verify APKs using the APK Signing Block, which can // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // verification. If the signature is found but does not verify, the APK is rejected. Set foundApkSigSchemeIds = new HashSet<>(2); if (maxSdkVersion >= AndroidSdkVersion.N) { RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; // Android P and newer attempts to verify APKs using APK Signature Scheme v3 if (maxSdkVersion >= AndroidSdkVersion.P) { try { ApkSigningBlockUtils.Result v3Result = V3SchemeVerifier.verify( executor, apk, zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P), maxSdkVersion); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); result.mergeFrom(v3Result); signatureSchemeApkContentDigests.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, getApkContentDigestsFromSigningSchemeResult(v3Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { // v3 signature not required } if (result.containsErrors()) { return result; } } // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if // no APK Signature Scheme v3 (or newer scheme) signatures were found. if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { try { ApkSigningBlockUtils.Result v2Result = V2SchemeVerifier.verify( executor, apk, zipSections, supportedSchemeNames, foundApkSigSchemeIds, Math.max(minSdkVersion, AndroidSdkVersion.N), maxSdkVersion); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); result.mergeFrom(v2Result); signatureSchemeApkContentDigests.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, getApkContentDigestsFromSigningSchemeResult(v2Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { // v2 signature not required } if (result.containsErrors()) { return result; } } // If v4 file is specified, use additional verification on it if (mV4SignatureFile != null) { final ApkSigningBlockUtils.Result v4Result = V4SchemeVerifier.verify(apk, mV4SignatureFile); foundApkSigSchemeIds.add( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); result.mergeFrom(v4Result); if (result.containsErrors()) { return result; } } } // Android O and newer requires that APKs targeting security sandbox version 2 and higher // are signed using APK Signature Scheme v2 or newer. if (maxSdkVersion >= AndroidSdkVersion.O) { if (androidManifest == null) { androidManifest = getAndroidManifestFromApk(apk, zipSections); } int targetSandboxVersion = getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice()); if (targetSandboxVersion > 1) { if (foundApkSigSchemeIds.isEmpty()) { result.addError( Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION, targetSandboxVersion); } } } List cdRecords = V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer // scheme) signatures were found. if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { V1SchemeVerifier.Result v1Result = V1SchemeVerifier.verify( apk, zipSections, supportedSchemeNames, foundApkSigSchemeIds, minSdkVersion, maxSdkVersion); result.mergeFrom(v1Result); signatureSchemeApkContentDigests.put( ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME, getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); } if (result.containsErrors()) { return result; } // Verify the SourceStamp, if found in the APK. try { CentralDirectoryRecord sourceStampCdRecord = null; for (CentralDirectoryRecord cdRecord : cdRecords) { if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals( cdRecord.getName())) { sourceStampCdRecord = cdRecord; break; } } // If SourceStamp file is found inside the APK, there must be a SourceStamp // block in the APK signing block as well. if (sourceStampCdRecord != null) { byte[] sourceStampCertificateDigest = LocalFileRecord.getUncompressedData( apk, sourceStampCdRecord, zipSections.getZipCentralDirectoryOffset()); ApkSigResult sourceStampResult = V2SourceStampVerifier.verify( apk, zipSections, sourceStampCertificateDigest, signatureSchemeApkContentDigests, Math.max(minSdkVersion, AndroidSdkVersion.R), maxSdkVersion); result.mergeFrom(sourceStampResult); } } catch (SignatureNotFoundException ignored) { result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); } catch (ZipFormatException e) { throw new ApkFormatException("Failed to read APK", e); } if (result.containsErrors()) { return result; } // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2 // signatures verified. if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) { ArrayList v1Signers = new ArrayList<>(result.getV1SchemeSigners()); ArrayList v2Signers = new ArrayList<>(result.getV2SchemeSigners()); ArrayList v1SignerCerts = new ArrayList<>(); ArrayList v2SignerCerts = new ArrayList<>(); for (Result.V1SchemeSignerInfo signer : v1Signers) { try { v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); } catch (CertificateEncodingException e) { throw new IllegalStateException( "Failed to encode JAR signer " + signer.getName() + " certs", e); } } for (Result.V2SchemeSignerInfo signer : v2Signers) { try { v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); } catch (CertificateEncodingException e) { throw new IllegalStateException( "Failed to encode APK Signature Scheme v2 signer (index: " + signer.getIndex() + ") certs", e); } } for (int i = 0; i < v1SignerCerts.size(); i++) { ByteArray v1Cert = v1SignerCerts.get(i); if (!v2SignerCerts.contains(v1Cert)) { Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i); v1Signer.addError(Issue.V2_SIG_MISSING); break; } } for (int i = 0; i < v2SignerCerts.size(); i++) { ByteArray v2Cert = v2SignerCerts.get(i); if (!v1SignerCerts.contains(v2Cert)) { Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i); v2Signer.addError(Issue.JAR_SIG_MISSING); break; } } } // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer // matches the oldest signing certificate in the provided SigningCertificateLineage if (result.isVerifiedUsingV3Scheme() && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) { SigningCertificateLineage lineage = result.getSigningCertificateLineage(); X509Certificate oldSignerCert; if (result.isVerifiedUsingV1Scheme()) { List v1Signers = result.getV1SchemeSigners(); if (v1Signers.size() != 1) { // APK Signature Scheme v3 only supports single-signers, error to sign with // multiple and then only one result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); } oldSignerCert = v1Signers.get(0).mCertChain.get(0); } else { List v2Signers = result.getV2SchemeSigners(); if (v2Signers.size() != 1) { // APK Signature Scheme v3 only supports single-signers, error to sign with // multiple and then only one result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); } oldSignerCert = v2Signers.get(0).mCerts.get(0); } if (lineage == null) { // no signing certificate history with which to contend, just make sure that v3 // matches previous versions List v3Signers = result.getV3SchemeSigners(); if (v3Signers.size() != 1) { // multiple v3 signers should never exist without rotation history, since // multiple signers implies a different signer for different platform versions result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS); } try { if (!Arrays.equals(oldSignerCert.getEncoded(), v3Signers.get(0).mCerts.get(0).getEncoded())) { result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); } } catch (CertificateEncodingException e) { // we just go the encoding for the v1/v2 certs above, so must be v3 throw new RuntimeException( "Failed to encode APK Signature Scheme v3 signer cert", e); } } else { // we have some signing history, make sure that the root of the history is the same // as our v1/v2 signer try { lineage = lineage.getSubLineage(oldSignerCert); if (lineage.size() != 1) { // the v1/v2 signer was found, but not at the root of the lineage result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); } } catch (IllegalArgumentException e) { // the v1/v2 signer was not found in the lineage result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); } } } // If there is a v4 scheme signer, make sure that their certificates match. // The apkDigest field in the v4 signature should match the selected v2/v3. if (result.isVerifiedUsingV4Scheme()) { List v4Signers = result.getV4SchemeSigners(); if (v4Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); } List digestsFromV4 = v4Signers.get(0).getContentDigests(); if (digestsFromV4.size() != 1) { result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); } final byte[] digestFromV4 = digestsFromV4.get(0).getValue(); if (result.isVerifiedUsingV3Scheme()) { List v3Signers = result.getV3SchemeSigners(); if (v3Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); } // Compare certificates. checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result); // Compare digests. final byte[] digestFromV3 = pickBestDigestForV4( v3Signers.get(0).getContentDigests()); if (!Arrays.equals(digestFromV4, digestFromV3)) { result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); } } else if (result.isVerifiedUsingV2Scheme()) { List v2Signers = result.getV2SchemeSigners(); if (v2Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); } // Compare certificates. checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result); // Compare digests. final byte[] digestFromV2 = pickBestDigestForV4( v2Signers.get(0).getContentDigests()); if (!Arrays.equals(digestFromV4, digestFromV2)) { result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); } } else { throw new RuntimeException("V4 signature must be also verified with V2/V3"); } } // If the targetSdkVersion has a minimum required signature scheme version then verify // that the APK was signed with at least that version. try { if (androidManifest == null) { androidManifest = getAndroidManifestFromApk(apk, zipSections); } } catch (ApkFormatException e) { // If the manifest is not available then skip the minimum signature scheme requirement // to support bundle verification. } if (androidManifest != null) { int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( androidManifest.slice()); int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); // The platform currently only enforces a single minimum signature scheme version, but // when later platform versions support another minimum version this will need to be // expanded to verify the minimum based on the target and maximum SDK version. if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME && maxSdkVersion >= targetSdkVersion) { switch (minSchemeVersion) { case VERSION_APK_SIGNATURE_SCHEME_V2: if (result.isVerifiedUsingV2Scheme()) { break; } // Allow this case to fall through to the next as a signature satisfying a // later scheme version will also satisfy this requirement. case VERSION_APK_SIGNATURE_SCHEME_V3: if (result.isVerifiedUsingV3Scheme()) { break; } result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion, minSchemeVersion); } } } if (result.containsErrors()) { return result; } // Verified result.setVerified(); if (result.isVerifiedUsingV3Scheme()) { List v3Signers = result.getV3SchemeSigners(); result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate()); } else if (result.isVerifiedUsingV2Scheme()) { for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { result.addSignerCertificate(signerInfo.getCertificate()); } } else if (result.isVerifiedUsingV1Scheme()) { for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) { result.addSignerCertificate(signerInfo.getCertificate()); } } else { throw new RuntimeException( "APK verified, but has not verified using any of v1, v2 or v3 schemes"); } return result; } /** * Verifies and returns the minimum SDK version, either as provided to the builder or as read * from the {@code apk}'s AndroidManifest.xml. */ private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections) throws ApkFormatException, IOException { if (mMinSdkVersion != null) { if (mMinSdkVersion < 0) { throw new IllegalArgumentException( "minSdkVersion must not be negative: " + mMinSdkVersion); } if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { throw new IllegalArgumentException( "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + ")"); } return mMinSdkVersion; } ByteBuffer androidManifest = null; // Need to obtain minSdkVersion from the APK's AndroidManifest.xml if (androidManifest == null) { androidManifest = getAndroidManifestFromApk(apk, zipSections); } int minSdkVersion = ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); if (minSdkVersion > mMaxSdkVersion) { throw new IllegalArgumentException( "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + ")"); } return minSdkVersion; } /** * Returns the mapping of signature scheme version to signature scheme name for all signature * schemes starting from V2 supported by the {@code maxSdkVersion}. */ private static Map getSupportedSchemeNames(int maxSdkVersion) { Map supportedSchemeNames; if (maxSdkVersion >= AndroidSdkVersion.P) { supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; } else if (maxSdkVersion >= AndroidSdkVersion.N) { supportedSchemeNames = new HashMap<>(1); supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, SUPPORTED_APK_SIG_SCHEME_NAMES.get( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); } else { supportedSchemeNames = Collections.emptyMap(); } return supportedSchemeNames; } /** * Verifies the APK's source stamp signature and returns the result of the verification. * *

The APK's source stamp can be considered verified if the result's {@link * Result#isVerified} returns {@code true}. The details of the source stamp verification can * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the * verification fails additional details regarding the failure can be obtained from {@link * Result#getAllErrors()}}. */ public Result verifySourceStamp() { return verifySourceStamp(null); } /** * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result * of the verification. * *

A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, * if present, without verifying the actual source stamp certificate used to sign the source * stamp. This can be used to verify an APK contains a properly signed source stamp without * verifying a particular signer. * * @see #verifySourceStamp() */ public Result verifySourceStamp(String expectedCertDigest) { Closeable in = null; try { DataSource apk; if (mApkDataSource != null) { apk = mApkDataSource; } else if (mApkFile != null) { RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); in = f; apk = DataSources.asDataSource(f, 0, f.length()); } else { throw new IllegalStateException("APK not provided"); } return verifySourceStamp(apk, expectedCertDigest); } catch (IOException e) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, Issue.UNEXPECTED_EXCEPTION, e); } finally { if (in != null) { try { in.close(); } catch (IOException ignored) { } } } } /** * Verifies the provided {@code apk}'s source stamp signature, including verification of the * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and * returns the result of the verification. * * @see #verifySourceStamp(String) */ private Result verifySourceStamp(DataSource apk, String expectedCertDigest) { try { ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); // Attempt to obtain the source stamp's certificate digest from the APK. List cdRecords = V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); CentralDirectoryRecord sourceStampCdRecord = null; for (CentralDirectoryRecord cdRecord : cdRecords) { if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { sourceStampCdRecord = cdRecord; break; } } // If the source stamp's certificate digest is not available within the APK then the // source stamp cannot be verified; check if a source stamp signing block is in the // APK's signature block to determine the appropriate status to return. if (sourceStampCdRecord == null) { boolean stampSigningBlockFound; try { ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( ApkSigningBlockUtils.VERSION_SOURCE_STAMP); ApkSigningBlockUtils.findSignature(apk, zipSections, SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result); stampSigningBlockFound = true; } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { stampSigningBlockFound = false; } if (stampSigningBlockFound) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); } else { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING, Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); } } // Verify that the contents of the source stamp certificate digest match the expected // value, if provided. byte[] sourceStampCertificateDigest = LocalFileRecord.getUncompressedData( apk, sourceStampCdRecord, zipSections.getZipCentralDirectoryOffset()); if (expectedCertDigest != null) { String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest); if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus .CERT_DIGEST_MISMATCH, Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest, expectedCertDigest); } } Map> signatureSchemeApkContentDigests = new HashMap<>(); Map supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion); Set foundApkSigSchemeIds = new HashSet<>(2); Result result = new Result(); ApkSigningBlockUtils.Result v3Result = null; if (mMaxSdkVersion >= AndroidSdkVersion.P) { v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames, signatureSchemeApkContentDigests, VERSION_APK_SIGNATURE_SCHEME_V3, Math.max(minSdkVersion, AndroidSdkVersion.P)); if (v3Result != null && v3Result.containsErrors()) { result.mergeFrom(v3Result); return mergeSourceStampResult( Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, result); } } ApkSigningBlockUtils.Result v2Result = null; if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty())) { v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames, signatureSchemeApkContentDigests, VERSION_APK_SIGNATURE_SCHEME_V2, Math.max(minSdkVersion, AndroidSdkVersion.N)); if (v2Result != null && v2Result.containsErrors()) { result.mergeFrom(v2Result); return mergeSourceStampResult( Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, result); } } if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) { signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); } ApkSigResult sourceStampResult = V2SourceStampVerifier.verify( apk, zipSections, sourceStampCertificateDigest, signatureSchemeApkContentDigests, minSdkVersion, mMaxSdkVersion); result.mergeFrom(sourceStampResult); // Since the caller is only seeking to verify the source stamp the Result can be marked // as verified if the source stamp verification was successful. if (sourceStampResult.verified) { result.setVerified(); } else { // To prevent APK signature verification with a failed / missing source stamp the // source stamp verification will only log warnings; to allow the caller to capture // the failure reason treat all warnings as errors. result.setWarningsAsErrors(true); } return result; } catch (ApkFormatException | IOException | ZipFormatException e) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, Issue.MALFORMED_APK, e); } catch (NoSuchAlgorithmException e) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, Issue.UNEXPECTED_EXCEPTION, e); } catch (SignatureNotFoundException e) { return createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, Issue.SOURCE_STAMP_SIG_MISSING); } } /** * Creates and returns a {@code Result} that can be returned for source stamp verification * with the provided source stamp {@code verificationStatus}, and logs an error for the * specified {@code issue} and {@code params}. */ private static Result createSourceStampResultWithError( Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue, Object... params) { Result result = new Result(); result.addError(issue, params); return mergeSourceStampResult(verificationStatus, result); } /** * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the * source stamp status to the provided {@code verificationStatus}. */ private static Result mergeSourceStampResult( Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Result result) { result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus); return result; } /** * Obtains the APK content digest(s) and adds them to the provided {@code * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be * merged with a {@code Result} to notify the client of any errors. * *

Note, this method currently only supports signature scheme V2 and V3; to obtain the * content digests for V1 signatures use {@link * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a * signature scheme version other than V2 or V3 is provided a {@code null} value will be * returned. */ private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, ApkUtils.ZipSections zipSections, Set foundApkSigSchemeIds, Map supportedSchemeNames, Map> sigSchemeApkContentDigests, int apkSigSchemeVersion, int minSdkVersion) throws IOException, NoSuchAlgorithmException { if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2 || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) { return null; } ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion); SignatureInfo signatureInfo; try { int sigSchemeBlockId = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3 ? V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID : V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, sigSchemeBlockId, result); } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { return null; } foundApkSigSchemeIds.add(apkSigSchemeVersion); Set contentDigestsToVerify = new HashSet<>(1); if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock, contentDigestsToVerify, supportedSchemeNames, foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result); } else { V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock, contentDigestsToVerify, result); } Map apkContentDigests = new EnumMap<>( ContentDigestAlgorithm.class); for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) { for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : signerInfo.contentDigests) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( contentDigest.getSignatureAlgorithmId()); if (signatureAlgorithm == null) { continue; } apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), contentDigest.getValue()); } } sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests); return result; } private static void checkV4Certificate(List v4Certs, List v2v3Certs, Result result) { try { byte[] v4Cert = v4Certs.get(0).getEncoded(); byte[] cert = v2v3Certs.get(0).getEncoded(); if (!Arrays.equals(cert, v4Cert)) { result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH); } } catch (CertificateEncodingException e) { throw new RuntimeException("Failed to encode APK signer cert", e); } } private static byte[] pickBestDigestForV4( List contentDigests) { Map apkContentDigests = new HashMap<>(); collectApkContentDigests(contentDigests, apkContentDigests); return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests); } private static Map getApkContentDigestsFromSigningSchemeResult( ApkSigningBlockUtils.Result apkSigningSchemeResult) { Map apkContentDigests = new HashMap<>(); for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) { collectApkContentDigests(signerInfo.contentDigests, apkContentDigests); } return apkContentDigests; } private static Map getApkContentDigestFromV1SigningScheme( List cdRecords, DataSource apk, ApkUtils.ZipSections zipSections) throws IOException, ApkFormatException { CentralDirectoryRecord manifestCdRecord = null; Map 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); } } private static void collectApkContentDigests( List contentDigests, Map apkContentDigests) { for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); if (signatureAlgorithm == null) { continue; } ContentDigestAlgorithm contentDigestAlgorithm = signatureAlgorithm.getContentDigestAlgorithm(); apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue()); } } private static ByteBuffer getAndroidManifestFromApk( DataSource apk, ApkUtils.ZipSections zipSections) throws IOException, ApkFormatException { List cdRecords = V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); try { return ApkSigner.getAndroidManifestFromApk( cdRecords, apk.slice(0, zipSections.getZipCentralDirectoryOffset())); } catch (ZipFormatException e) { throw new ApkFormatException("Failed to read AndroidManifest.xml", e); } } private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { if (targetSdkVersion >= AndroidSdkVersion.R) { return VERSION_APK_SIGNATURE_SCHEME_V2; } return VERSION_JAR_SIGNATURE_SCHEME; } /** * Result of verifying an APKs signatures. The APK can be considered verified iff * {@link #isVerified()} returns {@code true}. */ public static class Result { private final List mErrors = new ArrayList<>(); private final List mWarnings = new ArrayList<>(); private final List mSignerCerts = new ArrayList<>(); private final List mV1SchemeSigners = new ArrayList<>(); private final List mV1SchemeIgnoredSigners = new ArrayList<>(); private final List mV2SchemeSigners = new ArrayList<>(); private final List mV3SchemeSigners = new ArrayList<>(); private final List mV4SchemeSigners = new ArrayList<>(); private SourceStampInfo mSourceStampInfo; private boolean mVerified; private boolean mVerifiedUsingV1Scheme; private boolean mVerifiedUsingV2Scheme; private boolean mVerifiedUsingV3Scheme; private boolean mVerifiedUsingV4Scheme; private boolean mSourceStampVerified; private boolean mWarningsAsErrors; private SigningCertificateLineage mSigningCertificateLineage; /** * Returns {@code true} if the APK's signatures verified. */ public boolean isVerified() { return mVerified; } private void setVerified() { mVerified = true; } /** * Returns {@code true} if the APK's JAR signatures verified. */ public boolean isVerifiedUsingV1Scheme() { return mVerifiedUsingV1Scheme; } /** * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. */ public boolean isVerifiedUsingV2Scheme() { return mVerifiedUsingV2Scheme; } /** * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified. */ public boolean isVerifiedUsingV3Scheme() { return mVerifiedUsingV3Scheme; } /** * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified. */ public boolean isVerifiedUsingV4Scheme() { return mVerifiedUsingV4Scheme; } /** * Returns {@code true} if the APK's SourceStamp signature verified. */ public boolean isSourceStampVerified() { return mSourceStampVerified; } /** * Returns the verified signers' certificates, one per signer. */ public List getSignerCertificates() { return mSignerCerts; } private void addSignerCertificate(X509Certificate cert) { mSignerCerts.add(cert); } /** * Returns information about JAR signers associated with the APK's signature. These are the * signers used by Android. * * @see #getV1SchemeIgnoredSigners() */ public List getV1SchemeSigners() { return mV1SchemeSigners; } /** * Returns information about JAR signers ignored by the APK's signature verification * process. These signers are ignored by Android. However, each signer's errors or warnings * will contain information about why they are ignored. * * @see #getV1SchemeSigners() */ public List getV1SchemeIgnoredSigners() { return mV1SchemeIgnoredSigners; } /** * Returns information about APK Signature Scheme v2 signers associated with the APK's * signature. */ public List getV2SchemeSigners() { return mV2SchemeSigners; } /** * Returns information about APK Signature Scheme v3 signers associated with the APK's * signature. * * Multiple signers represent different targeted platform versions, not * a signing identity of multiple signers. APK Signature Scheme v3 only supports single * signer identities. */ public List getV3SchemeSigners() { return mV3SchemeSigners; } private List getV4SchemeSigners() { return mV4SchemeSigners; } /** * Returns information about SourceStamp associated with the APK's signature. */ public SourceStampInfo getSourceStampInfo() { return mSourceStampInfo; } /** * Returns the combined SigningCertificateLineage associated with this APK's APK Signature * Scheme v3 signing block. */ public SigningCertificateLineage getSigningCertificateLineage() { return mSigningCertificateLineage; } void addError(Issue msg, Object... parameters) { mErrors.add(new IssueWithParams(msg, parameters)); } void addWarning(Issue msg, Object... parameters) { mWarnings.add(new IssueWithParams(msg, parameters)); } /** * Sets whether warnings should be treated as errors. */ void setWarningsAsErrors(boolean value) { mWarningsAsErrors = value; } /** * Returns errors encountered while verifying the APK's signatures. */ public List getErrors() { if (!mWarningsAsErrors) { return mErrors; } else { List allErrors = new ArrayList<>(); allErrors.addAll(mErrors); allErrors.addAll(mWarnings); return allErrors; } } /** * Returns warnings encountered while verifying the APK's signatures. */ public List getWarnings() { return mWarnings; } private void mergeFrom(V1SchemeVerifier.Result source) { mVerifiedUsingV1Scheme = source.verified; mErrors.addAll(source.getErrors()); mWarnings.addAll(source.getWarnings()); for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); } for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); } } 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: mVerifiedUsingV2Scheme = source.verified; for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); } break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: mVerifiedUsingV3Scheme = source.verified; for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); } mSigningCertificateLineage = source.signingCertificateLineage; break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4: mVerifiedUsingV4Scheme = source.verified; for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { mV4SchemeSigners.add(new V4SchemeSignerInfo(signer)); } break; case ApkSigningBlockUtils.VERSION_SOURCE_STAMP: mSourceStampVerified = source.verified; if (!source.signers.isEmpty()) { mSourceStampInfo = new SourceStampInfo(source.signers.get(0)); } break; default: throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); } } /** * 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; } if (mWarningsAsErrors && !mWarnings.isEmpty()) { return true; } if (!mV1SchemeSigners.isEmpty()) { for (V1SchemeSignerInfo signer : mV1SchemeSigners) { if (signer.containsErrors()) { return true; } if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { return true; } } } if (!mV2SchemeSigners.isEmpty()) { for (V2SchemeSignerInfo signer : mV2SchemeSigners) { if (signer.containsErrors()) { return true; } if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { return true; } } } if (!mV3SchemeSigners.isEmpty()) { for (V3SchemeSignerInfo signer : mV3SchemeSigners) { if (signer.containsErrors()) { return true; } if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { return true; } } } if (mSourceStampInfo != null) { if (mSourceStampInfo.containsErrors()) { return true; } if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) { return true; } } return false; } /** * Returns all errors for this result, including any errors from signature scheme signers * and the source stamp. */ public List getAllErrors() { List errors = new ArrayList<>(); errors.addAll(mErrors); if (mWarningsAsErrors) { errors.addAll(mWarnings); } if (!mV1SchemeSigners.isEmpty()) { for (V1SchemeSignerInfo signer : mV1SchemeSigners) { errors.addAll(signer.mErrors); if (mWarningsAsErrors) { errors.addAll(signer.getWarnings()); } } } if (!mV2SchemeSigners.isEmpty()) { for (V2SchemeSignerInfo signer : mV2SchemeSigners) { errors.addAll(signer.mErrors); if (mWarningsAsErrors) { errors.addAll(signer.getWarnings()); } } } if (!mV3SchemeSigners.isEmpty()) { for (V3SchemeSignerInfo signer : mV3SchemeSigners) { errors.addAll(signer.mErrors); if (mWarningsAsErrors) { errors.addAll(signer.getWarnings()); } } } if (mSourceStampInfo != null) { errors.addAll(mSourceStampInfo.getErrors()); if (mWarningsAsErrors) { errors.addAll(mSourceStampInfo.getWarnings()); } } return errors; } /** * Information about a JAR signer associated with the APK's signature. */ public static class V1SchemeSignerInfo { private final String mName; private final List mCertChain; private final String mSignatureBlockFileName; private final String mSignatureFileName; private final List mErrors; private final List mWarnings; private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { mName = result.name; mCertChain = result.certChain; mSignatureBlockFileName = result.signatureBlockFileName; mSignatureFileName = result.signatureFileName; mErrors = result.getErrors(); mWarnings = result.getWarnings(); } /** * Returns a user-friendly name of the signer. */ public String getName() { return mName; } /** * Returns the name of the JAR entry containing this signer's JAR signature block file. */ public String getSignatureBlockFileName() { return mSignatureBlockFileName; } /** * Returns the name of the JAR entry containing this signer's JAR signature file. */ public String getSignatureFileName() { return mSignatureFileName; } /** * Returns this signer'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 signer's public key. */ public X509Certificate getCertificate() { return mCertChain.isEmpty() ? null : mCertChain.get(0); } /** * Returns the certificate chain for the signer's public key. The certificate containing * the public key is first, followed by the certificate (if any) which issued the * signing certificate, and so forth. An empty list may be returned if an error was * encountered during verification (see {@link #containsErrors()}). */ public List getCertificateChain() { return mCertChain; } /** * Returns {@code true} if an error was encountered while verifying this signer's JAR * signature. Any error prevents the signer's signature from being considered verified. */ public boolean containsErrors() { return !mErrors.isEmpty(); } /** * Returns errors encountered while verifying this signer's JAR signature. Any error * prevents the signer's signature from being considered verified. */ public List getErrors() { return mErrors; } /** * Returns warnings encountered while verifying this signer's JAR signature. Warnings * do not prevent the signer's signature from being considered verified. */ public List getWarnings() { return mWarnings; } private void addError(Issue msg, Object... parameters) { mErrors.add(new IssueWithParams(msg, parameters)); } } /** * Information about an APK Signature Scheme v2 signer associated with the APK's signature. */ public static class V2SchemeSignerInfo { private final int mIndex; private final List mCerts; private final List mErrors; private final List mWarnings; private final List mContentDigests; private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { mIndex = result.index; mCerts = result.certs; mErrors = result.getErrors(); mWarnings = result.getWarnings(); mContentDigests = result.contentDigests; } /** * Returns this signer's {@code 0}-based index in the list of signers contained in the * APK's APK Signature Scheme v2 signature. */ public int getIndex() { return mIndex; } /** * Returns this signer'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 signer's public key. */ public X509Certificate getCertificate() { return mCerts.isEmpty() ? null : mCerts.get(0); } /** * Returns this signer's certificates. The first certificate is for the signer's public * key. An empty list may be returned if an error was encountered during verification * (see {@link #containsErrors()}). */ public List getCertificates() { return mCerts; } private void addError(Issue msg, Object... parameters) { mErrors.add(new IssueWithParams(msg, parameters)); } public boolean containsErrors() { return !mErrors.isEmpty(); } public List getErrors() { return mErrors; } public List getWarnings() { return mWarnings; } public List getContentDigests() { return mContentDigests; } } /** * Information about an APK Signature Scheme v3 signer associated with the APK's signature. */ public static class V3SchemeSignerInfo { private final int mIndex; private final List mCerts; private final List mErrors; private final List mWarnings; private final List mContentDigests; private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { mIndex = result.index; mCerts = result.certs; mErrors = result.getErrors(); mWarnings = result.getWarnings(); mContentDigests = result.contentDigests; } /** * Returns this signer's {@code 0}-based index in the list of signers contained in the * APK's APK Signature Scheme v3 signature. */ public int getIndex() { return mIndex; } /** * Returns this signer'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 signer's public key. */ public X509Certificate getCertificate() { return mCerts.isEmpty() ? null : mCerts.get(0); } /** * Returns this signer's certificates. The first certificate is for the signer's public * key. An empty list may be returned if an error was encountered during verification * (see {@link #containsErrors()}). */ public List getCertificates() { return mCerts; } public boolean containsErrors() { return !mErrors.isEmpty(); } public List getErrors() { return mErrors; } public List getWarnings() { return mWarnings; } public List getContentDigests() { return mContentDigests; } } /** * Information about an APK Signature Scheme V4 signer associated with the APK's * signature. */ public static class V4SchemeSignerInfo { private final int mIndex; private final List mCerts; private final List mErrors; private final List mWarnings; private final List mContentDigests; private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { mIndex = result.index; mCerts = result.certs; mErrors = result.getErrors(); mWarnings = result.getWarnings(); mContentDigests = result.contentDigests; } /** * Returns this signer's {@code 0}-based index in the list of signers contained in the * APK's APK Signature Scheme v3 signature. */ public int getIndex() { return mIndex; } /** * Returns this signer'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 signer's public key. */ public X509Certificate getCertificate() { return mCerts.isEmpty() ? null : mCerts.get(0); } /** * Returns this signer's certificates. The first certificate is for the signer's public * key. An empty list may be returned if an error was encountered during verification * (see {@link #containsErrors()}). */ public List getCertificates() { return mCerts; } public boolean containsErrors() { return !mErrors.isEmpty(); } public List getErrors() { return mErrors; } public List getWarnings() { return mWarnings; } public List getContentDigests() { return mContentDigests; } } /** * Information about SourceStamp associated with the APK's signature. */ public static class SourceStampInfo { public enum SourceStampVerificationStatus { /** The stamp is present and was successfully verified. */ STAMP_VERIFIED, /** The stamp is present but failed verification. */ STAMP_VERIFICATION_FAILED, /** The expected cert digest did not match the digest in the APK. */ CERT_DIGEST_MISMATCH, /** The stamp is not present at all. */ STAMP_MISSING, /** The stamp is at least partially present, but was not able to be verified. */ STAMP_NOT_VERIFIED, /** The stamp was not able to be verified due to an unexpected error. */ VERIFICATION_ERROR } private final List mCertificates; private final List mCertificateLineage; private final List mErrors; private final List mWarnings; private final SourceStampVerificationStatus mSourceStampVerificationStatus; private SourceStampInfo(ApkSignerInfo result) { mCertificates = result.certs; mCertificateLineage = result.certificateLineage; mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( result.getErrors()); mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( result.getWarnings()); if (mErrors.isEmpty() && mWarnings.isEmpty()) { mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED; } else { mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED; } } SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) { mCertificates = Collections.emptyList(); mCertificateLineage = Collections.emptyList(); mErrors = Collections.emptyList(); mWarnings = Collections.emptyList(); mSourceStampVerificationStatus = sourceStampVerificationStatus; } /** * 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 list containing all of the certificates in the stamp certificate lineage. */ public List getCertificatesInLineage() { return mCertificateLineage; } public boolean containsErrors() { return !mErrors.isEmpty(); } public List getErrors() { return mErrors; } public List getWarnings() { return mWarnings; } /** * Returns the reason for any source stamp verification failures, or {@code * STAMP_VERIFIED} if the source stamp was successfully verified. */ public SourceStampVerificationStatus getSourceStampVerificationStatus() { return mSourceStampVerificationStatus; } } } /** * Error or warning encountered while verifying an APK's signatures. */ public enum Issue { /** * APK is not JAR-signed. */ JAR_SIG_NO_SIGNATURES("No JAR signatures"), /** * APK does not contain any entries covered by JAR signatures. */ JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"), /** * APK contains multiple entries with the same name. * *

*/ JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"), /** * JAR manifest contains a section with a duplicate name. * * */ JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"), /** * JAR manifest contains a section without a name. * * */ JAR_SIG_UNNNAMED_MANIFEST_SECTION( "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"), /** * JAR signature file contains a section without a name. * * */ JAR_SIG_UNNNAMED_SIG_FILE_SECTION( "Malformed %1$s: invidual section #%2$d does not have a name"), /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */ JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"), /** * JAR manifest references an entry which is not there in the APK. * * */ JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST( "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"), /** * JAR manifest does not list a digest for the specified entry. * * */ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"), /** * JAR signature does not list a digest for the specified entry. * * */ JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"), /** * The specified JAR entry is not covered by JAR signature. * * */ JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"), /** * JAR signature uses different set of signers to protect the two specified ZIP entries. * * */ JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH( "Entries %1$s and %3$s are signed with different sets of signers" + " : <%2$s> vs <%4$s>"), /** * Digest of the specified ZIP entry's data does not match the digest expected by the JAR * signature. * * */ JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY( "%2$s digest of %1$s does not match the digest specified in %3$s" + ". Expected: <%5$s>, actual: <%4$s>"), /** * Digest of the JAR manifest main section did not verify. * * */ JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY( "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest" + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"), /** * Digest of the specified JAR manifest section does not match the digest expected by the * JAR signature. * * */ JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY( "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest" + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"), /** * JAR signature file does not contain the whole-file digest of the JAR manifest file. The * digest speeds up verification of JAR signature. * * */ JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE( "%1$s does not specify digest of META-INF/MANIFEST.MF" + ". This slows down verification."), /** * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not * contain protections against stripping of these newer scheme signatures. * * */ JAR_SIG_NO_APK_SIG_STRIP_PROTECTION( "APK is signed using APK Signature Scheme v2 but these signatures may be stripped" + " without being detected because %1$s does not contain anti-stripping" + " protections."), /** * JAR signature of the signer is missing a file/entry. * * */ JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"), /** * An exception was encountered while verifying JAR signature contained in a signature block * against the signature file. * * */ JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"), /** * JAR signature contains unsupported digest algorithm. * * */ JAR_SIG_UNSUPPORTED_SIG_ALG( "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which" + " is not supported on API Level(s) %4$s for which this APK is being" + " verified"), /** * An exception was encountered while parsing JAR signature contained in a signature block. * * */ JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"), /** * An exception was encountered while parsing a certificate contained in the JAR signature * block. * * */ JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"), /** * JAR signature contained in a signature block file did not verify against the signature * file. * * */ JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"), /** * JAR signature contains no verified signers. * * */ JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"), /** * JAR signature file contains a section with a duplicate name. * * */ JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"), /** * JAR signature file's main section doesn't contain the mandatory Signature-Version * attribute. * * */ JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE( "Malformed %1$s: missing Signature-Version attribute"), /** * JAR signature file references an unknown APK signature scheme ID. * * */ JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID( "JAR signature %1$s references unknown APK signature scheme ID: %2$d"), /** * JAR signature file indicates that the APK is supposed to be signed with a supported APK * signature scheme (in addition to the JAR signature) but no such signature was found in * the APK. * * */ JAR_SIG_MISSING_APK_SIG_REFERENCED( "JAR signature %1$s indicates the APK is signed using %3$s but no such signature" + " was found. Signature stripped?"), /** * JAR entry is not covered by signature and thus unauthorized modifications to its contents * will not be detected. * * */ JAR_SIG_UNPROTECTED_ZIP_ENTRY( "%1$s not protected by signature. Unauthorized modifications to this JAR entry" + " will not be detected. Delete or move the entry outside of META-INF/."), /** * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK * Signature Scheme v2 signature from this signer, but does not contain a JAR signature * from this signer. */ JAR_SIG_MISSING("No JAR signature from this signer"), /** * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but * no such signature was found. * * */ NO_SIG_FOR_TARGET_SANDBOX_VERSION( "Missing APK Signature Scheme v2 signature required for target sandbox version" + " %1$d"), /** * APK is targeting an SDK version that requires a minimum signature scheme version, but the * APK is not signed with that version or later. * * */ MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET( "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is" + " not signed with this or a later signature scheme"), /** * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR * signature from this signer, but does not contain an APK Signature Scheme v2 signature * from this signer. */ V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"), /** * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. */ V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), /** * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 * signature. */ V2_SIG_MALFORMED_SIGNER("Malformed signer block"), /** * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be * parsed. * * */ V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), /** * This APK Signature Scheme v2 signer's certificate could not be parsed. * * */ V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), /** * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 * signature. * * */ V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), /** * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 * signature. * * */ V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), /** * This APK Signature Scheme v2 signer contains a malformed additional attribute. * * */ V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), /** * APK Signature Scheme v2 signature references an unknown APK signature scheme ID. * * */ V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID( "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: " + "%2$d"), /** * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a * supported APK signature scheme (in addition to the v2 signature) but no such signature * was found in the APK. * * */ V2_SIG_MISSING_APK_SIG_REFERENCED( "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but " + "no such signature was found. Signature stripped?"), /** * APK Signature Scheme v2 signature contains no signers. */ V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), /** * This APK Signature Scheme v2 signer contains a signature produced using an unknown * algorithm. * * */ V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), /** * This APK Signature Scheme v2 signer contains an unknown additional attribute. * * */ V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), /** * An exception was encountered while verifying APK Signature Scheme v2 signature of this * signer. * * */ V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), /** * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. * * */ V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), /** * This APK Signature Scheme v2 signer offers no signatures. */ V2_SIG_NO_SIGNATURES("No signatures"), /** * This APK Signature Scheme v2 signer offers signatures but none of them are supported. */ V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"), /** * This APK Signature Scheme v2 signer offers no certificates. */ V2_SIG_NO_CERTIFICATES("No certificates"), /** * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does * not match the public key listed in the signatures record. * * */ V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), /** * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures * record do not match the signature algorithms listed in the signatures record. * * */ V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( "Signature algorithms mismatch between signatures and digests records" + ": %1$s vs %2$s"), /** * The APK's digest does not match the digest contained in the APK Signature Scheme v2 * signature. * * */ V2_SIG_APK_DIGEST_DID_NOT_VERIFY( "APK integrity check failed. %1$s digest mismatch." + " Expected: <%2$s>, actual: <%3$s>"), /** * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature. */ V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"), /** * Failed to parse this signer's signer block contained in the APK Signature Scheme v3 * signature. */ V3_SIG_MALFORMED_SIGNER("Malformed signer block"), /** * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be * parsed. * * */ V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), /** * This APK Signature Scheme v3 signer's certificate could not be parsed. * * */ V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), /** * Failed to parse this signer's signature record contained in the APK Signature Scheme v3 * signature. * * */ V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"), /** * Failed to parse this signer's digest record contained in the APK Signature Scheme v3 * signature. * * */ V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"), /** * This APK Signature Scheme v3 signer contains a malformed additional attribute. * * */ V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), /** * APK Signature Scheme v3 signature contains no signers. */ V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"), /** * APK Signature Scheme v3 signature contains multiple signers (only one allowed per * platform version). */ V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single " + " platform version."), /** * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers * found, where only one may be used with APK Signature Scheme v3 */ V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK " + " Signature Scheme v3 signer. Only one allowed."), /** * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers, * or have them as the root of its signing certificate history */ V3_SIG_PAST_SIGNERS_MISMATCH( "v3 signer differs from v1/v2 signer without proper signing certificate lineage."), /** * This APK Signature Scheme v3 signer contains a signature produced using an unknown * algorithm. * * */ V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), /** * This APK Signature Scheme v3 signer contains an unknown additional attribute. * * */ V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), /** * An exception was encountered while verifying APK Signature Scheme v3 signature of this * signer. * * */ V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), /** * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK * versions. * * */ V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature " + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"), /** * APK Signature Scheme v3 signature over this signer's signed-data block did not verify. * * */ V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), /** * This APK Signature Scheme v3 signer offers no signatures. */ V3_SIG_NO_SIGNATURES("No signatures"), /** * This APK Signature Scheme v3 signer offers signatures but none of them are supported. */ V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), /** * This APK Signature Scheme v3 signer offers no certificates. */ V3_SIG_NO_CERTIFICATES("No certificates"), /** * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data * does not match the minSdkVersion listed in the signatures record. * * */ V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( "minSdkVersion mismatch between signed data and signature record:" + " <%1$s> vs <%2$s>"), /** * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data * does not match the maxSdkVersion listed in the signatures record. * * */ V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( "maxSdkVersion mismatch between signed data and signature record:" + " <%1$s> vs <%2$s>"), /** * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does * not match the public key listed in the signatures record. * * */ V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), /** * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures * record do not match the signature algorithms listed in the signatures record. * * */ V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( "Signature algorithms mismatch between signatures and digests records" + ": %1$s vs %2$s"), /** * The APK's digest does not match the digest contained in the APK Signature Scheme v3 * signature. * * */ V3_SIG_APK_DIGEST_DID_NOT_VERIFY( "APK integrity check failed. %1$s digest mismatch." + " Expected: <%2$s>, actual: <%3$s>"), /** * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with * signature(s) that did not verify. */ V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation" + " record with signature(s) that did not verify."), /** * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3 * signature's additional attributes section. */ V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the " + "APK Signature Scheme v3 signature's additional attributes section."), /** * The APK's signing certificate does not match the terminal node in the provided * proof-of-rotation structure describing the signing certificate history */ V3_SIG_POR_CERT_MISMATCH( "APK signing certificate differs from the associated certificate found in the " + "signer's SigningCertificateLineage."), /** * The APK Signature Scheme v3 signers encountered do not offer a continuous set of * supported platform versions. Either they overlap, resulting in potentially two * acceptable signers for a platform version, or there are holes which would create problems * in the event of platform version upgrades. */ V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + "versions are not continuous."), /** * The APK Signature Scheme v3 signers don't cover all requested SDK versions. * * */ V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + "versions do not cover the entire desired range. Found min: %1$s max %2$s"), /** * The SigningCertificateLineages for different platform versions using APK Signature Scheme * v3 do not go together. Specifically, each should be a subset of another, with the size * of each increasing as the platform level increases. */ V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions" + " using APK Signature Scheme v3 are not all a part of the same overall lineage."), /** * APK Signing Block contains an unknown entry. * * */ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"), /** * Failed to parse this signer's signature record contained in the APK Signature Scheme * V4 signature. * * */ V4_SIG_MALFORMED_SIGNERS( "V4 signature has malformed signer block"), /** * This APK Signature Scheme V4 signer contains a signature produced using an * unknown algorithm. * * */ V4_SIG_UNKNOWN_SIG_ALGORITHM( "V4 signature has unknown signing algorithm: %1$#x"), /** * This APK Signature Scheme V4 signer offers no signatures. */ V4_SIG_NO_SIGNATURES( "V4 signature has no signature found"), /** * This APK Signature Scheme V4 signer offers signatures but none of them are * supported. */ V4_SIG_NO_SUPPORTED_SIGNATURES( "V4 signature has no supported signature"), /** * APK Signature Scheme v3 signature over this signer's signed-data block did not verify. * * */ V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), /** * An exception was encountered while verifying APK Signature Scheme v3 signature of this * signer. * * */ V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), /** * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be * parsed. * * */ V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), /** * This APK Signature Scheme V4 signer's certificate could not be parsed. * * */ V4_SIG_MALFORMED_CERTIFICATE( "V4 signature has malformed certificate"), /** * This APK Signature Scheme V4 signer offers no certificate. */ V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"), /** * This APK Signature Scheme V4 signer's public key listed in the signer's * certificate does not match the public key listed in the signature proto. * * */ V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"), /** * The APK's hash root (aka digest) does not match the hash root contained in the Signature * Scheme V4 signature. * * */ V4_SIG_APK_ROOT_DID_NOT_VERIFY( "V4 signature's hash tree root (content digest) did not verity"), /** * The APK's hash tree does not match the hash tree contained in the Signature * Scheme V4 signature. * * */ V4_SIG_APK_TREE_DID_NOT_VERIFY( "V4 signature's hash tree did not verity"), /** * Using more than one Signer to sign APK Signature Scheme V4 signature. */ V4_SIG_MULTIPLE_SIGNERS( "V4 signature only supports one signer"), /** * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer * used to sign APK Signature Scheme V4 signature. */ V4_SIG_V2_V3_SIGNERS_MISMATCH( "V4 signature and V2/V3 signature have mismatched certificates"), V4_SIG_V2_V3_DIGESTS_MISMATCH( "V4 signature and V2/V3 signature have mismatched digests"), /** * The v4 signature format version isn't the same as the tool's current version, something * may go wrong. */ V4_SIG_VERSION_NOT_CURRENT( "V4 signature format version %1$d is different from the tool's current " + "version %2$d"), /** * The APK does not contain the source stamp certificate digest file nor the signature block * when verification expected a source stamp to be present. */ SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING( "Neither the source stamp certificate digest file nor the signature block are " + "present in the APK"), /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"), /** * SourceStamp's certificate could not be parsed. * * */ SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"), /** Failed to parse SourceStamp's signature. */ SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"), /** * SourceStamp contains a signature produced using an unknown algorithm. * * */ SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), /** * An exception was encountered while verifying SourceStamp signature. * * */ SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), /** * SourceStamp signature block did not verify. * * */ SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), /** SourceStamp offers no signatures. */ SOURCE_STAMP_NO_SIGNATURE("No signature"), /** * SourceStamp offers an unsupported signature. * */ SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"), /** * SourceStamp's certificate listed in the APK signing block does not match the certificate * listed in the SourceStamp file in the APK. * * */ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK( "Certificate mismatch between SourceStamp block in APK signing block and" + " SourceStamp file in APK: <%1$s> vs <%2$s>"), /** * The APK contains a source stamp signature block without the expected certificate digest * in the APK contents. */ SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST( "A source stamp signature block was found without a corresponding certificate " + "digest in the APK"), /** * When verifying just the source stamp, the certificate digest in the APK does not match * the expected digest. * */ SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH( "The source stamp certificate digest in the APK, %1$s, does not match the " + "expected digest, %2$s"), /** * Source stamp block contains a malformed attribute. * * */ SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"), /** * Source stamp block contains an unknown attribute. * * */ SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"), /** * Failed to parse the SigningCertificateLineage structure in the source stamp * attributes section. */ SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage " + "structure in the source stamp attributes section."), /** * The source stamp certificate does not match the terminal node in the provided * proof-of-rotation structure describing the stamp certificate history. */ SOURCE_STAMP_POR_CERT_MISMATCH( "APK signing certificate differs from the associated certificate found in the " + "signer's SigningCertificateLineage."), /** * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record * with signature(s) that did not verify. */ SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute " + "contains a proof-of-rotation record with signature(s) that did not verify."), /** * The APK could not be properly parsed due to a ZIP or APK format exception. * */ MALFORMED_APK( "Malformed APK; the following exception was caught when attempting to parse the " + "APK: %1$s"), /** * An unexpected exception was caught when attempting to verify the signature(s) within the * APK. * */ UNEXPECTED_EXCEPTION( "An unexpected exception was caught when verifying the signature: %1$s"); private final String mFormat; Issue(String format) { mFormat = format; } /** * Returns the format string suitable for combining the parameters of this issue into a * readable string. See {@link java.util.Formatter} for format. */ private String getFormat() { return mFormat; } } /** * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted * form. */ public static class IssueWithParams extends ApkVerificationIssue { private final Issue mIssue; private final Object[] mParams; /** * Constructs a new {@code IssueWithParams} of the specified type and with provided * parameters. */ public IssueWithParams(Issue issue, Object[] params) { super(issue.mFormat, params); mIssue = issue; mParams = params; } /** * Returns the type of this issue. */ public Issue getIssue() { return mIssue; } /** * Returns the parameters of this issue. */ public Object[] getParams() { return mParams.clone(); } /** * Returns a readable form of this issue. */ @Override public String toString() { return String.format(mIssue.getFormat(), mParams); } } /** * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate * on the contents of the arrays rather than on references. */ private static class ByteArray { private final byte[] mArray; private final int mHashCode; private ByteArray(byte[] arr) { mArray = arr; mHashCode = Arrays.hashCode(mArray); } @Override public int hashCode() { return mHashCode; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ByteArray)) { return false; } ByteArray other = (ByteArray) obj; if (hashCode() != other.hashCode()) { return false; } if (!Arrays.equals(mArray, other.mArray)) { return false; } return true; } } /** * Builder of {@link ApkVerifier} instances. * *

The resulting verifier by default checks whether the APK will verify on all platform * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}. */ public static class Builder { private final File mApkFile; private final DataSource mApkDataSource; private File mV4SignatureFile; private Integer mMinSdkVersion; private int mMaxSdkVersion = Integer.MAX_VALUE; /** * Constructs a new {@code Builder} for verifying the provided APK file. */ public Builder(File apk) { if (apk == null) { throw new NullPointerException("apk == null"); } mApkFile = apk; mApkDataSource = null; } /** * Constructs a new {@code Builder} for verifying the provided 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 is verified. APK verification * will confirm that the APK is expected to install successfully on all known Android * platforms starting from the platform version with the provided API Level. The upper end * of the platform versions range can be modified via * {@link #setMaxCheckedPlatformVersion(int)}. * *

This method is useful for overriding the default behavior which checks that the APK * will verify on all platform versions supported by the APK, as specified by * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}. * * @param minSdkVersion API Level of the oldest platform for which to verify the APK * @see #setMinCheckedPlatformVersion(int) */ public Builder setMinCheckedPlatformVersion(int minSdkVersion) { mMinSdkVersion = minSdkVersion; return this; } /** * Sets the newest Android platform version for which the APK is verified. APK verification * will confirm that the APK is expected to install successfully on all platform versions * supported by the APK up until and including the provided version. 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 Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { mMaxSdkVersion = maxSdkVersion; return this; } public Builder setV4SignatureFile(File v4SignatureFile) { mV4SignatureFile = v4SignatureFile; return this; } /** * Returns an {@link ApkVerifier} initialized according to the configuration of this * builder. */ public ApkVerifier build() { return new ApkVerifier( mApkFile, mApkDataSource, mV4SignatureFile, mMinSdkVersion, mMaxSdkVersion); } } /** * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link * IssueWithParams} equivalent. */ public static class ApkVerificationIssueAdapter { private ApkVerificationIssueAdapter() { } // This field is visible for testing static final Map sVerificationIssueIdToIssue = new HashMap<>(); static { sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS, Issue.V2_SIG_MALFORMED_SIGNERS); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS, Issue.V2_SIG_NO_SIGNERS); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER, Issue.V2_SIG_MALFORMED_SIGNER); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE, Issue.V2_SIG_MALFORMED_SIGNATURE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES, Issue.V2_SIG_NO_SIGNATURES); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE, Issue.V2_SIG_MALFORMED_CERTIFICATE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES, Issue.V2_SIG_NO_CERTIFICATES); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST, Issue.V2_SIG_MALFORMED_DIGEST); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS, Issue.V3_SIG_MALFORMED_SIGNERS); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS, Issue.V3_SIG_NO_SIGNERS); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER, Issue.V3_SIG_MALFORMED_SIGNER); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE, Issue.V3_SIG_MALFORMED_SIGNATURE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES, Issue.V3_SIG_NO_SIGNATURES); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE, Issue.V3_SIG_MALFORMED_CERTIFICATE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES, Issue.V3_SIG_NO_CERTIFICATES); sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST, Issue.V3_SIG_MALFORMED_DIGEST); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE, Issue.SOURCE_STAMP_NO_SIGNATURE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, Issue.SOURCE_STAMP_DID_NOT_VERIFY); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, Issue.SOURCE_STAMP_VERIFY_EXCEPTION); sVerificationIssueIdToIssue.put( ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); sVerificationIssueIdToIssue.put( ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST, Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); sVerificationIssueIdToIssue.put( ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING, Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); sVerificationIssueIdToIssue.put( ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); sVerificationIssueIdToIssue.put( ApkVerificationIssue .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK, Issue.MALFORMED_APK); sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION, Issue.UNEXPECTED_EXCEPTION); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING, Issue.SOURCE_STAMP_SIG_MISSING); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE, Issue.SOURCE_STAMP_MALFORMED_LINEAGE); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH, Issue.SOURCE_STAMP_POR_CERT_MISMATCH); sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY, Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY); sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES, Issue.JAR_SIG_NO_SIGNATURES); sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, Issue.JAR_SIG_PARSE_EXCEPTION); } /** * Converts the provided {@code verificationIssues} to a {@code List} of corresponding * {@link IssueWithParams} instances. */ public static List getIssuesFromVerificationIssues( List verificationIssues) { List 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; } } }