Merge "maxSdkVersion can be specified for APK verification."
This commit is contained in:
@@ -56,16 +56,33 @@ 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);
|
|
||||||
|
// 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 {
|
try {
|
||||||
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
|
V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
|
||||||
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
|
foundApkSigSchemeIds.add(APK_SIGNATURE_SCHEME_V2_ID);
|
||||||
@@ -74,6 +91,9 @@ public class ApkVerifier {
|
|||||||
if (result.containsErrors()) {
|
if (result.containsErrors()) {
|
||||||
return result;
|
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
|
||||||
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
|
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
|
||||||
@@ -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()) {
|
||||||
|
@@ -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,11 +867,17 @@ public abstract class V1SchemeVerifier {
|
|||||||
setIgnored();
|
setIgnored();
|
||||||
return;
|
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(
|
checkForStrippedApkSignatures(
|
||||||
sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
|
sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
|
||||||
if (mResult.containsErrors()) {
|
if (mResult.containsErrors()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
boolean createdBySigntool = false;
|
boolean createdBySigntool = false;
|
||||||
String createdBy = sfMainSection.getAttributeValue("Created-By");
|
String createdBy = sfMainSection.getAttributeValue("Created-By");
|
||||||
@@ -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,7 +1200,8 @@ public abstract class V1SchemeVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JB MR2 and newer, Android platform picks the strongest algorithm out of:
|
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.
|
// SHA-512, SHA-384, SHA-256, SHA-1.
|
||||||
for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
|
for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
|
||||||
String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
|
String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
|
||||||
@@ -1179,6 +1217,7 @@ public abstract class V1SchemeVerifier {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user