Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f31ced2e authored by Alex Klyubin's avatar Alex Klyubin
Browse files

maxSdkVersion can be specified for APK verification.

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
parent 21213cff
Loading
Loading
Loading
Loading
+32 −11
Original line number Diff line number Diff line
@@ -56,16 +56,33 @@ 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);

        // 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);
@@ -74,6 +91,9 @@ public class ApkVerifier {
            if (result.containsErrors()) {
                return result;
            }
        } else {
            foundApkSigSchemeIds = Collections.emptySet();
        }

        // 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.
@@ -86,7 +106,8 @@ public class ApkVerifier {
                            zipSections,
                            SUPPORTED_APK_SIG_SCHEME_NAMES,
                            foundApkSigSchemeIds,
                            minSdkVersion);
                            minSdkVersion,
                            maxSdkVersion);
            result.mergeFrom(v1Result);
        }
        if (result.containsErrors()) {
+73 −34
Original line number Diff line number Diff line
@@ -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,11 +867,17 @@ public abstract class V1SchemeVerifier {
                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");
@@ -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,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.
            for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
                String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
@@ -1179,6 +1217,7 @@ public abstract class V1SchemeVerifier {
                }
                break;
            }
        }

        return result;
    }