blob: e6cee3b59a201d1ba1bd0fc734fc41008b8529c0 [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.internal.apk.v1;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.ApkVerifier.IssueWithParams;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.jar.ManifestParser;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.InclusiveIntRange;
import com.android.apksig.internal.util.MessageDigestSink;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;
/**
* APK verifier which uses JAR signing (aka v1 signing scheme).
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
*/
public abstract class V1SchemeVerifier {
private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME;
private V1SchemeVerifier() {}
/**
* Verifies the provided APK's JAR signatures and returns the result of verification. APK is
* considered verified only if {@link Result#verified} is {@code true}. If verification fails,
* the result will contain errors -- see {@link Result#getErrors()}.
*
* @throws ApkFormatException if the APK is malformed
* @throws IOException if an I/O error occurs when reading the APK
* @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
*/
public static Result verify(
DataSource apk,
ApkUtils.ZipSections apkSections,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException {
if (minSdkVersion > maxSdkVersion) {
throw new IllegalArgumentException(
"minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion
+ ")");
}
Result result = new Result();
// Parse the ZIP Central Directory and check that there are no entries with duplicate names.
List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections);
Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result);
if (result.containsErrors()) {
return result;
}
// Verify JAR signature(s).
Signers.verify(
apk,
apkSections.getZipCentralDirectoryOffset(),
cdRecords,
cdEntryNames,
supportedApkSigSchemeNames,
foundApkSigSchemeIds,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Returns the set of entry names and reports any duplicate entry names in the {@code result}
* as errors.
*/
private static Set<String> checkForDuplicateEntries(
List<CentralDirectoryRecord> cdRecords, Result result) {
Set<String> cdEntryNames = new HashSet<>(cdRecords.size());
Set<String> duplicateCdEntryNames = null;
for (CentralDirectoryRecord cdRecord : cdRecords) {
String entryName = cdRecord.getName();
if (!cdEntryNames.add(entryName)) {
// This is an error. Report this once per duplicate name.
if (duplicateCdEntryNames == null) {
duplicateCdEntryNames = new HashSet<>();
}
if (duplicateCdEntryNames.add(entryName)) {
result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName);
}
}
}
return cdEntryNames;
}
/**
* All JAR signers of an APK.
*/
private static class Signers {
/**
* Verifies JAR signatures of the provided APK and populates the provided result container
* with errors, warnings, and information about signers. The APK is considered verified if
* the {@link Result#verified} is {@code true}.
*/
private static void verify(
DataSource apk,
long cdStartOffset,
List<CentralDirectoryRecord> cdRecords,
Set<String> cdEntryNames,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion,
Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
// Find JAR manifest and signature block files.
CentralDirectoryRecord manifestEntry = null;
Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
for (CentralDirectoryRecord cdRecord : cdRecords) {
String entryName = cdRecord.getName();
if (!entryName.startsWith("META-INF/")) {
continue;
}
if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) {
manifestEntry = cdRecord;
continue;
}
if (entryName.endsWith(".SF")) {
sigFileEntries.put(entryName, cdRecord);
continue;
}
if ((entryName.endsWith(".RSA"))
|| (entryName.endsWith(".DSA"))
|| (entryName.endsWith(".EC"))) {
sigBlockEntries.add(cdRecord);
continue;
}
}
if (manifestEntry == null) {
result.addError(Issue.JAR_SIG_NO_MANIFEST);
return;
}
// Parse the JAR manifest and check that all JAR entries it references exist in the APK.
byte[] manifestBytes;
try {
manifestBytes =
LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
} catch (ZipFormatException e) {
throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e);
}
Map<String, ManifestParser.Section> entryNameToManifestSection = null;
ManifestParser manifest = new ManifestParser(manifestBytes);
ManifestParser.Section manifestMainSection = manifest.readSection();
List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections();
entryNameToManifestSection = new HashMap<>(manifestIndividualSections.size());
int manifestSectionNumber = 0;
for (ManifestParser.Section manifestSection : manifestIndividualSections) {
manifestSectionNumber++;
String entryName = manifestSection.getName();
if (entryName == null) {
result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber);
continue;
}
if (entryNameToManifestSection.put(entryName, manifestSection) != null) {
result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName);
continue;
}
if (!cdEntryNames.contains(entryName)) {
result.addError(
Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName);
continue;
}
}
if (result.containsErrors()) {
return;
}
// STATE OF AFFAIRS:
// * All JAR entries listed in JAR manifest are present in the APK.
// Identify signers
List<Signer> signers = new ArrayList<>(sigBlockEntries.size());
for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) {
String sigBlockEntryName = sigBlockEntry.getName();
int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.');
if (extensionDelimiterIndex == -1) {
throw new RuntimeException(
"Signature block file name does not contain extension: "
+ sigBlockEntryName);
}
String sigFileEntryName =
sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF";
CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName);
if (sigFileEntry == null) {
result.addWarning(
Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName);
continue;
}
String signerName = sigBlockEntryName.substring("META-INF/".length());
Result.SignerInfo signerInfo =
new Result.SignerInfo(
signerName, sigBlockEntryName, sigFileEntry.getName());
Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo);
signers.add(signer);
}
if (signers.isEmpty()) {
result.addError(Issue.JAR_SIG_NO_SIGNATURES);
return;
}
// Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding
// signature file .SF. Any error encountered for any signer terminates verification, to
// mimic Android's behavior.
for (Signer signer : signers) {
signer.verifySigBlockAgainstSigFile(
apk, cdStartOffset, minSdkVersion, maxSdkVersion);
if (signer.getResult().containsErrors()) {
result.signers.add(signer.getResult());
}
}
if (result.containsErrors()) {
return;
}
// STATE OF AFFAIRS:
// * All JAR entries listed in JAR manifest are present in the APK.
// * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
// Verify each signer's signature file (.SF) against the JAR manifest.
List<Signer> remainingSigners = new ArrayList<>(signers.size());
for (Signer signer : signers) {
signer.verifySigFileAgainstManifest(
manifestBytes,
manifestMainSection,
entryNameToManifestSection,
supportedApkSigSchemeNames,
foundApkSigSchemeIds,
minSdkVersion,
maxSdkVersion);
if (signer.isIgnored()) {
result.ignoredSigners.add(signer.getResult());
} else {
if (signer.getResult().containsErrors()) {
result.signers.add(signer.getResult());
} else {
remainingSigners.add(signer);
}
}
}
if (result.containsErrors()) {
return;
}
signers = remainingSigners;
if (signers.isEmpty()) {
result.addError(Issue.JAR_SIG_NO_SIGNATURES);
return;
}
// STATE OF AFFAIRS:
// * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
// * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
// * All JAR entries listed in JAR manifest are present in the APK.
// Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's
// JAR entry is considered signed by signers associated with an .SF file iff the entry
// is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest
// match theentry's uncompressed data. Android requires that all such JAR entries are
// signed by the same set of signers. This set may be smaller than the set of signers
// we've identified so far.
Set<Signer> apkSigners =
verifyJarEntriesAgainstManifestAndSigners(
apk,
cdStartOffset,
cdRecords,
entryNameToManifestSection,
signers,
minSdkVersion,
maxSdkVersion,
result);
if (result.containsErrors()) {
return;
}
// STATE OF AFFAIRS:
// * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
// * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
// * All JAR entries listed in JAR manifest are present in the APK.
// * All JAR entries present in the APK and supposed to be covered by JAR signature
// (i.e., reside outside of META-INF/) are covered by signatures from the same set
// of signers.
// Report any JAR entries which aren't covered by signature.
Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2);
signatureEntryNames.add(manifestEntry.getName());
for (Signer signer : apkSigners) {
signatureEntryNames.add(signer.getSignatureBlockEntryName());
signatureEntryNames.add(signer.getSignatureFileEntryName());
}
for (CentralDirectoryRecord cdRecord : cdRecords) {
String entryName = cdRecord.getName();
if ((entryName.startsWith("META-INF/"))
&& (!entryName.endsWith("/"))
&& (!signatureEntryNames.contains(entryName))) {
result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName);
}
}
// Reflect the sets of used signers and ignored signers in the result.
for (Signer signer : signers) {
if (apkSigners.contains(signer)) {
result.signers.add(signer.getResult());
} else {
result.ignoredSigners.add(signer.getResult());
}
}
result.verified = true;
}
}
static class Signer {
private final String mName;
private final Result.SignerInfo mResult;
private final CentralDirectoryRecord mSignatureFileEntry;
private final CentralDirectoryRecord mSignatureBlockEntry;
private boolean mIgnored;
private byte[] mSigFileBytes;
private Set<String> mSigFileEntryNames;
private Signer(
String name,
CentralDirectoryRecord sigBlockEntry,
CentralDirectoryRecord sigFileEntry,
Result.SignerInfo result) {
mName = name;
mResult = result;
mSignatureBlockEntry = sigBlockEntry;
mSignatureFileEntry = sigFileEntry;
}
public String getName() {
return mName;
}
public String getSignatureFileEntryName() {
return mSignatureFileEntry.getName();
}
public String getSignatureBlockEntryName() {
return mSignatureBlockEntry.getName();
}
void setIgnored() {
mIgnored = true;
}
public boolean isIgnored() {
return mIgnored;
}
public Set<String> getSigFileEntryNames() {
return mSigFileEntryNames;
}
public Result.SignerInfo getResult() {
return mResult;
}
@SuppressWarnings("restriction")
public void verifySigBlockAgainstSigFile(
DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
byte[] sigBlockBytes;
try {
sigBlockBytes =
LocalFileRecord.getUncompressedData(
apk, mSignatureBlockEntry, cdStartOffset);
} catch (ZipFormatException e) {
throw new ApkFormatException(
"Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e);
}
try {
mSigFileBytes =
LocalFileRecord.getUncompressedData(
apk, mSignatureFileEntry, cdStartOffset);
} catch (ZipFormatException e) {
throw new ApkFormatException(
"Malformed ZIP entry: " + mSignatureFileEntry.getName(), e);
}
PKCS7 sigBlock;
try {
sigBlock = new PKCS7(sigBlockBytes);
} catch (IOException e) {
if (e.getCause() instanceof CertificateException) {
mResult.addError(
Issue.JAR_SIG_MALFORMED_CERTIFICATE, mSignatureBlockEntry.getName(), e);
} else {
mResult.addError(
Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
}
return;
}
SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos();
if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) {
mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
return;
}
SignerInfo verifiedSignerInfo = null;
if ((unverifiedSignerInfos != null) && (unverifiedSignerInfos.length > 0)) {
for (int i = 0; i < unverifiedSignerInfos.length; i++) {
SignerInfo unverifiedSignerInfo = unverifiedSignerInfos[i];
String digestAlgorithmOid =
unverifiedSignerInfo.getDigestAlgorithmId().getOID().toString();
String signatureAlgorithmOid =
unverifiedSignerInfo
.getDigestEncryptionAlgorithmId().getOID().toString();
InclusiveIntRange desiredApiLevels =
InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
mResult.addError(
Issue.JAR_SIG_UNSUPPORTED_SIG_ALG,
mSignatureBlockEntry.getName(),
digestAlgorithmOid,
signatureAlgorithmOid,
String.valueOf(apiLevelsWhereDigestAlgorithmNotSupported));
return;
}
try {
verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes);
} catch (SignatureException e) {
mResult.addError(
Issue.JAR_SIG_VERIFY_EXCEPTION,
mSignatureBlockEntry.getName(),
mSignatureFileEntry.getName(),
e);
return;
}
if (verifiedSignerInfo != null) {
// Verified
break;
}
// Did not verify
if (minSdkVersion < AndroidSdkVersion.N) {
// Prior to N, Android attempted to verify only the first SignerInfo.
mResult.addError(
Issue.JAR_SIG_DID_NOT_VERIFY,
mSignatureBlockEntry.getName(),
mSignatureFileEntry.getName());
return;
}
}
}
if (verifiedSignerInfo == null) {
mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
return;
}
// TODO: PKCS7 class doesn't guarantee that returned certificates' getEncoded returns
// the original encoded form of certificates rather than the DER re-encoded form. We
// need to replace the PKCS7 parser/verifier.
List<X509Certificate> certChain;
try {
certChain = verifiedSignerInfo.getCertificateChain(sigBlock);
} catch (IOException e) {
throw new RuntimeException(
"Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e);
}
if ((certChain == null) || (certChain.isEmpty())) {
throw new RuntimeException("Verified SignerInfo does not have a certificate chain");
}
mResult.certChain.clear();
mResult.certChain.addAll(certChain);
}
private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
static final String OID_SIG_DSA = "1.2.840.10040.4.1";
private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
private static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
new HashMap<>();
{
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_DSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.from(9));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.from(21));
}
private static void addSupportedSigAlg(
String digestAlgorithmOid,
String signatureAlgorithmOid,
InclusiveIntRange... supportedApiLevels) {
SUPPORTED_SIG_ALG_OIDS.put(
digestAlgorithmOid + "with" + signatureAlgorithmOid,
Arrays.asList(supportedApiLevels));
}
private List<InclusiveIntRange> getSigAlgSupportedApiLevels(
String digestAlgorithmOid,
String signatureAlgorithmOid) {
List<InclusiveIntRange> result =
SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
return (result != null) ? result : Collections.emptyList();
}
public void verifySigFileAgainstManifest(
byte[] manifestBytes,
ManifestParser.Section manifestMainSection,
Map<String, ManifestParser.Section> entryNameToManifestSection,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion) throws NoSuchAlgorithmException {
// Inspect the main section of the .SF file.
ManifestParser sf = new ManifestParser(mSigFileBytes);
ManifestParser.Section sfMainSection = sf.readSection();
if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) {
mResult.addError(
Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE,
mSignatureFileEntry.getName());
setIgnored();
return;
}
if (maxSdkVersion >= AndroidSdkVersion.N) {
// Android N and newer rejects APKs whose .SF file says they were supposed to be
// signed with APK Signature Scheme v2 (or newer) and yet no such signature was
// found.
checkForStrippedApkSignatures(
sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
if (mResult.containsErrors()) {
return;
}
}
boolean createdBySigntool = false;
String createdBy = sfMainSection.getAttributeValue("Created-By");
if (createdBy != null) {
createdBySigntool = createdBy.indexOf("signtool") != -1;
}
boolean manifestDigestVerified =
verifyManifestDigest(
sfMainSection,
createdBySigntool,
manifestBytes,
minSdkVersion,
maxSdkVersion);
if (!createdBySigntool) {
verifyManifestMainSectionDigest(
sfMainSection,
manifestMainSection,
manifestBytes,
minSdkVersion,
maxSdkVersion);
}
if (mResult.containsErrors()) {
return;
}
// Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest
// verifies, per-entry sections should be ignored. However, most Android platform
// implementations require that such sections exist.
List<ManifestParser.Section> sfSections = sf.readAllSections();
Set<String> sfEntryNames = new HashSet<>(sfSections.size());
int sfSectionNumber = 0;
for (ManifestParser.Section sfSection : sfSections) {
sfSectionNumber++;
String entryName = sfSection.getName();
if (entryName == null) {
mResult.addError(
Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION,
mSignatureFileEntry.getName(),
sfSectionNumber);
setIgnored();
return;
}
if (!sfEntryNames.add(entryName)) {
mResult.addError(
Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION,
mSignatureFileEntry.getName(),
entryName);
setIgnored();
return;
}
if (manifestDigestVerified) {
// No need to verify this entry's corresponding JAR manifest entry because the
// JAR manifest verifies in full.
continue;
}
// Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify
// the digest of the JAR manifest section corresponding to this .SF section.
ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
if (manifestSection == null) {
mResult.addError(
Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
entryName,
mSignatureFileEntry.getName());
setIgnored();
continue;
}
verifyManifestIndividualSectionDigest(
sfSection,
createdBySigntool,
manifestSection,
manifestBytes,
minSdkVersion,
maxSdkVersion);
}
mSigFileEntryNames = sfEntryNames;
}
/**
* Returns {@code true} if the whole-file digest of the manifest against the main section of
* the .SF file.
*/
private boolean verifyManifestDigest(
ManifestParser.Section sfMainSection,
boolean createdBySigntool,
byte[] manifestBytes,
int minSdkVersion,
int maxSdkVersion) throws NoSuchAlgorithmException {
Collection<NamedDigest> expectedDigests =
getDigestsToVerify(
sfMainSection,
((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
minSdkVersion,
maxSdkVersion);
boolean digestFound = !expectedDigests.isEmpty();
if (!digestFound) {
mResult.addWarning(
Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE,
mSignatureFileEntry.getName());
return false;
}
boolean verified = true;
for (NamedDigest expectedDigest : expectedDigests) {
String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
byte[] actual = digest(jcaDigestAlgorithm, manifestBytes);
byte[] expected = expectedDigest.digest;
if (!Arrays.equals(expected, actual)) {
mResult.addWarning(
Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
V1SchemeSigner.MANIFEST_ENTRY_NAME,
jcaDigestAlgorithm,
mSignatureFileEntry.getName(),
Base64.getEncoder().encodeToString(actual),
Base64.getEncoder().encodeToString(expected));
verified = false;
}
}
return verified;
}
/**
* Verifies the digest of the manifest's main section against the main section of the .SF
* file.
*/
private void verifyManifestMainSectionDigest(
ManifestParser.Section sfMainSection,
ManifestParser.Section manifestMainSection,
byte[] manifestBytes,
int minSdkVersion,
int maxSdkVersion) throws NoSuchAlgorithmException {
Collection<NamedDigest> expectedDigests =
getDigestsToVerify(
sfMainSection,
"-Digest-Manifest-Main-Attributes",
minSdkVersion,
maxSdkVersion);
if (expectedDigests.isEmpty()) {
return;
}
for (NamedDigest expectedDigest : expectedDigests) {
String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
byte[] actual =
digest(
jcaDigestAlgorithm,
manifestBytes,
manifestMainSection.getStartOffset(),
manifestMainSection.getSizeBytes());
byte[] expected = expectedDigest.digest;
if (!Arrays.equals(expected, actual)) {
mResult.addError(
Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY,
jcaDigestAlgorithm,
mSignatureFileEntry.getName(),
Base64.getEncoder().encodeToString(actual),
Base64.getEncoder().encodeToString(expected));
}
}
}
/**
* Verifies the digest of the manifest's individual section against the corresponding
* individual section of the .SF file.
*/
private void verifyManifestIndividualSectionDigest(
ManifestParser.Section sfIndividualSection,
boolean createdBySigntool,
ManifestParser.Section manifestIndividualSection,
byte[] manifestBytes,
int minSdkVersion,
int maxSdkVersion) throws NoSuchAlgorithmException {
String entryName = sfIndividualSection.getName();
Collection<NamedDigest> expectedDigests =
getDigestsToVerify(
sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion);
if (expectedDigests.isEmpty()) {
mResult.addError(
Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
entryName,
mSignatureFileEntry.getName());
return;
}
int sectionStartIndex = manifestIndividualSection.getStartOffset();
int sectionSizeBytes = manifestIndividualSection.getSizeBytes();
if (createdBySigntool) {
int sectionEndIndex = sectionStartIndex + sectionSizeBytes;
if ((manifestBytes[sectionEndIndex - 1] == '\n')
&& (manifestBytes[sectionEndIndex - 2] == '\n')) {
sectionSizeBytes--;
}
}
for (NamedDigest expectedDigest : expectedDigests) {
String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
byte[] actual =
digest(
jcaDigestAlgorithm,
manifestBytes,
sectionStartIndex,
sectionSizeBytes);
byte[] expected = expectedDigest.digest;
if (!Arrays.equals(expected, actual)) {
mResult.addError(
Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY,
entryName,
jcaDigestAlgorithm,
mSignatureFileEntry.getName(),
Base64.getEncoder().encodeToString(actual),
Base64.getEncoder().encodeToString(expected));
}
}
}
private void checkForStrippedApkSignatures(
ManifestParser.Section sfMainSection,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds) {
String signedWithApkSchemes =
sfMainSection.getAttributeValue(
V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
// This field contains a comma-separated list of APK signature scheme IDs which were
// used to sign this APK. Android rejects APKs where an ID is known to the platform but
// the APK didn't verify using that scheme.
if (signedWithApkSchemes == null) {
// APK signature (e.g., v2 scheme) stripping protections not enabled.
if (!foundApkSigSchemeIds.isEmpty()) {
// APK is signed with an APK signature scheme such as v2 scheme.
mResult.addWarning(
Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION,
mSignatureFileEntry.getName());
}
return;
}
if (supportedApkSigSchemeNames.isEmpty()) {
return;
}
Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ",");
while (tokenizer.hasMoreTokens()) {
String idText = tokenizer.nextToken().trim();
if (idText.isEmpty()) {
continue;
}
int id;
try {
id = Integer.parseInt(idText);
} catch (Exception ignored) {
continue;
}
// This APK was supposed to be signed with the APK signature scheme having
// this ID.
if (supportedApkSigSchemeIds.contains(id)) {
supportedExpectedApkSigSchemeIds.add(id);
} else {
mResult.addWarning(
Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID,
mSignatureFileEntry.getName(),
id);
}
}
for (int id : supportedExpectedApkSigSchemeIds) {
if (!foundApkSigSchemeIds.contains(id)) {
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
mResult.addError(
Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED,
mSignatureFileEntry.getName(),
id,
apkSigSchemeName);
}
}
}
}
private static Collection<NamedDigest> getDigestsToVerify(
ManifestParser.Section section,
String digestAttrSuffix,
int minSdkVersion,
int maxSdkVersion) {
Decoder base64Decoder = Base64.getDecoder();
List<NamedDigest> result = new ArrayList<>(1);
if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) {
// Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is
// to rely on the ancient Digest-Algorithms attribute which contains
// whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The
// first digest attribute (with supported digest algorithm) found using the list is
// used.
String algs = section.getAttributeValue("Digest-Algorithms");
if (algs == null) {
algs = "SHA SHA1";
}
StringTokenizer tokens = new StringTokenizer(algs);
while (tokens.hasMoreTokens()) {
String alg = tokens.nextToken();
String attrName = alg + digestAttrSuffix;
String digestBase64 = section.getAttributeValue(attrName);
if (digestBase64 == null) {
// Attribute not found
continue;
}
alg = getCanonicalJcaMessageDigestAlgorithm(alg);
if ((alg == null)
|| (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg)
> minSdkVersion)) {
// Unsupported digest algorithm
continue;
}
// Supported digest algorithm
result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64)));
break;
}
// No supported digests found -- this will fail to verify on pre-JB MR2 Androids.
if (result.isEmpty()) {
return result;
}
}
if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) {
// On JB MR2 and newer, Android platform picks the strongest algorithm out of:
// SHA-512, SHA-384, SHA-256, SHA-1.
for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
String digestBase64 = section.getAttributeValue(attrName);
if (digestBase64 == null) {
// Attribute not found
continue;
}
byte[] digest = base64Decoder.decode(digestBase64);
byte[] digestInResult = getDigest(result, alg);
if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) {
result.add(new NamedDigest(alg, digest));
}
break;
}
}
return result;
}
private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA-1",
};
private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) {
return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US));
}
public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(
String jcaAlgorithmName) {
Integer result =
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get(
jcaAlgorithmName.toUpperCase(Locale.US));
return (result != null) ? result : Integer.MAX_VALUE;
}
private static String getJarDigestAttributeName(
String jcaDigestAlgorithm, String attrNameSuffix) {
if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) {
return "SHA1" + attrNameSuffix;
} else {
return jcaDigestAlgorithm + attrNameSuffix;
}
}
private static final Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL;
static {
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8);
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384");
UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512");
}
private static final Map<String, Integer>
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST;
static {
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5);
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0);
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0);
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0);
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
"SHA-384", AndroidSdkVersion.GINGERBREAD);
MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
"SHA-512", AndroidSdkVersion.GINGERBREAD);
}
private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) {
for (NamedDigest digest : digests) {
if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) {
return digest.digest;
}
}
return null;
}
public static List<CentralDirectoryRecord> parseZipCentralDirectory(
DataSource apk,
ApkUtils.ZipSections apkSections)
throws IOException, ApkFormatException {
// Read the ZIP Central Directory
long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
if (cdSizeBytes > Integer.MAX_VALUE) {
throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
}
long cdOffset = apkSections.getZipCentralDirectoryOffset();
ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
cd.order(ByteOrder.LITTLE_ENDIAN);
// Parse the ZIP Central Directory
int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
for (int i = 0; i < expectedCdRecordCount; i++) {
CentralDirectoryRecord cdRecord;
int offsetInsideCd = cd.position();
try {
cdRecord = CentralDirectoryRecord.getRecord(cd);
} catch (ZipFormatException e) {
throw new ApkFormatException(
"Malformed ZIP Central Directory record #" + (i + 1)
+ " at file offset " + (cdOffset + offsetInsideCd),
e);
}
String entryName = cdRecord.getName();
if (entryName.endsWith("/")) {
// Ignore directory entries
continue;
}
cdRecords.add(cdRecord);
}
// There may be more data in Central Directory, but we don't warn or throw because Android
// ignores unused CD data.
return cdRecords;
}
/**
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
* manifest for the APK to verify on Android.
*/
private static boolean isJarEntryDigestNeededInManifest(String entryName) {
// NOTE: This logic is different from what's required by the JAR signing scheme. This is
// because Android's APK verification logic differs from that spec. In particular, JAR
// signing spec includes into JAR manifest all files in subdirectories of META-INF and
// any files inside META-INF not related to signatures.
if (entryName.startsWith("META-INF/")) {
return false;
}
return !entryName.endsWith("/");
}
private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners(
DataSource apk,
long cdOffsetInApk,
Collection<CentralDirectoryRecord> cdRecords,
Map<String, ManifestParser.Section> entryNameToManifestSection,
List<Signer> signers,
int minSdkVersion,
int maxSdkVersion,
Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
// Iterate over APK contents as sequentially as possible to improve performance.
List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset =
new ArrayList<>(cdRecords);
Collections.sort(
cdRecordsSortedByLocalFileHeaderOffset,
CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
Set<String> manifestEntryNamesMissingFromApk =
new HashSet<>(entryNameToManifestSection.keySet());
List<Signer> firstSignedEntrySigners = null;
String firstSignedEntryName = null;
for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
String entryName = cdRecord.getName();
manifestEntryNamesMissingFromApk.remove(entryName);
if (!isJarEntryDigestNeededInManifest(entryName)) {
continue;
}
ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
if (manifestSection == null) {
result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
continue;
}
List<Signer> entrySigners = new ArrayList<>(signers.size());
for (Signer signer : signers) {
if (signer.getSigFileEntryNames().contains(entryName)) {
entrySigners.add(signer);
}
}
if (entrySigners.isEmpty()) {
result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName);
continue;
}
if (firstSignedEntrySigners == null) {
firstSignedEntrySigners = entrySigners;
firstSignedEntryName = entryName;
} else if (!entrySigners.equals(firstSignedEntrySigners)) {
result.addError(
Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH,
firstSignedEntryName,
getSignerNames(firstSignedEntrySigners),
entryName,
getSignerNames(entrySigners));
continue;
}
List<NamedDigest> expectedDigests =
new ArrayList<>(
getDigestsToVerify(
manifestSection, "-Digest", minSdkVersion, maxSdkVersion));
if (expectedDigests.isEmpty()) {
result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
continue;
}
MessageDigest[] mds = new MessageDigest[expectedDigests.size()];
for (int i = 0; i < expectedDigests.size(); i++) {
mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm);
}
try {
LocalFileRecord.outputUncompressedData(
apk,
cdRecord,
cdOffsetInApk,
new MessageDigestSink(mds));
} catch (ZipFormatException e) {
throw new ApkFormatException("Malformed ZIP entry: " + entryName, e);
} catch (IOException e) {
throw new IOException("Failed to read entry: " + entryName, e);
}
for (int i = 0; i < expectedDigests.size(); i++) {
NamedDigest expectedDigest = expectedDigests.get(i);
byte[] actualDigest = mds[i].digest();
if (!Arrays.equals(expectedDigest.digest, actualDigest)) {
result.addError(
Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
entryName,
expectedDigest.jcaDigestAlgorithm,
V1SchemeSigner.MANIFEST_ENTRY_NAME,
Base64.getEncoder().encodeToString(actualDigest),
Base64.getEncoder().encodeToString(expectedDigest.digest));
}
}
}
if (firstSignedEntrySigners == null) {
result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES);
return Collections.emptySet();
} else {
return new HashSet<>(firstSignedEntrySigners);
}
}
private static List<String> getSignerNames(List<Signer> signers) {
if (signers.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>(signers.size());
for (Signer signer : signers) {
result.add(signer.getName());
}
return result;
}
private static MessageDigest getMessageDigest(String algorithm)
throws NoSuchAlgorithmException {
return MessageDigest.getInstance(algorithm);
}
private static byte[] digest(String algorithm, byte[] data, int offset, int length)
throws NoSuchAlgorithmException {
MessageDigest md = getMessageDigest(algorithm);
md.update(data, offset, length);
return md.digest();
}
private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException {
return getMessageDigest(algorithm).digest(data);
}
private static class NamedDigest {
private final String jcaDigestAlgorithm;
private final byte[] digest;
private NamedDigest(String jcaDigestAlgorithm, byte[] digest) {
this.jcaDigestAlgorithm = jcaDigestAlgorithm;
this.digest = digest;
}
}
public static class Result {
/** Whether the APK's JAR signature verifies. */
public boolean verified;
/** List of APK's signers. These signers are used by Android. */
public final List<SignerInfo> signers = new ArrayList<>();
/**
* Signers encountered in the APK but not included in the set of the APK's signers. These
* signers are ignored by Android.
*/
public final List<SignerInfo> ignoredSigners = new ArrayList<>();
private final List<IssueWithParams> mWarnings = new ArrayList<>();
private final List<IssueWithParams> mErrors = new ArrayList<>();
private boolean containsErrors() {
if (!mErrors.isEmpty()) {
return true;
}
for (SignerInfo signer : signers) {
if (signer.containsErrors()) {
return true;
}
}
return false;
}
private void addError(Issue msg, Object... parameters) {
mErrors.add(new IssueWithParams(msg, parameters));
}
private void addWarning(Issue msg, Object... parameters) {
mWarnings.add(new IssueWithParams(msg, parameters));
}
public List<IssueWithParams> getErrors() {
return mErrors;
}
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
public static class SignerInfo {
public final String name;
public final String signatureFileName;
public final String signatureBlockFileName;
public final List<X509Certificate> certChain = new ArrayList<>();
private final List<IssueWithParams> mWarnings = new ArrayList<>();
private final List<IssueWithParams> mErrors = new ArrayList<>();
private SignerInfo(
String name, String signatureBlockFileName, String signatureFileName) {
this.name = name;
this.signatureBlockFileName = signatureBlockFileName;
this.signatureFileName = signatureFileName;
}
private boolean containsErrors() {
return !mErrors.isEmpty();
}
private void addError(Issue msg, Object... parameters) {
mErrors.add(new IssueWithParams(msg, parameters));
}
private void addWarning(Issue msg, Object... parameters) {
mWarnings.add(new IssueWithParams(msg, parameters));
}
public List<IssueWithParams> getErrors() {
return mErrors;
}
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
}
}
}