Merge Android 14 QPR2 to AOSP main

Bug: 319669529
Merged-In: Ib2f81cd63a741a25c67a6114fefbf4a47a968a91
Change-Id: Ie8964fcbe4f39e1b2db1a4766edf100852fecbaa
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 078996a..50b3d9f 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -27,6 +27,7 @@
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
 import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
 import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
 
@@ -510,21 +511,36 @@
             List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
                     v4Signers.get(0).getContentDigests();
             if (digestsFromV4.size() != 1) {
-                result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+                result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV4.size());
+                if (digestsFromV4.isEmpty()) {
+                    return result;
+                }
             }
             final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
 
             if (result.isVerifiedUsingV3Scheme()) {
-                int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1;
+                final boolean isV31 = result.isVerifiedUsingV31Scheme();
+                final int expectedSize = isV31 ? 2 : 1;
                 if (v4Signers.size() != expectedSize) {
-                    result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+                    result.addError(isV31 ? Issue.V41_SIG_NEEDS_TWO_SIGNERS
+                            : Issue.V4_SIG_MULTIPLE_SIGNERS);
+                    return result;
                 }
 
                 checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
                         result);
-                if (result.isVerifiedUsingV31Scheme()) {
+                if (isV31) {
+                    List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV41 =
+                            v4Signers.get(1).getContentDigests();
+                    if (digestsFromV41.size() != 1) {
+                        result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV41.size());
+                        if (digestsFromV41.isEmpty()) {
+                            return result;
+                        }
+                    }
+                    final byte[] digestFromV41 = digestsFromV41.get(0).getValue();
                     checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
-                            digestFromV4, result);
+                            digestFromV41, result);
                 }
             } else if (result.isVerifiedUsingV2Scheme()) {
                 if (v4Signers.size() != 1) {
@@ -543,7 +559,8 @@
                 final byte[] digestFromV2 = pickBestDigestForV4(
                         v2Signers.get(0).getContentDigests());
                 if (!Arrays.equals(digestFromV4, digestFromV2)) {
-                    result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+                    result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 2, toHex(digestFromV2),
+                            toHex(digestFromV4));
                 }
             } else {
                 throw new RuntimeException("V4 signature must be also verified with V2/V3");
@@ -1135,7 +1152,8 @@
         // Compare digests.
         final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
         if (!Arrays.equals(digestFromV4, digestFromV3)) {
-            result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+            result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 3, toHex(digestFromV3),
+                    toHex(digestFromV4));
         }
     }
 
@@ -3124,14 +3142,40 @@
                 "V4 signature only supports one signer"),
 
         /**
+         * V4.1 signature requires two signers to match the v3 and the v3.1.
+         */
+        V41_SIG_NEEDS_TWO_SIGNERS("V4.1 signature requires two signers"),
+
+        /**
          * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
          * used to sign APK Signature Scheme V4 signature.
          */
         V4_SIG_V2_V3_SIGNERS_MISMATCH(
                 "V4 signature and V2/V3 signature have mismatched certificates"),
 
