OSDN Git Service

maxSdkVersion can be specified for APK verification.
authorAlex Klyubin <klyubin@google.com>
Fri, 17 Jun 2016 16:38:32 +0000 (09:38 -0700)
committerAlex Klyubin <klyubin@google.com>
Fri, 17 Jun 2016 17:02:47 +0000 (10:02 -0700)
This enables verification of APKs which are served to a specific
range of Android platform versions, or to replicate behavior of
particular platform versions.

Bug: 27461702
Change-Id: I44ab4c99419eb97d72c4ccd109137fe1efda577d

tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java

index 9c58085..c3999b5 100644 (file)
@@ -56,23 +56,43 @@ public class ApkVerifier {
      * @param apk APK file contents
      * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures
      *        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 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);
 
-        // Attempt to verify the APK using APK Signature Scheme v2
         Result result = new Result();
-        Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
-        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;
+
+        // 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 {
+                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
@@ -86,7 +106,8 @@ public class ApkVerifier {
                             zipSections,
                             SUPPORTED_APK_SIG_SCHEME_NAMES,
                             foundApkSigSchemeIds,
-                            minSdkVersion);
+                            minSdkVersion,
+                            maxSdkVersion);
             result.mergeFrom(v1Result);
         }
         if (result.containsErrors()) {
index 91aa62e..60a47b2 100644 (file)
@@ -78,7 +78,14 @@ public abstract class V1SchemeVerifier {
             ApkUtils.ZipSections apkSections,
             Map<Integer, String> supportedApkSigSchemeNames,
             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();
 
         // Parse the ZIP Central Directory and check that there are no entries with duplicate names.
@@ -97,6 +104,7 @@ public abstract class V1SchemeVerifier {
                 supportedApkSigSchemeNames,
                 foundApkSigSchemeIds,
                 minSdkVersion,
+                maxSdkVersion,
                 result);
 
         return result;
@@ -143,6 +151,7 @@ public abstract class V1SchemeVerifier {
                 Map<Integer, String> supportedApkSigSchemeNames,
                 Set<Integer> foundApkSigSchemeIds,
                 int minSdkVersion,
+                int maxSdkVersion,
                 Result result) throws ZipFormatException, IOException {
 
             // 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
             // mimic Android's behavior.
             for (Signer signer : signers) {
-                signer.verifySigBlockAgainstSigFile(apk, cdStartOffset, minSdkVersion);
+                signer.verifySigBlockAgainstSigFile(
+                        apk, cdStartOffset, minSdkVersion, maxSdkVersion);
                 if (signer.getResult().containsErrors()) {
                     result.signers.add(signer.getResult());
                 }
@@ -264,7 +274,8 @@ public abstract class V1SchemeVerifier {
                         entryNameToManifestSection,
                         supportedApkSigSchemeNames,
                         foundApkSigSchemeIds,
-                        minSdkVersion);
+                        minSdkVersion,
+                        maxSdkVersion);
                 if (signer.isIgnored()) {
                     result.ignoredSigners.add(signer.getResult());
                 } else {
@@ -393,7 +404,7 @@ public abstract class V1SchemeVerifier {
 
         @SuppressWarnings("restriction")
         public void verifySigBlockAgainstSigFile(
-                DataSource apk, long cdStartOffset, int minSdkVersion)
+                DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
                         throws IOException, ZipFormatException {
             byte[] sigBlockBytes =
                     LocalFileHeader.getUncompressedData(
@@ -433,7 +444,8 @@ public abstract class V1SchemeVerifier {
                     String signatureAlgorithmOid =
                             unverifiedSignerInfo
                                     .getDigestEncryptionAlgorithmId().getOID().toString();
-                    InclusiveIntRange desiredApiLevels = InclusiveIntRange.from(minSdkVersion);
+                    InclusiveIntRange desiredApiLevels =
+                            InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
                     List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
                             getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
                     List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
@@ -843,7 +855,8 @@ public abstract class V1SchemeVerifier {
                 Map<String, ManifestParser.Section> entryNameToManifestSection,
                 Map<Integer, String> supportedApkSigSchemeNames,
                 Set<Integer> foundApkSigSchemeIds,
-                int minSdkVersion) {
+                int minSdkVersion,
+                int maxSdkVersion) {
             // Inspect the main section of the .SF file.
             ManifestParser sf = new ManifestParser(mSigFileBytes);
             ManifestParser.Section sfMainSection = sf.readSection();
@@ -854,10 +867,16 @@ public abstract class V1SchemeVerifier {
                 setIgnored();
                 return;
             }
-            checkForStrippedApkSignatures(
-                    sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
-            if (mResult.containsErrors()) {
-                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;
@@ -867,10 +886,18 @@ public abstract class V1SchemeVerifier {
             }
             boolean manifestDigestVerified =
                     verifyManifestDigest(
-                            sfMainSection, createdBySigntool, manifestBytes, minSdkVersion);
+                            sfMainSection,
+                            createdBySigntool,
+                            manifestBytes,
+                            minSdkVersion,
+                            maxSdkVersion);
             if (!createdBySigntool) {
                 verifyManifestMainSectionDigest(
-                        sfMainSection, manifestMainSection, manifestBytes, minSdkVersion);
+                        sfMainSection,
+                        manifestMainSection,
+                        manifestBytes,
+                        minSdkVersion,
+                        maxSdkVersion);
             }
             if (mResult.containsErrors()) {
                 return;
@@ -922,7 +949,8 @@ public abstract class V1SchemeVerifier {
                         createdBySigntool,
                         manifestSection,
                         manifestBytes,
-                        minSdkVersion);
+                        minSdkVersion,
+                        maxSdkVersion);
             }
             mSigFileEntryNames = sfEntryNames;
         }
@@ -936,12 +964,14 @@ public abstract class V1SchemeVerifier {
                 ManifestParser.Section sfMainSection,
                 boolean createdBySigntool,
                 byte[] manifestBytes,
-                int minSdkVersion) {
+                int minSdkVersion,
+                int maxSdkVersion) {
             Collection<NamedDigest> expectedDigests =
                     getDigestsToVerify(
                             sfMainSection,
                             ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
-                            minSdkVersion);
+                            minSdkVersion,
+                            maxSdkVersion);
             boolean digestFound = !expectedDigests.isEmpty();
             if (!digestFound) {
                 mResult.addWarning(
@@ -977,10 +1007,14 @@ public abstract class V1SchemeVerifier {
                 ManifestParser.Section sfMainSection,
                 ManifestParser.Section manifestMainSection,
                 byte[] manifestBytes,
-                int minSdkVersion) {
+                int minSdkVersion,
+                int maxSdkVersion) {
             Collection<NamedDigest> expectedDigests =
                     getDigestsToVerify(
-                            sfMainSection, "-Digest-Manifest-Main-Attributes", minSdkVersion);
+                            sfMainSection,
+                            "-Digest-Manifest-Main-Attributes",
+                            minSdkVersion,
+                            maxSdkVersion);
             if (expectedDigests.isEmpty()) {
                 return;
             }
@@ -1014,10 +1048,12 @@ public abstract class V1SchemeVerifier {
                 boolean createdBySigntool,
                 ManifestParser.Section manifestIndividualSection,
                 byte[] manifestBytes,
-                int minSdkVersion) {
+                int minSdkVersion,
+                int maxSdkVersion) {
             String entryName = sfIndividualSection.getName();
             Collection<NamedDigest> expectedDigests =
-                    getDigestsToVerify(sfIndividualSection, "-Digest", minSdkVersion);
+                    getDigestsToVerify(
+                            sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion);
             if (expectedDigests.isEmpty()) {
                 mResult.addError(
                         Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
@@ -1124,7 +1160,8 @@ public abstract class V1SchemeVerifier {
     private static Collection<NamedDigest> getDigestsToVerify(
             ManifestParser.Section section,
             String digestAttrSuffix,
-            int minSdkVersion) {
+            int minSdkVersion,
+            int maxSdkVersion) {
         Decoder base64Decoder = Base64.getDecoder();
         List<NamedDigest> result = new ArrayList<>(1);
         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:
-        // 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));
+        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;
             }
-            break;
         }
 
         return result;