Merge "maxSdkVersion can be specified for APK verification."

This commit is contained in:
Alex Klyubin
2016-06-17 19:10:42 +00:00
committed by Gerrit Code Review
2 changed files with 105 additions and 45 deletions

View File

@@ -56,23 +56,43 @@ public class ApkVerifier {
* @param apk APK file contents * @param apk APK file contents
* @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures
* may need to be verified * may need to be verified
* @param maxSdkVersion API Level of the newest Android platform on which the APK's signatures
* may need to be verified
* *
* @throws IOException if an I/O error is encountered while reading the APK * @throws IOException if an I/O error is encountered while reading the APK
* @throws ZipFormatException if the APK is malformed at ZIP format level * @throws ZipFormatException if the APK is malformed at ZIP format level
*/ */
public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException { public Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
throws IOException, ZipFormatException {
if (minSdkVersion < 0) {
throw new IllegalArgumentException(
"minSdkVersion must not be negative: " + minSdkVersion);
}
if (minSdkVersion > maxSdkVersion) {
throw new IllegalArgumentException(
"minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion
+ ")");
}
ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
// Attempt to verify the APK using APK Signature Scheme v2
Result result = new Result(); Result result = new Result();
Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
try { // Android N and newer attempts to verify APK Signature Scheme v2 signature of the APK.
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections); // If the signature is not found, it falls back to JAR signature verification. If the
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID); // signature is found but does not verify, the APK is rejected.
result.mergeFrom(v2Result); Set<Integer> foundApkSigSchemeIds;
} catch (V2SchemeVerifier.SignatureNotFoundException ignored) {} if (maxSdkVersion >= AndroidSdkVersion.N) {
if (result.containsErrors()) { foundApkSigSchemeIds = new HashSet<>(1);
return result; try {
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
result.mergeFrom(v2Result);
} catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
if (result.containsErrors()) {
return result;
}
} else {
foundApkSigSchemeIds = Collections.emptySet();
} }
// Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
@@ -86,7 +106,8 @@ public class ApkVerifier {
zipSections, zipSections,
SUPPORTED_APK_SIG_SCHEME_NAMES, SUPPORTED_APK_SIG_SCHEME_NAMES,
foundApkSigSchemeIds, foundApkSigSchemeIds,
minSdkVersion); minSdkVersion,
maxSdkVersion);
result.mergeFrom(v1Result); result.mergeFrom(v1Result);
} }
if (result.containsErrors()) { if (result.containsErrors()) {

View File

@@ -78,7 +78,14 @@ public abstract class V1SchemeVerifier {
ApkUtils.ZipSections apkSections, ApkUtils.ZipSections apkSections,
Map<Integer, String> supportedApkSigSchemeNames, Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds, Set<Integer> foundApkSigSchemeIds,
int minSdkVersion) throws IOException, ZipFormatException { int minSdkVersion,
int maxSdkVersion) throws IOException, ZipFormatException {
if (minSdkVersion > maxSdkVersion) {
throw new IllegalArgumentException(
"minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion
+ ")");
}
Result result = new Result(); Result result = new Result();
// Parse the ZIP Central Directory and check that there are no entries with duplicate names. // Parse the ZIP Central Directory and check that there are no entries with duplicate names.
@@ -97,6 +104,7 @@ public abstract class V1SchemeVerifier {
supportedApkSigSchemeNames, supportedApkSigSchemeNames,
foundApkSigSchemeIds, foundApkSigSchemeIds,
minSdkVersion, minSdkVersion,
maxSdkVersion,
result); result);
return result; return result;
@@ -143,6 +151,7 @@ public abstract class V1SchemeVerifier {
Map<Integer, String> supportedApkSigSchemeNames, Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds, Set<Integer> foundApkSigSchemeIds,
int minSdkVersion, int minSdkVersion,
int maxSdkVersion,
Result result) throws ZipFormatException, IOException { Result result) throws ZipFormatException, IOException {
// Find JAR manifest and signature block files. // Find JAR manifest and signature block files.
@@ -243,7 +252,8 @@ public abstract class V1SchemeVerifier {
// signature file .SF. Any error encountered for any signer terminates verification, to // signature file .SF. Any error encountered for any signer terminates verification, to
// mimic Android's behavior. // mimic Android's behavior.
for (Signer signer : signers) { for (Signer signer : signers) {
signer.verifySigBlockAgainstSigFile(apk, cdStartOffset, minSdkVersion); signer.verifySigBlockAgainstSigFile(
apk, cdStartOffset, minSdkVersion, maxSdkVersion);
if (signer.getResult().containsErrors()) { if (signer.getResult().containsErrors()) {
result.signers.add(signer.getResult()); result.signers.add(signer.getResult());
} }
@@ -264,7 +274,8 @@ public abstract class V1SchemeVerifier {
entryNameToManifestSection, entryNameToManifestSection,
supportedApkSigSchemeNames, supportedApkSigSchemeNames,
foundApkSigSchemeIds, foundApkSigSchemeIds,
minSdkVersion); minSdkVersion,
maxSdkVersion);
if (signer.isIgnored()) { if (signer.isIgnored()) {
result.ignoredSigners.add(signer.getResult()); result.ignoredSigners.add(signer.getResult());
} else { } else {
@@ -393,7 +404,7 @@ public abstract class V1SchemeVerifier {
@SuppressWarnings("restriction") @SuppressWarnings("restriction")
public void verifySigBlockAgainstSigFile( public void verifySigBlockAgainstSigFile(
DataSource apk, long cdStartOffset, int minSdkVersion) DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
throws IOException, ZipFormatException { throws IOException, ZipFormatException {
byte[] sigBlockBytes = byte[] sigBlockBytes =
LocalFileHeader.getUncompressedData( LocalFileHeader.getUncompressedData(
@@ -433,7 +444,8 @@ public abstract class V1SchemeVerifier {
String signatureAlgorithmOid = String signatureAlgorithmOid =
unverifiedSignerInfo unverifiedSignerInfo
.getDigestEncryptionAlgorithmId().getOID().toString(); .getDigestEncryptionAlgorithmId().getOID().toString();
InclusiveIntRange desiredApiLevels = InclusiveIntRange.from(minSdkVersion); InclusiveIntRange desiredApiLevels =
InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported = List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported = List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
@@ -843,7 +855,8 @@ public abstract class V1SchemeVerifier {
Map<String, ManifestParser.Section> entryNameToManifestSection, Map<String, ManifestParser.Section> entryNameToManifestSection,
Map<Integer, String> supportedApkSigSchemeNames, Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds, Set<Integer> foundApkSigSchemeIds,
int minSdkVersion) { int minSdkVersion,
int maxSdkVersion) {
// Inspect the main section of the .SF file. // Inspect the main section of the .SF file.
ManifestParser sf = new ManifestParser(mSigFileBytes); ManifestParser sf = new ManifestParser(mSigFileBytes);
ManifestParser.Section sfMainSection = sf.readSection(); ManifestParser.Section sfMainSection = sf.readSection();
@@ -854,10 +867,16 @@ public abstract class V1SchemeVerifier {
setIgnored(); setIgnored();
return; return;
} }
checkForStrippedApkSignatures(
sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds); if (maxSdkVersion >= AndroidSdkVersion.N) {
if (mResult.containsErrors()) { // Android N and newer rejects APKs whose .SF file says they were supposed to be
return; // 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; boolean createdBySigntool = false;
@@ -867,10 +886,18 @@ public abstract class V1SchemeVerifier {
} }
boolean manifestDigestVerified = boolean manifestDigestVerified =
verifyManifestDigest( verifyManifestDigest(
sfMainSection, createdBySigntool, manifestBytes, minSdkVersion); sfMainSection,
createdBySigntool,
manifestBytes,
minSdkVersion,
maxSdkVersion);
if (!createdBySigntool) { if (!createdBySigntool) {
verifyManifestMainSectionDigest( verifyManifestMainSectionDigest(
sfMainSection, manifestMainSection, manifestBytes, minSdkVersion); sfMainSection,
manifestMainSection,
manifestBytes,
minSdkVersion,
maxSdkVersion);
} }
if (mResult.containsErrors()) { if (mResult.containsErrors()) {
return; return;
@@ -922,7 +949,8 @@ public abstract class V1SchemeVerifier {
createdBySigntool, createdBySigntool,
manifestSection, manifestSection,
manifestBytes, manifestBytes,
minSdkVersion); minSdkVersion,
maxSdkVersion);
} }
mSigFileEntryNames = sfEntryNames; mSigFileEntryNames = sfEntryNames;
} }
@@ -936,12 +964,14 @@ public abstract class V1SchemeVerifier {
ManifestParser.Section sfMainSection, ManifestParser.Section sfMainSection,
boolean createdBySigntool, boolean createdBySigntool,
byte[] manifestBytes, byte[] manifestBytes,
int minSdkVersion) { int minSdkVersion,
int maxSdkVersion) {
Collection<NamedDigest> expectedDigests = Collection<NamedDigest> expectedDigests =
getDigestsToVerify( getDigestsToVerify(
sfMainSection, sfMainSection,
((createdBySigntool) ? "-Digest" : "-Digest-Manifest"), ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
minSdkVersion); minSdkVersion,
maxSdkVersion);
boolean digestFound = !expectedDigests.isEmpty(); boolean digestFound = !expectedDigests.isEmpty();
if (!digestFound) { if (!digestFound) {
mResult.addWarning( mResult.addWarning(
@@ -977,10 +1007,14 @@ public abstract class V1SchemeVerifier {
ManifestParser.Section sfMainSection, ManifestParser.Section sfMainSection,
ManifestParser.Section manifestMainSection, ManifestParser.Section manifestMainSection,
byte[] manifestBytes, byte[] manifestBytes,
int minSdkVersion) { int minSdkVersion,
int maxSdkVersion) {
Collection<NamedDigest> expectedDigests = Collection<NamedDigest> expectedDigests =
getDigestsToVerify( getDigestsToVerify(
sfMainSection, "-Digest-Manifest-Main-Attributes", minSdkVersion); sfMainSection,
"-Digest-Manifest-Main-Attributes",
minSdkVersion,
maxSdkVersion);
if (expectedDigests.isEmpty()) { if (expectedDigests.isEmpty()) {
return; return;
} }
@@ -1014,10 +1048,12 @@ public abstract class V1SchemeVerifier {
boolean createdBySigntool, boolean createdBySigntool,
ManifestParser.Section manifestIndividualSection, ManifestParser.Section manifestIndividualSection,
byte[] manifestBytes, byte[] manifestBytes,
int minSdkVersion) { int minSdkVersion,
int maxSdkVersion) {
String entryName = sfIndividualSection.getName(); String entryName = sfIndividualSection.getName();
Collection<NamedDigest> expectedDigests = Collection<NamedDigest> expectedDigests =
getDigestsToVerify(sfIndividualSection, "-Digest", minSdkVersion); getDigestsToVerify(
sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion);
if (expectedDigests.isEmpty()) { if (expectedDigests.isEmpty()) {
mResult.addError( mResult.addError(
Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
@@ -1124,7 +1160,8 @@ public abstract class V1SchemeVerifier {
private static Collection<NamedDigest> getDigestsToVerify( private static Collection<NamedDigest> getDigestsToVerify(
ManifestParser.Section section, ManifestParser.Section section,
String digestAttrSuffix, String digestAttrSuffix,
int minSdkVersion) { int minSdkVersion,
int maxSdkVersion) {
Decoder base64Decoder = Base64.getDecoder(); Decoder base64Decoder = Base64.getDecoder();
List<NamedDigest> result = new ArrayList<>(1); List<NamedDigest> result = new ArrayList<>(1);
if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) {
@@ -1163,21 +1200,23 @@ public abstract class V1SchemeVerifier {
} }
} }
// JB MR2 and newer, Android platform picks the strongest algorithm out of: if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) {
// SHA-512, SHA-384, SHA-256, SHA-1. // On JB MR2 and newer, Android platform picks the strongest algorithm out of:
for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { // SHA-512, SHA-384, SHA-256, SHA-1.
String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
String digestBase64 = section.getAttributeValue(attrName); String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
if (digestBase64 == null) { String digestBase64 = section.getAttributeValue(attrName);
// Attribute not found if (digestBase64 == null) {
continue; // 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;
} }
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; return result;