blob: 5101f20b1c396aa5c914dc9983579a3a140b6ac8 [file] [log] [blame]
/*
* 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 com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* APK signature verifier which mimics the behavior of the Android platform.
*
* <p>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.
*
* <p>Use {@link Builder} to obtain instances of this verifier.
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
public class ApkVerifier {
private static final int APK_SIGNATURE_SCHEME_V2_ID = 2;
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
private final File mApkFile;
private final DataSource mApkDataSource;
private final Integer mMinSdkVersion;
private final int mMaxSdkVersion;
private ApkVerifier(
File apkFile,
DataSource apkDataSource,
Integer minSdkVersion,
int maxSdkVersion) {
mApkFile = apkFile;
mApkDataSource = apkDataSource;
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.
*
* <p>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();
}
}
}
/**
* 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 {
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
+ ")");
}
}
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;
if (mMinSdkVersion != null) {
// No need to obtain minSdkVersion from the APK's AndroidManifest.xml
minSdkVersion = mMinSdkVersion;
} else {
// Need to obtain minSdkVersion from the APK's AndroidManifest.xml
if (androidManifest == null) {
androidManifest = getAndroidManifestFromApk(apk, zipSections);
}
minSdkVersion =
ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
if (minSdkVersion > mMaxSdkVersion) {
throw new IllegalArgumentException(
"minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+ mMaxSdkVersion + ")");
}
}
Result result = new Result();
// Android N and newer attempts to verify APK Signature Scheme v2 signature of the APK.
// If the signature is not found, it falls back to JAR signature verification. If the
// signature is found but does not verify, the APK is rejected.
Set<Integer> foundApkSigSchemeIds;
if (maxSdkVersion >= AndroidSdkVersion.N) {
foundApkSigSchemeIds = new HashSet<>(1);
try {
ApkSigningBlockUtils.Result v2Result =
V2SchemeVerifier.verify(apk, zipSections,
Math.max(minSdkVersion, AndroidSdkVersion.N), maxSdkVersion);
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
result.mergeFrom(v2Result);
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {}
if (result.containsErrors()) {
return result;
}
} else {
foundApkSigSchemeIds = Collections.emptySet();
}
// 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);
}
}
}
// 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,
SUPPORTED_APK_SIG_SCHEME_NAMES,
foundApkSigSchemeIds,
minSdkVersion,
maxSdkVersion);
result.mergeFrom(v1Result);
}
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<Result.V1SchemeSignerInfo> v1Signers =
new ArrayList<>(result.getV1SchemeSigners());
ArrayList<Result.V2SchemeSignerInfo> v2Signers =
new ArrayList<>(result.getV2SchemeSigners());
ArrayList<ByteArray> v1SignerCerts = new ArrayList<>();
ArrayList<ByteArray> v2SignerCerts = new ArrayList<>();
for (Result.V1SchemeSignerInfo signer : v1Signers) {
try {
v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
} catch (CertificateEncodingException e) {
throw new RuntimeException(
"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 RuntimeException(
"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 (result.containsErrors()) {
return result;
}
// Verified
result.setVerified();
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 considered verified, but has not verified using either v1 or v2 schemes");
}
return result;
}
private static ByteBuffer getAndroidManifestFromApk(
DataSource apk, ApkUtils.ZipSections zipSections)
throws IOException, ApkFormatException {
List<CentralDirectoryRecord> 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);
}
}
/**
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
* AndroidManifest.xml.
*/
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
/**
* Returns the security sandbox version targeted by an APK with the provided
* {@code AndroidManifest.xml}.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws ApkFormatException if an error occurred while determining the version
*/
private static int getTargetSandboxVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// Return the value of the android:targetSandboxVersion attribute of the top-level manifest
// element
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 1)
&& ("manifest".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
// In each manifest element, targetSandboxVersion defaults to 1
int result = 1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i)
== TARGET_SANDBOX_VERSION_ATTR_ID) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
result = parser.getAttributeIntValue(i);
break;
default:
throw new ApkFormatException(
"Failed to determine APK's target sandbox version"
+ ": unsupported value type of"
+ " AndroidManifest.xml"
+ " android:targetSandboxVersion"
+ ". Only integer values supported.");
}
break;
}
}
return result;
}
eventType = parser.next();
}
throw new ApkFormatException(
"Failed to determine APK's target sandbox version"
+ " : no manifest element in AndroidManifest.xml");
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Failed to determine APK's target sandbox version"
+ ": malformed AndroidManifest.xml",
e);
}
}
/**
* 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<IssueWithParams> mErrors = new ArrayList<>();
private final List<IssueWithParams> mWarnings = new ArrayList<>();
private final List<X509Certificate> mSignerCerts = new ArrayList<>();
private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>();
private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
private boolean mVerified;
private boolean mVerifiedUsingV1Scheme;
private boolean mVerifiedUsingV2Scheme;
/**
* 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 the verified signers' certificates, one per signer.
*/
public List<X509Certificate> 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<V1SchemeSignerInfo> 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<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() {
return mV1SchemeIgnoredSigners;
}
/**
* Returns information about APK Signature Scheme v2 signers associated with the APK's
* signature.
*/
public List<V2SchemeSignerInfo> getV2SchemeSigners() {
return mV2SchemeSigners;
}
void addError(Issue msg, Object... parameters) {
mErrors.add(new IssueWithParams(msg, parameters));
}
/**
* Returns errors encountered while verifying the APK's signatures.
*/
public List<IssueWithParams> getErrors() {
return mErrors;
}
/**
* Returns warnings encountered while verifying the APK's signatures.
*/
public List<IssueWithParams> 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(ApkSigningBlockUtils.Result source) {
mVerifiedUsingV2Scheme = source.verified;
mErrors.addAll(source.getErrors());
mWarnings.addAll(source.getWarnings());
for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
}
}
/**
* 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 (!mV1SchemeSigners.isEmpty()) {
for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
if (signer.containsErrors()) {
return true;
}
}
}
if (!mV2SchemeSigners.isEmpty()) {
for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
if (signer.containsErrors()) {
return true;
}
}
}
return false;
}
/**
* Information about a JAR signer associated with the APK's signature.
*/
public static class V1SchemeSignerInfo {
private final String mName;
private final List<X509Certificate> mCertChain;
private final String mSignatureBlockFileName;
private final String mSignatureFileName;
private final List<IssueWithParams> mErrors;
private final List<IssueWithParams> 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()}.
*
* <p>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<X509Certificate> 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<IssueWithParams> 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<IssueWithParams> 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<X509Certificate> mCerts;
private final List<IssueWithParams> mErrors;
private final List<IssueWithParams> mWarnings;
private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
mIndex = result.index;
mCerts = result.certs;
mErrors = result.getErrors();
mWarnings = result.getWarnings();
}
/**
* 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()}.
*
* <p>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<X509Certificate> getCertificates() {
return mCerts;
}
private void addError(Issue msg, Object... parameters) {
mErrors.add(new IssueWithParams(msg, parameters));
}
public boolean containsErrors() {
return !mErrors.isEmpty();
}
public List<IssueWithParams> getErrors() {
return mErrors;
}
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
}
}
/**
* Error or warning encountered while verifying an APK's signatures.
*/
public static 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.
*
* <ul>
* <li>Parameter 1: name ({@code String})</li>
* </ul>
*/
JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
/**
* JAR manifest contains a section with a duplicate name.
*
* <ul>
* <li>Parameter 1: section name ({@code String})</li>
* </ul>
*/
JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
/**
* JAR manifest contains a section without a name.
*
* <ul>
* <li>Parameter 1: section index (1-based) ({@code Integer})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: signature file name ({@code String})</li>
* <li>Parameter 2: section index (1-based) ({@code Integer})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* <li>Parameter 2: signature file name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: first entry name ({@code String})</li>
* <li>Parameter 2: first entry signer names ({@code List<String>})</li>
* <li>Parameter 3: second entry name ({@code String})</li>
* <li>Parameter 4: second entry signer names ({@code List<String>})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
* <li>Parameter 3: name of the entry in which the expected digest is specified
* ({@code String})</li>
* <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
* <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li>
* <li>Parameter 2: name of the entry in which the expected digest is specified
* ({@code String})</li>
* <li>Parameter 3: base64-encoded actual digest ({@code String})</li>
* <li>Parameter 4: base64-encoded expected digest ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: section name ({@code String})</li>
* <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
* <li>Parameter 3: name of the signature file in which the expected digest is specified
* ({@code String})</li>
* <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
* <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature file ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature file ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the encountered file ({@code String})</li>
* <li>Parameter 2: name of the missing file ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* <li>Parameter 2: name of the signature file ({@code String})</li>
* <li>Parameter 3: exception ({@code Throwable})</li>
* </ul>
*/
JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
/**
* JAR signature contains unsupported digest algorithm.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* <li>Parameter 2: digest algorithm OID ({@code String})</li>
* <li>Parameter 3: signature algorithm OID ({@code String})</li>
* <li>Parameter 4: API Levels on which this combination of algorithms is not supported
* ({@code String})</li>
* <li>Parameter 5: user-friendly variant of digest algorithm ({@code String})</li>
* <li>Parameter 6: user-friendly variant of signature algorithm ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* <li>Parameter 2: exception ({@code Throwable})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* <li>Parameter 2: exception ({@code Throwable})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* <li>Parameter 2: name of the signature file ({@code String})</li>
* </ul>
*/
JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
/**
* JAR signature contains no verified signers.
*
* <ul>
* <li>Parameter 1: name of the signature block file ({@code String})</li>
* </ul>
*/
JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
/**
* JAR signature file contains a section with a duplicate name.
*
* <ul>
* <li>Parameter 1: signature file name ({@code String})</li>
* <li>Parameter 1: section name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: signature file name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature file ({@code String})</li>
* <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: name of the signature file ({@code String})</li>
* <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li>
* <li>Parameter 3: APK signature scheme English name ({@code} String)</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: entry name ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: target sandbox version ({@code Integer})</li>
* </ul>
*/
NO_SIG_FOR_TARGET_SANDBOX_VERSION(
"Missing APK Signature Scheme v2 signature required for target sandbox version"
+ " %1$d"),
/**
* 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.
*
* <ul>
* <li>Parameter 1: error details ({@code Throwable})</li>
* </ul>
*/
V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
/**
* This APK Signature Scheme v2 signer's certificate could not be parsed.
*
* <ul>
* <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
* certificates ({@code Integer})</li>
* <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
* list of certificates ({@code Integer})</li>
* <li>Parameter 3: error details ({@code Throwable})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
* </ul>
*/
V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
/**
* This APK Signature Scheme v2 signer contains a malformed additional attribute.
*
* <ul>
* <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
* </ul>
*/
V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
/**
* 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.
*
* <ul>
* <li>Parameter 1: algorithm ID ({@code Integer})</li>
* </ul>
*/
V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
/**
* This APK Signature Scheme v2 signer contains an unknown additional attribute.
*
* <ul>
* <li>Parameter 1: attribute ID ({@code Integer})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
* <li>Parameter 2: exception ({@code Throwable})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
* </ul>
*/
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"),
/**
* 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.
*
* <ul>
* <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
* <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
* <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
* </ul>
*/
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.
*
* <ul>
* <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
* <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
* <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
* </ul>
*/
V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
"APK integrity check failed. %1$s digest mismatch."
+ " Expected: <%2$s>, actual: <%3$s>"),
/**
* APK Signing Block contains an unknown entry.
*
* <ul>
* <li>Parameter 1: entry ID ({@code Integer})</li>
* </ul>
*/
APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
private final String mFormat;
private 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 {
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) {
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 == null) {
return false;
}
if (getClass() != obj.getClass()) {
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.
*
* <p>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 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)}.
*
* <p>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;
}
/**
* Returns an {@link ApkVerifier} initialized according to the configuration of this
* builder.
*/
public ApkVerifier build() {
return new ApkVerifier(
mApkFile,
mApkDataSource,
mMinSdkVersion,
mMaxSdkVersion);
}
}
}