+        /**
+         * The v4 signature's digest does not match the digest from the corresponding v2 / v3
+         * signature.
+         *
+         * <ul>
+         *     <li>Parameter 1: Signature scheme of mismatched digest ({@code int})
+         *     <li>Parameter 2: v2/v3 digest ({@code String})
+         *     <li>Parameter 3: v4 digest ({@code String})
+         * </ul>
+         */
         V4_SIG_V2_V3_DIGESTS_MISMATCH(
-                "V4 signature and V2/V3 signature have mismatched digests"),
+                "V4 signature and V%1$d signature have mismatched digests, V%1$d digest: %2$s, V4"
+                        + " digest: %3$s"),
+
+        /**
+         * The v4 signature does not contain the expected number of digests.
+         *
+         * <ul>
+         *     <li>Parameter 1: Number of digests found ({@code int})
+         * </ul>
+         */
+        V4_SIG_UNEXPECTED_DIGESTS(
+                "V4 signature does not have the expected number of digests, found %1$d"),
 
         /**
          * The v4 signature format version isn't the same as the tool's current version, something
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 2f9ecb3..7bf952d 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -16,8 +16,12 @@
 
 package com.android.apksig.internal.apk.v4;
 
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
 import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
 import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
 import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
 
 import com.android.apksig.apk.ApkUtils;
@@ -26,7 +30,6 @@
 import com.android.apksig.internal.apk.SignatureAlgorithm;
 import com.android.apksig.internal.apk.SignatureInfo;
 import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
-import com.android.apksig.internal.apk.v3.V3SchemeConstants;
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
 import com.android.apksig.internal.util.Pair;
@@ -43,11 +46,12 @@
 import java.security.PublicKey;
 import java.security.SignatureException;
 import java.security.cert.CertificateEncodingException;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -136,8 +140,9 @@
 
         final long fileSize = apkContent.size();
 
-        // Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512).
-        final byte[] apkDigest = getApkDigest(apkContent);
+        // Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks
+        // (CHUNKED_SHA256 or CHUNKED_SHA512).
+        final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent);
 
         // Obtaining the merkle tree and the root hash in verity format.
         ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
@@ -157,7 +162,7 @@
         // Generating SigningInfo and combining everything into V4Signature.
         final V4Signature signature;
         try {
-            signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData,
+            signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData,
                     fileSize);
         } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
             throw new InvalidKeyException("Signer failed", e);
@@ -209,16 +214,24 @@
     private static V4Signature generateSignature(
             SignerConfig signerConfig,
             V4Signature.HashingInfo hashingInfo,
-            byte[] apkDigest, byte[] additionalData, long fileSize)
+            Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize)
             throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
             CertificateEncodingException {
+        byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
+                ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
+                    : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);
         final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
                 hashingInfo, apkDigest, additionalData, fileSize);
 
         final V4Signature.SigningInfos signingInfos;
         if (signerConfig.v41Config != null) {
+            if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) {
+                throw new IllegalStateException(
+                        "V4.1 cannot be signed without a V3.1 content digest");
+            }
+            apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31);
             final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
-                    V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
+                    APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
                     generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
                             additionalData, fileSize).toByteArray());
             signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
@@ -230,8 +243,16 @@
                 signingInfos.toByteArray());
     }
 
-    // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
-    private static byte[] getApkDigest(DataSource apk) throws IOException {
+    /**
+     * Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the
+     * strongest supported content digest found in that version's signature block for the V2,
+     * V3, and V3.1 signatures in the provided {@code apk}.
+     *
+     * <p>If a supported content digest algorithm is not found in any of the signature blocks,
+     * or if the APK is not signed by any of these signature schemes, then an {@code IOException}
+     * is thrown.
+     */
+    private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException {
         ApkUtils.ZipSections zipSections;
         try {
             zipSections = ApkUtils.findZipSections(apk);
@@ -239,34 +260,60 @@
             throw new IOException("Malformed APK: not a ZIP archive", e);
         }
 
-        final SignatureException v3Exception;
+        Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1);
         try {
-            return getBestV3Digest(apk, zipSections);
+            byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest);
+        } catch (SignatureException expected) {
+            // It is expected to catch a SignatureException if the APK does not have a v3.1
+            // signature.
+        }
+
+        SignatureException v3Exception = null;
+        try {
+            byte[] digest =  getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest);
         } catch (SignatureException e) {
             v3Exception = e;
         }
 
-        final SignatureException v2Exception;
+        SignatureException v2Exception = null;
         try {
-            return getBestV2Digest(apk, zipSections);
+            byte[] digest = getBestV2Digest(apk, zipSections);
+            sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest);
         } catch (SignatureException e) {
             v2Exception = e;
         }
 
+        if (sigSchemeToDigest.size() > 0) {
+            return sigSchemeToDigest;
+        }
+
         throw new IOException(
                 "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
                         + v2Exception);
     }
 
-    private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections)
-            throws SignatureException {
+    private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections,
+            int v3SchemeVersion) throws SignatureException {
         final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
         final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
-                ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+                v3SchemeVersion);
+        final int blockId;
+        switch (v3SchemeVersion) {
+            case VERSION_APK_SIGNATURE_SCHEME_V31:
+                blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+                break;
+            case VERSION_APK_SIGNATURE_SCHEME_V3:
+                blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid V3 scheme provided: " + v3SchemeVersion);
+        }
         try {
             final SignatureInfo signatureInfo =
-                    ApkSigningBlockUtils.findSignature(apk, zipSections,
-                            APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+                    ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result);
             final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
             V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
                     result);
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 0b6702a..525f38f 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -55,7 +55,6 @@
 import com.android.apksig.util.DataSources;
 import com.android.apksig.zip.ZipFormatException;
 
-import java.security.InvalidKeyException;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -102,6 +101,8 @@
     static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
     static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
 
+    static final String FIRST_RSA_4096_SIGNER_RESOURCE_NAME = "rsa-4096";
+
     private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
     private static final String EC_P256_2_SIGNER_RESOURCE_NAME = "ec-p256_2";
 
@@ -117,6 +118,8 @@
             "rsa-2048-lineage-3-signers-1-no-caps";
     private static final String LINEAGE_RSA_2048_2_SIGNERS_2_3_RESOURCE_NAME =
             "rsa-2048-lineage-2-signers-2-3";
+    private static final String LINEAGE_RSA_2048_TO_RSA_4096_RESOURCE_NAME =
+            "rsa-2048-to-4096-lineage-2-signers";
 
     private static final String LINEAGE_EC_P256_2_SIGNERS_RESOURCE_NAME =
             "ec-p256-lineage-2-signers";
@@ -3050,6 +3053,37 @@
     }
 
     @Test
+    public void testV41_rotationWithDifferentDigestAlgos_v41UsesCorrectDigest() throws Exception {
+        // When signing an APK, the digest algorithm is determined by the number of bits in the
+        // signing key to ensure the digest is not weaker than the key. If an original signing key
+        // meets the requirements for the CHUNKED_SHA256 digest and the rotated signing key
+        // meets the requirements for CHUNKED_SHA512, then the v3.0 and v3.1 signing blocks will
+        // use different digests. The v4.1 signature must use the content digest from the v3.1
+        // block since that's the digest that will be used to verify the v4.1 signature on all
+        // platform versions that support the v3.1 signer.
+        List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+                Arrays.asList(
+                        getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+                        getDefaultSignerConfigFromResources(FIRST_RSA_4096_SIGNER_RESOURCE_NAME));
+        SigningCertificateLineage lineage =
+                Resources.toSigningCertificateLineage(
+                        ApkSignerTest.class, LINEAGE_RSA_2048_TO_RSA_4096_RESOURCE_NAME);
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(true)
+                        .setMinSdkVersionForRotation(AndroidSdkVersion.T)
+                        .setSigningCertificateLineage(lineage));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertResultContainsV4Signers(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+                FIRST_RSA_4096_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
     public void
     testSourceStampTimestamp_signWithSourceStampAndTimestampDefault_validTimestampValue()
             throws Exception {
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 3242f5e..763fee0 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -44,13 +44,17 @@
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.security.Provider;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import org.junit.Assume;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -75,6 +79,8 @@
 
 @RunWith(JUnit4.class)
 public class ApkVerifierTest {
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
     private static final String[] DSA_KEY_NAMES = {"1024", "2048", "3072"};
     private static final String[] DSA_KEY_NAMES_1024_AND_SMALLER = {"1024"};
@@ -574,9 +580,10 @@
 
         // Signature claims to be RSA PKCS#1 v1.5 with SHA-256, but is actually using SHA-512.
         // Based on v2-only-with-rsa-pkcs1-sha256-2048.apk.
-        assertVerificationFailure(
-                "v2-only-with-rsa-pkcs1-sha256-2048-sig-does-not-verify.apk",
-                Issue.V2_SIG_VERIFY_EXCEPTION);
+        assertVerificationIssue(
+                verify("v2-only-with-rsa-pkcs1-sha256-2048-sig-does-not-verify.apk"),
+                true,
+                Issue.V2_SIG_VERIFY_EXCEPTION, Issue.V2_SIG_DID_NOT_VERIFY);
 
         // Bitflip in the ECDSA signature. Based on v2-only-with-ecdsa-sha256-p256.apk.
         assertVerificationFailure(
@@ -1843,6 +1850,16 @@
         assertTrue(result.isVerifiedUsingV31Scheme());
     }
 
+    @Test
+    public void verify41_v41DigestMismatchedWithV31_reportsError() throws Exception {
+        // This test verifies a digest mismatch between the v4.1 signature and the v3.1 signature
+        // is properly reported during v4 signature verification.
+        ApkVerifier.Result result = verifyWithV4Signature("v41-digest-mismatched-with-v31.apk",
+                "v41-digest-mismatched-with-v31.apk.idsig");
+
+        assertVerificationFailure(result, Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+    }
+
     @Test(expected = IOException.class)
     public void verify_largeFileSize_doesNotFailWithOOMError() throws Exception {
         // During V1 signature verification, each file needs to be uncompressed to calculate
@@ -1972,6 +1989,21 @@
         return builder.build().verify();
     }
 
+    private ApkVerifier.Result verifyWithV4Signature(
+            String apkFilenameInResources,
+            String v4SignatureFile)
+            throws IOException, ApkFormatException, NoSuchAlgorithmException, URISyntaxException {
+        byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
+
+        ApkVerifier.Builder builder =
+                new ApkVerifier.Builder(DataSources.asDataSource(ByteBuffer.wrap(apkBytes)));
+        if (v4SignatureFile != null) {
+            builder.setV4SignatureFile(
+                    Resources.toFile(getClass(), v4SignatureFile, mTemporaryFolder));
+        }
+        return builder.build().verify();
+    }
+
     private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources) throws Exception {
         return verifySourceStamp(apkFilenameInResources, null, null, null);
     }
@@ -2089,28 +2121,30 @@
     }
 
     static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
-        assertVerificationIssue(result, expectedIssue, true);
+        assertVerificationIssue(result, true, expectedIssue);
     }
 
     static void assertVerificationWarning(ApkVerifier.Result result, Issue expectedIssue) {
-        assertVerificationIssue(result, expectedIssue, false);
+        assertVerificationIssue(result, false, expectedIssue);
     }
 
     /**
-     * Asserts the provided {@code result} contains the {@code expectedIssue}; if {@code
+     * Asserts the provided {@code result} contains one of the {@code expectedIssues}; if {@code
      * verifyError} is set to {@code true} then the specified {@link Issue} will be expected as an
      * error, otherwise it will be expected as a warning.
      */
-    private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue,
-            boolean verifyError) {
+    private static void assertVerificationIssue(ApkVerifier.Result result, boolean verifyError,
+            Issue... expectedIssues) {
+        List<Issue> expectedIssuesList = expectedIssues != null
+                ? Arrays.asList(expectedIssues) : Collections.emptyList();
         if (result.isVerified() && verifyError) {
-            fail("APK verification succeeded instead of failing with " + expectedIssue);
+            fail("APK verification succeeded instead of failing with " + expectedIssuesList);
             return;
         }
 
         StringBuilder msg = new StringBuilder();
         for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) {
-            if (issue.getIssue().equals(expectedIssue)) {
+            if (expectedIssuesList.contains(issue.getIssue())) {
                 return;
             }
             if (msg.length() > 0) {
@@ -2122,7 +2156,7 @@
             String signerName = signer.getName();
             for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (issue.getIssue().equals(expectedIssue)) {
+                if (expectedIssuesList.contains(issue.getIssue())) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -2140,7 +2174,7 @@
             String signerName = "signer #" + (signer.getIndex() + 1);
             for (IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (issue.getIssue().equals(expectedIssue)) {
+                if (expectedIssuesList.contains(issue.getIssue())) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -2156,7 +2190,7 @@
             String signerName = "signer #" + (signer.getIndex() + 1);
             for (IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (issue.getIssue().equals(expectedIssue)) {
+                if (expectedIssuesList.contains(issue.getIssue())) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -2172,7 +2206,7 @@
             String signerName = "signer #" + (signer.getIndex() + 1);
             for (IssueWithParams issue : (verifyError ? signer.getErrors()
                     : signer.getWarnings())) {
-                if (issue.getIssue().equals(expectedIssue)) {
+                if (expectedIssuesList.contains(issue.getIssue())) {
                     return;
                 }
                 if (msg.length() > 0) {
@@ -2184,14 +2218,14 @@
                         .append(issue);
             }
         }
-        if (expectedIssue == null && msg.length() == 0) {
+        if ((expectedIssuesList.isEmpty() || expectedIssues[0] == null) && msg.length() == 0) {
             return;
         }
 
         fail(
                 "APK failed verification for the wrong reason"
                         + ". Expected: "
-                        + expectedIssue
+                        + expectedIssuesList
                         + ", actual: "
                         + msg);
     }
diff --git a/src/test/java/com/android/apksig/internal/util/Resources.java b/src/test/java/com/android/apksig/internal/util/Resources.java
index 82bf76f..ac00946 100644
--- a/src/test/java/com/android/apksig/internal/util/Resources.java
+++ b/src/test/java/com/android/apksig/internal/util/Resources.java
@@ -20,8 +20,13 @@
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.util.DataSource;
 
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
@@ -140,4 +145,17 @@
         DataSource lineageDataSource = toDataSource(cls, fileResourceName);
         return SigningCertificateLineage.readFromDataSource(lineageDataSource);
     }
+
+    public static File toFile(Class<?> cls, String fileResourceName,
+            TemporaryFolder temporaryFolder) throws IOException {
+        File outFile = temporaryFolder.newFile();
+        try (InputStream in = cls.getResourceAsStream(fileResourceName);
+             OutputStream out = new FileOutputStream(outFile)) {
+            if (in == null) {
+                throw new IllegalArgumentException("Resource not found: " + fileResourceName);
+            }
+            in.transferTo(out);
+            return outFile;
+        }
+    }
 }
diff --git a/src/test/resources/com/android/apksig/rsa-2048-to-4096-lineage-2-signers b/src/test/resources/com/android/apksig/rsa-2048-to-4096-lineage-2-signers
new file mode 100644
index 0000000..d98b2db
--- /dev/null
+++ b/src/test/resources/com/android/apksig/rsa-2048-to-4096-lineage-2-signers
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk b/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk
new file mode 100644
index 0000000..348bb1d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk.idsig b/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk.idsig
new file mode 100644
index 0000000..d2e1399
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v41-digest-mismatched-with-v31.apk.idsig
Binary files differ