release-request-e73a0a41-91c3-4249-808e-8d196d54a344-for-git_oc-mr1-release-4273744 snap-temp-L04700000093069831

Change-Id: I7d2e75022566c927f73e9b61fb370578f27325b8
diff --git a/Android.mk b/Android.mk
index f78e412..bc33f01 100644
--- a/Android.mk
+++ b/Android.mk
@@ -21,20 +21,6 @@
 LOCAL_MODULE := apksig
 LOCAL_SRC_FILES := $(call all-java-files-under, src/main/java)
 
-# Disable warnnings about our use of internal proprietary OpenJDK API.
-# TODO: Remove this workaround by moving to our own implementation of PKCS #7
-# SignedData block generation, parsing, and verification.
-LOCAL_JAVACFLAGS := -XDignore.symbol.file
-
-# http://b/63748551 apksig relies on some (internal) sun.security.* imports.
-# Export the corresponding packages for now so it can compile under OpenJDK 9's
-# javac -target 1.9 -source 1.9
-ifeq ($(EXPERIMENTAL_USE_OPENJDK9),true)
-LOCAL_JAVACFLAGS += --add-exports java.base/sun.security.pkcs=ALL-UNNAMED
-LOCAL_JAVACFLAGS += --add-exports java.base/sun.security.util=ALL-UNNAMED
-LOCAL_JAVACFLAGS += --add-exports java.base/sun.security.x509=ALL-UNNAMED
-endif
-
 include $(BUILD_HOST_JAVA_LIBRARY)
 
 
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 9aed804..ffdf4d1 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -28,6 +28,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
@@ -68,7 +69,7 @@
  */
 public class ApkSignerTool {
 
-    private static final String VERSION = "0.7";
+    private static final String VERSION = "0.8";
     private static final String HELP_PAGE_GENERAL = "help.txt";
     private static final String HELP_PAGE_SIGN = "help_sign.txt";
     private static final String HELP_PAGE_VERIFY = "help_verify.txt";
@@ -161,6 +162,16 @@
                         optionsParser.getRequiredValue("KeyStore password");
             } else if ("key-pass".equals(optionName)) {
                 signerParams.keyPasswordSpec = optionsParser.getRequiredValue("Key password");
+            } else if ("pass-encoding".equals(optionName)) {
+                String charsetName =
+                        optionsParser.getRequiredValue("Password character encoding");
+                try {
+                    signerParams.passwordCharset = PasswordRetriever.getCharsetByName(charsetName);
+                } catch (IllegalArgumentException e) {
+                    throw new ParameterException(
+                            "Unsupported password character encoding requested using"
+                                    + " --pass-encoding: " + charsetName);
+                }
             } else if ("v1-signer-name".equals(optionName)) {
                 signerParams.v1SigFileBasename =
                         optionsParser.getRequiredValue("JAR signature file basename");
@@ -604,6 +615,7 @@
         String keystoreKeyAlias;
         String keystorePasswordSpec;
         String keyPasswordSpec;
+        Charset passwordCharset;
         String keystoreType;
         String keystoreProviderName;
         String keystoreProviderClass;
@@ -623,6 +635,7 @@
                     && (keystoreKeyAlias == null)
                     && (keystorePasswordSpec == null)
                     && (keyPasswordSpec == null)
+                    && (passwordCharset == null)
                     && (keystoreType == null)
                     && (keystoreProviderName == null)
                     && (keystoreProviderClass == null)
@@ -690,13 +703,19 @@
 
             // 2. Load the KeyStore
             List<char[]> keystorePasswords;
+            Charset[] additionalPasswordEncodings;
             {
                 String keystorePasswordSpec =
                         (this.keystorePasswordSpec != null)
                                 ?  this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN;
+                additionalPasswordEncodings =
+                        (passwordCharset != null)
+                                ? new Charset[] {passwordCharset} : new Charset[0];
                 keystorePasswords =
                         passwordRetriever.getPasswords(
-                                keystorePasswordSpec, "Keystore password for " + name);
+                                keystorePasswordSpec,
+                                "Keystore password for " + name,
+                                additionalPasswordEncodings);
                 loadKeyStoreFromFile(
                         ks,
                         "NONE".equals(keystoreFile) ? null : keystoreFile,
@@ -746,7 +765,8 @@
                     List<char[]> keyPasswords =
                             passwordRetriever.getPasswords(
                                     keyPasswordSpec,
-                                    "Key \"" + keyAlias + "\" password for " + name);
+                                    "Key \"" + keyAlias + "\" password for " + name,
+                                    additionalPasswordEncodings);
                     entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                 } else {
                     // Key password spec is not specified. This means we should assume that key
@@ -759,7 +779,8 @@
                         List<char[]> keyPasswords =
                                 passwordRetriever.getPasswords(
                                         PasswordRetriever.SPEC_STDIN,
-                                        "Key \"" + keyAlias + "\" password for " + name);
+                                        "Key \"" + keyAlias + "\" password for " + name,
+                                        additionalPasswordEncodings);
                         entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                     }
                 }
@@ -858,9 +879,14 @@
                 // The blob is indeed an encrypted private key blob
                 String passwordSpec =
                         (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN;
+                Charset[] additionalPasswordEncodings =
+                        (passwordCharset != null)
+                                ? new Charset[] {passwordCharset} : new Charset[0];
                 List<char[]> keyPasswords =
                         passwordRetriver.getPasswords(
-                                passwordSpec, "Private key password for " + name);
+                                passwordSpec,
+                                "Private key password for " + name,
+                                additionalPasswordEncodings);
                 keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords);
             } catch (IOException e) {
                 // The blob is not an encrypted private key blob
diff --git a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
index c09089d..83437ed 100644
--- a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
+++ b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
@@ -42,23 +42,29 @@
  * input) which adds the need to keep some sources open across password retrievals. This class
  * addresses the need.
  *
- * <p>To use this retriever, construct a new instance, use {@link #getPasswords(String, String)} to
- * retrieve passwords, and then invoke {@link #close()} on the instance when done, enabling the
- * instance to release any held resources.
+ * <p>To use this retriever, construct a new instance, use
+ * {@link #getPasswords(String, String, Charset...)} to retrieve passwords, and then invoke
+ * {@link #close()} on the instance when done, enabling the instance to release any held resources.
  */
 class PasswordRetriever implements AutoCloseable {
     public static final String SPEC_STDIN = "stdin";
 
-    private static final Charset CONSOLE_CHARSET = getConsoleEncoding();
+    /** Character encoding used by the console or {@code null} if not known. */
+    private final Charset mConsoleEncoding;
 
     private final Map<File, InputStream> mFileInputStreams = new HashMap<>();
 
     private boolean mClosed;
 
+    PasswordRetriever() {
+        mConsoleEncoding = getConsoleEncoding();
+    }
+
     /**
      * Returns the passwords described by the provided spec. The reason there may be more than one
      * password is compatibility with {@code keytool} and {@code jarsigner} which in certain cases
-     * use the form of passwords encoded using the console's character encoding.
+     * use the form of passwords encoded using the console's character encoding or the JVM default
+     * encoding.
      *
      * <p>Supported specs:
      * <ul>
@@ -72,8 +78,17 @@
      *
      * <p>When the same file (including standard input) is used for providing multiple passwords,
      * the passwords are read from the file one line at a time.
+     *
+     * @param additionalPwdEncodings additional encodings for converting the password into KeyStore
+     *        or PKCS #8 encrypted key password. These encoding are used in addition to using the
+     *        password verbatim or encoded using JVM default character encoding. A useful encoding
+     *        to provide is the console character encoding on Windows machines where the console
+     *        may be different from the JVM default encoding. Unfortunately, there is no public API
+     *        to obtain the console's character encoding.
      */
-    public List<char[]> getPasswords(String spec, String description) throws IOException {
+    public List<char[]> getPasswords(
+            String spec, String description, Charset... additionalPwdEncodings)
+                    throws IOException {
         // IMPLEMENTATION NOTE: Java KeyStore and PBEKeySpec APIs take passwords as arrays of
         // Unicode characters (char[]). Unfortunately, it appears that Sun/Oracle keytool and
         // jarsigner in some cases use passwords which are the encoded form obtained using the
@@ -82,12 +97,13 @@
         // encoded form to char. This occurs only when the password is read from stdin/console, and
         // does not occur when the password is read from a command-line parameter.
         // There are other tools which use the Java KeyStore API correctly.
-        // Thus, for each password spec, there may be up to three passwords:
+        // Thus, for each password spec, a valid password is typically one of these three:
         // * Unicode characters,
         // * characters (upcast bytes) obtained from encoding the password using the console's
-        //   character encoding,
+        //   character encoding of the console used on the environment where the KeyStore was
+        //   created,
         // * characters (upcast bytes) obtained from encoding the password using the JVM's default
-        //   character encoding.
+        //   character encoding of the machine where the KeyStore was created.
         //
         // For a sample password "\u0061\u0062\u00a1\u00e4\u044e\u0031":
         // On Windows 10 with English US as the UI language, IBM437 is used as console encoding and
@@ -106,14 +122,25 @@
         //   generates a keystore and key which decrypt only with
         //   "\u0061\u0062\u00c2\u00a1\u00c3\u00a4\u00d1\u008e\u0031"
         // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
-        //     -alias test
+        //     -alias test -storepass <pass here>
         //   generates a keystore and key which decrypt only with
         //   "\u0061\u0062\u00a1\u00e4\u044e\u0031"
+        //
+        // We optimize for the case where the KeyStore was created on the same machine where
+        // apksigner is executed. Thus, we can assume the JVM default encoding used for creating the
+        // KeyStore is the same as the current JVM's default encoding. We can make a similar
+        // assumption about the console's encoding. However, there is no public API for obtaining
+        // the console's character encoding. Prior to Java 9, we could cheat by using Reflection API
+        // to access Console.encoding field. However, in the official Java 9 JVM this field is not
+        // only inaccessible, but results in warnings being spewed to stdout during access attempts.
+        // As a result, we cannot auto-detect the console's encoding and thus rely on the user to
+        // explicitly provide it to apksigner as a command-line parameter (and passed into this
+        // method as additionalPwdEncodings), if the password is using non-ASCII characters.
 
         assertNotClosed();
         if (spec.startsWith("pass:")) {
             char[] pwd = spec.substring("pass:".length()).toCharArray();
-            return getPasswords(pwd);
+            return getPasswords(pwd, additionalPwdEncodings);
         } else if (SPEC_STDIN.equals(spec)) {
             Console console = System.console();
             if (console != null) {
@@ -122,9 +149,9 @@
                 if (pwd == null) {
                     throw new IOException("Failed to read " + description + ": console closed");
                 }
-                return getPasswords(pwd);
+                return getPasswords(pwd, additionalPwdEncodings);
             } else {
-                // Console not available -- reading from redirected input
+                // Console not available -- reading from standard input
                 System.out.println(description + ": ");
                 byte[] encodedPwd = readEncodedPassword(System.in);
                 if (encodedPwd.length == 0) {
@@ -132,9 +159,8 @@
                             "Failed to read " + description + ": standard input closed");
                 }
                 // By default, textual input obtained via standard input is supposed to be decoded
-                // using the in JVM default character encoding but we also try the console's
-                // encoding just in case.
-                return getPasswords(encodedPwd, Charset.defaultCharset(), CONSOLE_CHARSET);
+                // using the in JVM default character encoding.
+                return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
             }
         } else if (spec.startsWith("file:")) {
             String name = spec.substring("file:".length());
@@ -151,7 +177,7 @@
             }
             // By default, textual input from files is supposed to be treated as encoded using JVM's
             // default character encoding.
-            return getPasswords(encodedPwd, Charset.defaultCharset());
+            return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
         } else if (spec.startsWith("env:")) {
             String name = spec.substring("env:".length());
             String value = System.getenv(name);
@@ -160,7 +186,7 @@
                         "Failed to read " + description + ": environment variable " + value
                                 + " not specified");
             }
-            return getPasswords(value.toCharArray());
+            return getPasswords(value.toCharArray(), additionalPwdEncodings);
         } else {
             throw new IOException("Unsupported password spec for " + description + ": " + spec);
         }
@@ -170,9 +196,9 @@
      * Returns the provided password and all password variants derived from the password. The
      * resulting list is guaranteed to contain at least one element.
      */
-    private static List<char[]> getPasswords(char[] pwd) {
+    private List<char[]> getPasswords(char[] pwd, Charset... additionalEncodings) {
         List<char[]> passwords = new ArrayList<>(3);
-        addPasswords(passwords, pwd);
+        addPasswords(passwords, pwd, additionalEncodings);
         return passwords;
     }
 
@@ -180,19 +206,18 @@
      * Returns the provided password and all password variants derived from the password. The
      * resulting list is guaranteed to contain at least one element.
      *
-     * @param encodedPwd password encoded using the provided character encoding.
-     * @param encodings character encodings in which the password is encoded in {@code encodedPwd}.
+     * @param encodedPwd password encoded using {@code encodingForDecoding}.
      */
-    private static List<char[]> getPasswords(byte[] encodedPwd, Charset... encodings) {
+    private List<char[]> getPasswords(
+            byte[] encodedPwd, Charset encodingForDecoding,
+            Charset... additionalEncodings) {
         List<char[]> passwords = new ArrayList<>(4);
 
-        for (Charset encoding : encodings) {
-            // Decode password and add it and its variants to the list
-            try {
-                char[] pwd = decodePassword(encodedPwd, encoding);
-                addPasswords(passwords, pwd);
-            } catch (IOException ignored) {}
-        }
+        // Decode password and add it and its variants to the list
+        try {
+            char[] pwd = decodePassword(encodedPwd, encodingForDecoding);
+            addPasswords(passwords, pwd, additionalEncodings);
+        } catch (IOException ignored) {}
 
         // Add the original encoded form
         addPassword(passwords, castBytesToChars(encodedPwd));
@@ -204,23 +229,34 @@
      *
      * <p>NOTE: This method adds only the passwords/variants which are not yet in the list.
      */
-    private static void addPasswords(List<char[]> passwords, char[] pwd) {
+    private void addPasswords(List<char[]> passwords, char[] pwd, Charset... additionalEncodings) {
+        if ((additionalEncodings != null) && (additionalEncodings.length > 0)) {
+            for (Charset encoding : additionalEncodings) {
+                // Password encoded using provided encoding (usually the console's character
+                // encoding) and upcast into char[]
+                try {
+                    char[] encodedPwd = castBytesToChars(encodePassword(pwd, encoding));
+                    addPassword(passwords, encodedPwd);
+                } catch (IOException ignored) {}
+            }
+        }
+
         // Verbatim password
         addPassword(passwords, pwd);
 
+        // Password encoded using the console encoding and upcast into char[]
+        if (mConsoleEncoding != null) {
+            try {
+                char[] encodedPwd = castBytesToChars(encodePassword(pwd, mConsoleEncoding));
+                addPassword(passwords, encodedPwd);
+            } catch (IOException ignored) {}
+        }
+
         // Password encoded using the JVM default character encoding and upcast into char[]
         try {
             char[] encodedPwd = castBytesToChars(encodePassword(pwd, Charset.defaultCharset()));
             addPassword(passwords, encodedPwd);
         } catch (IOException ignored) {}
-
-        // Password encoded using console character encoding and upcast into char[]
-        if (!CONSOLE_CHARSET.equals(Charset.defaultCharset())) {
-            try {
-                char[] encodedPwd = castBytesToChars(encodePassword(pwd, CONSOLE_CHARSET));
-                addPassword(passwords, encodedPwd);
-            } catch (IOException ignored) {}
-        }
     }
 
     /**
@@ -274,44 +310,61 @@
         return chars;
     }
 
+    private static boolean isJava9OrHigherErrOnTheSideOfCaution() {
+        // Before Java 9, this string is of major.minor form, such as "1.8" for Java 8.
+        // From Java 9 onwards, this is a single number: major, such as "9" for Java 9.
+        // See JEP 223: New Version-String Scheme.
+
+        String versionString = System.getProperty("java.specification.version");
+        if (versionString == null) {
+            // Better safe than sorry
+            return true;
+        }
+        return !versionString.startsWith("1.");
+    }
+
     /**
-     * Returns the character encoding used by the console.
+     * Returns the character encoding used by the console or {@code null} if the encoding is not
+     * known.
      */
     private static Charset getConsoleEncoding() {
         // IMPLEMENTATION NOTE: There is no public API for obtaining the console's character
         // encoding. We thus cheat by using implementation details of the most popular JVMs.
-        String consoleCharsetName;
+        // Unfortunately, this doesn't work on Java 9 JVMs where access to Console.encoding is
+        // restricted by default and leads to spewing to stdout at runtime.
+        if (isJava9OrHigherErrOnTheSideOfCaution()) {
+            return null;
+        }
+        String consoleCharsetName = null;
         try {
             Method encodingMethod = Console.class.getDeclaredMethod("encoding");
             encodingMethod.setAccessible(true);
             consoleCharsetName = (String) encodingMethod.invoke(null);
-            if (consoleCharsetName == null) {
-              return Charset.defaultCharset();
-            }
-        } catch (ReflectiveOperationException e) {
-          Charset defaultCharset = Charset.defaultCharset();
-          System.err.println(
-                  "warning: Failed to obtain console character encoding name. Assuming "
-                          + defaultCharset);
-          return defaultCharset;
+        } catch (ReflectiveOperationException ignored) {
+            return null;
+        }
+
+        if (consoleCharsetName == null) {
+            // Console encoding is the same as this JVM's default encoding
+            return Charset.defaultCharset();
         }
 
         try {
-            return Charset.forName(consoleCharsetName);
+            return getCharsetByName(consoleCharsetName);
         } catch (IllegalArgumentException e) {
-            // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't
-            // have a mapping for cp65001...
-            if ("cp65001".equals(consoleCharsetName)) {
-                return StandardCharsets.UTF_8;
-            }
-            Charset defaultCharset = Charset.defaultCharset();
-            System.err.println(
-                    "warning: Console uses unknown character encoding: " + consoleCharsetName
-                            + ". Using " + defaultCharset + " instead");
-            return defaultCharset;
+            return null;
         }
     }
 
+    public static Charset getCharsetByName(String charsetName) throws IllegalArgumentException {
+        // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't
+        // have a mapping for cp65001...
+        if ("cp65001".equalsIgnoreCase(charsetName)) {
+            return StandardCharsets.UTF_8;
+        }
+        return Charset.forName(charsetName);
+    }
+
     private static byte[] readEncodedPassword(InputStream in) throws IOException {
         ByteArrayOutputStream result = new ByteArrayOutputStream();
         int b;
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index 11014e0..80c5fa4 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -108,6 +108,21 @@
                       signer, KeyStore password is read before the key password
                       is read.
 
+--pass-encoding       Additional character encoding (e.g., ibm437 or utf-8) to
+                      try for passwords containing non-ASCII characters.
+                      KeyStores created by keytool are often encrypted not using
+                      the Unicode form of the password but rather using the form
+                      produced by encoding the password using the console's
+                      character encoding. apksigner by default tries to decrypt
+                      using several forms of the password: the Unicode form, the
+                      form encoded using the JVM default charset, and, on Java 8
+                      and older, the form encoded using the console's charset.
+                      On Java 9, apksigner cannot detect the console's charset
+                      and may need to be provided with --pass-encoding when a
+                      non-ASCII password is used. --pass-encoding may also need
+                      to be provided for a KeyStore created by keytool on a
+                      different OS or in a different locale.
+
 --ks-type             Type/algorithm of KeyStore to use. By default, the default
                       type is used.
 
@@ -136,7 +151,7 @@
 
         JCA PROVIDER INSTALLATION OPTIONS
 These options enable you to install additional Java Crypto Architecture (JCA)
-Providers, such PKCS #11 providers. Use --next-provider to delimit options of
+Providers, such as PKCS #11 providers. Use --next-provider to delimit options of
 different providers. Providers are installed in the order in which they appear
 on the command-line.
 
@@ -171,3 +186,12 @@
 5. Sign an APK using PKCS #11 JCA Provider:
 $ apksigner sign --provider-class sun.security.pkcs11.SunPKCS11 \
     --provider-arg token.cfg --ks NONE --ks-type PKCS11 app.apk
+
+6. Sign an APK using a non-ASCII password KeyStore created on English Windows.
+   The --pass-encoding parameter is not needed if apksigner is being run on
+   English Windows with Java 8 or older.
+$ apksigner sign --ks release.jks --pass-encoding ibm437 app.apk
+
+7. Sign an APK on Windows using a non-ASCII password KeyStore created on a
+   modern OSX or Linux machine:
+$ apksigner sign --ks release.jks --pass-encoding utf-8 app.apk
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index bf3997a..a181669 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -877,14 +877,17 @@
          * <ul>
          * <li>Parameter 1: name of the signature block file ({@code String})</li>
          * <li>Parameter 2: digest algorithm OID ({@code String})</li>
-         * <li>Parameter 2: signature algorithm OID ({@code String})</li>
-         * <li>Parameter 3: API Levels on which this combination of algorithms is not supported
+         * <li>Parameter 3: signature algorithm OID ({@code String})</li>
+         * <li>Parameter 4: API Levels on which this combination of algorithms is not supported
          *     ({@code String})</li>
+         * <li>Parameter 5: user-friendly variant of digest algorithm ({@code String})</li>
+         * <li>Parameter 6: user-friendly variant of signature algorithm ({@code String})</li>
          * </ul>
          */
         JAR_SIG_UNSUPPORTED_SIG_ALG(
-                "JAR signature %1$s uses digest algorithm %2$s and signature algorithm %3$s which"
-                        + " is not supported on API Levels %4$s"),
+                "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which"
+                        + " is not supported on API Level(s) %4$s for which this APK is being"
+                        + " verified"),
 
         /**
          * An exception was encountered while parsing JAR signature contained in a signature block.
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index e6cee3b..06b3d38 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -20,21 +20,43 @@
 import com.android.apksig.ApkVerifier.IssueWithParams;
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1DecodingException;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
 import com.android.apksig.internal.jar.ManifestParser;
+import com.android.apksig.internal.pkcs7.Attribute;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+import com.android.apksig.internal.pkcs7.Pkcs7DecodingException;
+import com.android.apksig.internal.pkcs7.SignedData;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.pkcs7.SignerInfo;
 import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.internal.util.InclusiveIntRange;
 import com.android.apksig.internal.util.MessageDigestSink;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.LocalFileRecord;
 import com.android.apksig.util.DataSource;
 import com.android.apksig.zip.ZipFormatException;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.Signature;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -50,8 +72,7 @@
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.jar.Attributes;
-import sun.security.pkcs.PKCS7;
-import sun.security.pkcs.SignerInfo;
+import javax.security.auth.x500.X500Principal;
 
 /**
  * APK verifier which uses JAR signing (aka v1 signing scheme).
@@ -407,10 +428,10 @@
             return mResult;
         }
 
-        @SuppressWarnings("restriction")
         public void verifySigBlockAgainstSigFile(
                 DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
                         throws IOException, ApkFormatException, NoSuchAlgorithmException {
+            // Obtain the signature block from the APK
             byte[] sigBlockBytes;
             try {
                 sigBlockBytes =
@@ -420,6 +441,7 @@
                 throw new ApkFormatException(
                         "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e);
             }
+            // Obtain the signature file from the APK
             try {
                 mSigFileBytes =
                         LocalFileRecord.getUncompressedData(
@@ -428,95 +450,377 @@
                 throw new ApkFormatException(
                         "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e);
             }
-            PKCS7 sigBlock;
+
+            // Extract PKCS #7 SignedData from the signature block
+            SignedData signedData;
             try {
-                sigBlock = new PKCS7(sigBlockBytes);
-            } catch (IOException e) {
-                if (e.getCause() instanceof CertificateException) {
-                    mResult.addError(
-                            Issue.JAR_SIG_MALFORMED_CERTIFICATE, mSignatureBlockEntry.getName(), e);
-                } else {
+                ContentInfo contentInfo =
+                        Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class);
+                if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
+                    throw new Asn1DecodingException(
+                          "Unsupported ContentInfo.contentType: " + contentInfo.contentType);
+                }
+                signedData =
+                        Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
+            } catch (Asn1DecodingException e) {
+                e.printStackTrace();
+                mResult.addError(
+                        Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                return;
+            }
+
+            if (signedData.signerInfos.isEmpty()) {
+                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
+                return;
+            }
+
+            // Find the first SignedData.SignerInfos element which verifies against the signature
+            // file
+            SignerInfo firstVerifiedSignerInfo = null;
+            X509Certificate firstVerifiedSignerInfoSigningCertificate = null;
+            // Prior to Android N, Android attempts to verify only the first SignerInfo. From N
+            // onwards, Android attempts to verify all SignerInfos and then picks the first verified
+            // SignerInfo.
+            List<SignerInfo> unverifiedSignerInfosToTry;
+            if (minSdkVersion < AndroidSdkVersion.N) {
+                unverifiedSignerInfosToTry =
+                        Collections.singletonList(signedData.signerInfos.get(0));
+            } else {
+                unverifiedSignerInfosToTry = signedData.signerInfos;
+            }
+            List<X509Certificate> signedDataCertificates = null;
+            for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) {
+                // Parse SignedData.certificates -- they are needed to verify SignerInfo
+                if (signedDataCertificates == null) {
+                    try {
+                        signedDataCertificates = parseCertificates(signedData.certificates);
+                    } catch (CertificateException e) {
+                        mResult.addError(
+                                Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                        return;
+                    }
+                }
+
+                // Verify SignerInfo
+                X509Certificate signingCertificate;
+                try {
+                    signingCertificate =
+                            verifySignerInfoAgainstSigFile(
+                                    signedData,
+                                    signedDataCertificates,
+                                    unverifiedSignerInfo,
+                                    mSigFileBytes,
+                                    minSdkVersion,
+                                    maxSdkVersion);
+                    if (mResult.containsErrors()) {
+                        return;
+                    }
+                    if (signingCertificate != null) {
+                        // SignerInfo verified
+                        if (firstVerifiedSignerInfo == null) {
+                            firstVerifiedSignerInfo = unverifiedSignerInfo;
+                            firstVerifiedSignerInfoSigningCertificate = signingCertificate;
+                        }
+                    }
+                } catch (Pkcs7DecodingException e) {
                     mResult.addError(
                             Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
+                    return;
+                } catch (InvalidKeyException | SignatureException e) {
+                    mResult.addError(
+                            Issue.JAR_SIG_VERIFY_EXCEPTION,
+                            mSignatureBlockEntry.getName(),
+                            mSignatureFileEntry.getName(),
+                            e);
+                    return;
                 }
+            }
+            if (firstVerifiedSignerInfo == null) {
+                // No SignerInfo verified
+                mResult.addError(
+                        Issue.JAR_SIG_DID_NOT_VERIFY,
+                        mSignatureBlockEntry.getName(),
+                        mSignatureFileEntry.getName());
                 return;
             }
-            SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos();
-            if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) {
-                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
-                return;
+            // Verified
+            List<X509Certificate> signingCertChain =
+                    getCertificateChain(
+                            signedDataCertificates, firstVerifiedSignerInfoSigningCertificate);
+            mResult.certChain.clear();
+            mResult.certChain.addAll(signingCertChain);
+        }
+
+        /**
+         * Returns the signing certificate if the provided {@link SignerInfo} verifies against the
+         * contents of the provided signature file, or {@code null} if it does not verify.
+         */
+        private X509Certificate verifySignerInfoAgainstSigFile(
+                SignedData signedData,
+                Collection<X509Certificate> signedDataCertificates,
+                SignerInfo signerInfo,
+                byte[] signatureFile,
+                int minSdkVersion,
+                int maxSdkVersion)
+                        throws Pkcs7DecodingException, NoSuchAlgorithmException,
+                                InvalidKeyException, SignatureException {
+            String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm;
+            String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm;
+            InclusiveIntRange desiredApiLevels =
+                    InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
+            List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
+                    getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
+            List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
+                    desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
+            if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
+                String digestAlgorithmUserFriendly =
+                        OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                                digestAlgorithmOid);
+                if (digestAlgorithmUserFriendly == null) {
+                    digestAlgorithmUserFriendly = digestAlgorithmOid;
+                }
+                String signatureAlgorithmUserFriendly =
+                        OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+                                signatureAlgorithmOid);
+                if (signatureAlgorithmUserFriendly == null) {
+                    signatureAlgorithmUserFriendly = signatureAlgorithmOid;
+                }
+                StringBuilder apiLevelsUserFriendly = new StringBuilder();
+                for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) {
+                    if (apiLevelsUserFriendly.length() > 0) {
+                        apiLevelsUserFriendly.append(", ");
+                    }
+                    if (range.getMin() == range.getMax()) {
+                        apiLevelsUserFriendly.append(String.valueOf(range.getMin()));
+                    } else if (range.getMax() == Integer.MAX_VALUE) {
+                        apiLevelsUserFriendly.append(range.getMin() + "+");
+                    } else {
+                        apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax());
+                    }
+                }
+                mResult.addError(
+                        Issue.JAR_SIG_UNSUPPORTED_SIG_ALG,
+                        mSignatureBlockEntry.getName(),
+                        digestAlgorithmOid,
+                        signatureAlgorithmOid,
+                        apiLevelsUserFriendly.toString(),
+                        digestAlgorithmUserFriendly,
+                        signatureAlgorithmUserFriendly);
+                return null;
             }
 
-            SignerInfo verifiedSignerInfo = null;
-            if ((unverifiedSignerInfos != null) && (unverifiedSignerInfos.length > 0)) {
-                for (int i = 0; i < unverifiedSignerInfos.length; i++) {
-                    SignerInfo unverifiedSignerInfo = unverifiedSignerInfos[i];
-                    String digestAlgorithmOid =
-                            unverifiedSignerInfo.getDigestAlgorithmId().getOID().toString();
-                    String signatureAlgorithmOid =
-                            unverifiedSignerInfo
-                                    .getDigestEncryptionAlgorithmId().getOID().toString();
-                    InclusiveIntRange desiredApiLevels =
-                            InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
-                    List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
-                            getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
-                    List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
-                            desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
-                    if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
-                        mResult.addError(
-                                Issue.JAR_SIG_UNSUPPORTED_SIG_ALG,
-                                mSignatureBlockEntry.getName(),
-                                digestAlgorithmOid,
-                                signatureAlgorithmOid,
-                                String.valueOf(apiLevelsWhereDigestAlgorithmNotSupported));
-                        return;
+            // From the bag of certs, obtain the certificate referenced by the SignerInfo,
+            // and verify the cryptographic signature in the SignerInfo against the certificate.
+
+            // Locate the signing certificate referenced by the SignerInfo
+            X509Certificate signingCertificate =
+                    findCertificate(signedDataCertificates, signerInfo.sid);
+            if (signingCertificate == null) {
+                throw new SignatureException(
+                        "Signing certificate referenced in SignerInfo not found in"
+                                + " SignedData");
+            }
+
+            // Check whether the signing certificate is acceptable. Android performs these
+            // checks explicitly, instead of delegating this to
+            // Signature.initVerify(Certificate).
+            if (signingCertificate.hasUnsupportedCriticalExtension()) {
+                throw new SignatureException(
+                        "Signing certificate has unsupported critical extensions");
+            }
+            boolean[] keyUsageExtension = signingCertificate.getKeyUsage();
+            if (keyUsageExtension != null) {
+                boolean digitalSignature =
+                        (keyUsageExtension.length >= 1) && (keyUsageExtension[0]);
+                boolean nonRepudiation =
+                        (keyUsageExtension.length >= 2) && (keyUsageExtension[1]);
+                if ((!digitalSignature) && (!nonRepudiation)) {
+                    throw new SignatureException(
+                            "Signing certificate not authorized for use in digital signatures"
+                                    + ": keyUsage extension missing digitalSignature and"
+                                    + " nonRepudiation");
+                }
+            }
+
+            // Verify the cryptographic signature in SignerInfo against the certificate's
+            // public key
+            String jcaSignatureAlgorithm =
+                    getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid);
+            Signature s = Signature.getInstance(jcaSignatureAlgorithm);
+            s.initVerify(signingCertificate.getPublicKey());
+            if (signerInfo.signedAttrs != null) {
+                // Signed attributes present -- verify signature against the ASN.1 DER encoded form
+                // of signed attributes. This verifies integrity of the signature file because
+                // signed attributes must contain the digest of the signature file.
+                if (minSdkVersion < AndroidSdkVersion.KITKAT) {
+                    // Prior to Android KitKat, APKs with signed attributes are unsafe:
+                    // * The APK's contents are not protected by the JAR signature because the
+                    //   digest in signed attributes is not verified. This means an attacker can
+                    //   arbitrarily modify the APK without invalidating its signature.
+                    // * Luckily, the signature over signed attributes was verified incorrectly
+                    //   (over the verbatim IMPLICIT [0] form rather than over re-encoded
+                    //   UNIVERSAL SET form) which means that JAR signatures which would verify on
+                    //   pre-KitKat Android and yet do not protect the APK from modification could
+                    //   be generated only by broken tools or on purpose by the entity signing the
+                    //   APK.
+                    //
+                    // We thus reject such unsafe APKs, even if they verify on platforms before
+                    // KitKat.
+                    throw new SignatureException(
+                            "APKs with Signed Attributes broken on platforms with API Level < "
+                                    + AndroidSdkVersion.KITKAT);
+                }
+                try {
+                    List<Attribute> signedAttributes =
+                            Asn1BerParser.parseImplicitSetOf(
+                                    signerInfo.signedAttrs.getEncoded(), Attribute.class);
+                    SignedAttributes signedAttrs = new SignedAttributes(signedAttributes);
+                    if (maxSdkVersion >= AndroidSdkVersion.N) {
+                        // Content Type attribute is checked only on Android N and newer
+                        String contentType =
+                                signedAttrs.getSingleObjectIdentifierValue(
+                                        Pkcs7Constants.OID_CONTENT_TYPE);
+                        if (contentType == null) {
+                            throw new SignatureException("No Content Type in signed attributes");
+                        }
+                        if (!contentType.equals(signedData.encapContentInfo.contentType)) {
+                            // Did not verify: Content type signed attribute does not match
+                            // SignedData.encapContentInfo.eContentType. This fails verification of
+                            // this SignerInfo but should not prevent verification of other
+                            // SignerInfos. Hence, no exception is thrown.
+                            return null;
+                        }
                     }
-                    try {
-                        verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes);
-                    } catch (SignatureException e) {
-                        mResult.addError(
-                                Issue.JAR_SIG_VERIFY_EXCEPTION,
-                                mSignatureBlockEntry.getName(),
-                                mSignatureFileEntry.getName(),
-                                e);
-                        return;
+                    byte[] expectedSignatureFileDigest =
+                            signedAttrs.getSingleOctetStringValue(
+                                    Pkcs7Constants.OID_MESSAGE_DIGEST);
+                    if (expectedSignatureFileDigest == null) {
+                        throw new SignatureException("No content digest in signed attributes");
                     }
-                    if (verifiedSignerInfo != null) {
-                        // Verified
+                    byte[] actualSignatureFileDigest =
+                            MessageDigest.getInstance(
+                                    getJcaDigestAlgorithm(digestAlgorithmOid))
+                                    .digest(signatureFile);
+                    if (!Arrays.equals(
+                            expectedSignatureFileDigest, actualSignatureFileDigest)) {
+                        // Skip verification: signature file digest in signed attributes does not
+                        // match the signature file. This fails verification of
+                        // this SignerInfo but should not prevent verification of other
+                        // SignerInfos. Hence, no exception is thrown.
+                        return null;
+                    }
+                } catch (Asn1DecodingException e) {
+                    throw new SignatureException("Failed to parse signed attributes", e);
+                }
+                // PKCS #7 requires that signature is over signed attributes re-encoded as
+                // ASN.1 DER. However, Android does not re-encode except for changing the
+                // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the
+                // same for maximum compatibility.
+                ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded();
+                s.update((byte) 0x31); // UNIVERSAL SET
+                signedAttrsOriginalEncoding.position(1);
+                s.update(signedAttrsOriginalEncoding);
+            } else {
+                // No signed attributes present -- verify signature against the contents of the
+                // signature file
+                s.update(signatureFile);
+            }
+            byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice());
+            if (!s.verify(sigBytes)) {
+                // Cryptographic signature did not verify. This fails verification of this
+                // SignerInfo but should not prevent verification of other SignerInfos. Hence, no
+                // exception is thrown.
+                return null;
+            }
+            // Cryptographic signature verified
+            return signingCertificate;
+        }
+
+        private static List<X509Certificate> parseCertificates(
+                List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
+            if (encodedCertificates.isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            CertificateFactory certFactory;
+            try {
+                certFactory = CertificateFactory.getInstance("X.509");
+            } catch (CertificateException e) {
+                throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
+            }
+
+            List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
+            for (int i = 0; i < encodedCertificates.size(); i++) {
+                Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
+                X509Certificate certificate;
+                byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
+                try {
+                    certificate =
+                            (X509Certificate) certFactory.generateCertificate(
+                                    new ByteArrayInputStream(encodedForm));
+                } catch (CertificateException e) {
+                    throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
+                }
+                // Wrap the cert so that the result's getEncoded returns exactly the original
+                // encoded form. Without this, getEncoded may return a different form from what was
+                // stored in the signature. This is because some X509Certificate(Factory)
+                // implementations re-encode certificates and/or some implementations of
+                // X509Certificate.getEncoded() re-encode certificates.
+                certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
+                result.add(certificate);
+            }
+            return result;
+        }
+
+        public static X509Certificate findCertificate(
+                Collection<X509Certificate> certs, SignerIdentifier id) {
+            for (X509Certificate cert : certs) {
+                if (isMatchingCerticicate(cert, id)) {
+                    return cert;
+                }
+            }
+            return null;
+        }
+
+        public static List<X509Certificate> getCertificateChain(
+                List<X509Certificate> certs, X509Certificate leaf) {
+            List<X509Certificate> unusedCerts = new ArrayList<>(certs);
+            List<X509Certificate> result = new ArrayList<>(1);
+            result.add(leaf);
+            unusedCerts.remove(leaf);
+            X509Certificate root = leaf;
+            while (!root.getSubjectDN().equals(root.getIssuerDN())) {
+                Principal targetDn = root.getIssuerDN();
+                boolean issuerFound = false;
+                for (int i = 0; i < unusedCerts.size(); i++) {
+                    X509Certificate unusedCert = unusedCerts.get(i);
+                    if (targetDn.equals(unusedCert.getSubjectDN())) {
+                        issuerFound = true;
+                        unusedCerts.remove(i);
+                        result.add(unusedCert);
+                        root = unusedCert;
                         break;
                     }
-
-                    // Did not verify
-                    if (minSdkVersion < AndroidSdkVersion.N) {
-                        // Prior to N, Android attempted to verify only the first SignerInfo.
-                        mResult.addError(
-                                Issue.JAR_SIG_DID_NOT_VERIFY,
-                                mSignatureBlockEntry.getName(),
-                                mSignatureFileEntry.getName());
-                        return;
-                    }
+                }
+                if (!issuerFound) {
+                    break;
                 }
             }
-            if (verifiedSignerInfo == null) {
-                mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
-                return;
-            }
+            return result;
+        }
 
-            // TODO: PKCS7 class doesn't guarantee that returned certificates' getEncoded returns
-            // the original encoded form of certificates rather than the DER re-encoded form. We
-            // need to replace the PKCS7 parser/verifier.
-            List<X509Certificate> certChain;
-            try {
-                certChain = verifiedSignerInfo.getCertificateChain(sigBlock);
-            } catch (IOException e) {
-                throw new RuntimeException(
-                        "Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e);
+        private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) {
+            if (id.issuerAndSerialNumber == null) {
+                // Android doesn't support any other means of identifying the signing certificate
+                return false;
             }
-            if ((certChain == null) || (certChain.isEmpty())) {
-                throw new RuntimeException("Verified SignerInfo does not have a certificate chain");
-            }
-            mResult.certChain.clear();
-            mResult.certChain.addAll(certChain);
+            IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber;
+            byte[] encodedIssuer =
+                    ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded());
+            X500Principal idIssuer = new X500Principal(encodedIssuer);
+            BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber;
+            return idSerialNumber.equals(cert.getSerialNumber())
+                    && idIssuer.equals(cert.getIssuerX500Principal());
         }
 
         private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
@@ -880,6 +1184,117 @@
             return (result != null) ? result : Collections.emptyList();
         }
 
+        private static class OidToUserFriendlyNameMapper {
+            private OidToUserFriendlyNameMapper() {}
+
+            private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
+            static {
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
+
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
+
+
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
+
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
+                OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
+            }
+
+            public static String getUserFriendlyNameForOid(String oid) {
+                return OID_TO_USER_FRIENDLY_NAME.get(oid);
+            }
+        }
+
+        private static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
+        static {
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
+            OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
+        }
+
+        private static String getJcaDigestAlgorithm(String oid)
+                throws SignatureException {
+            String result = OID_TO_JCA_DIGEST_ALG.get(oid);
+            if (result == null) {
+                throw new SignatureException("Unsupported digest algorithm: " + oid);
+            }
+            return result;
+        }
+
+        private static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
+        static {
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
+
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
+
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
+            OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
+        }
+
+        private static String getJcaSignatureAlgorithm(
+                String digestAlgorithmOid,
+                String signatureAlgorithmOid) throws SignatureException {
+            // First check whether the signature algorithm OID alone is sufficient
+            String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
+            if (result != null) {
+                return result;
+            }
+
+            // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
+            // with signature algorithm OID.
+            String suffix;
+            if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
+                suffix = "RSA";
+            } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
+                suffix = "DSA";
+            } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
+                suffix = "ECDSA";
+            } else {
+                throw new SignatureException(
+                        "Unsupported JCA Signature algorithm"
+                                + " . Digest algorithm: " + digestAlgorithmOid
+                                + ", signature algorithm: " + signatureAlgorithmOid);
+            }
+            String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
+            // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
+            // SHA algorithms.
+            if (jcaDigestAlg.startsWith("SHA-")) {
+                jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
+            }
+            return jcaDigestAlg + "with" + suffix;
+        }
+
         public void verifySigFileAgainstManifest(
                 byte[] manifestBytes,
                 ManifestParser.Section manifestMainSection,
@@ -1593,4 +2008,65 @@
             }
         }
     }
+
+    private static class SignedAttributes {
+        private Map<String, List<Asn1OpaqueObject>> mAttrs;
+
+        public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException {
+            Map<String, List<Asn1OpaqueObject>> result = new HashMap<>(attrs.size());
+            for (Attribute attr : attrs) {
+                if (result.put(attr.attrType, attr.attrValues) != null) {
+                    throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType);
+                }
+            }
+            mAttrs = result;
+        }
+
+        private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException {
+            List<Asn1OpaqueObject> values = mAttrs.get(attrOid);
+            if ((values == null) || (values.isEmpty())) {
+                return null;
+            }
+            if (values.size() > 1) {
+                throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values");
+            }
+            return values.get(0);
+        }
+
+        public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException {
+            Asn1OpaqueObject value = getSingleValue(attrOid);
+            if (value == null) {
+                return null;
+            }
+            try {
+                return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value;
+            } catch (Asn1DecodingException e) {
+                throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
+            }
+        }
+
+        public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException {
+            Asn1OpaqueObject value = getSingleValue(attrOid);
+            if (value == null) {
+                return null;
+            }
+            try {
+                return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value;
+            } catch (Asn1DecodingException e) {
+                throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
+            }
+        }
+    }
+
+    @Asn1Class(type = Asn1Type.CHOICE)
+    public static class OctetStringChoice {
+        @Asn1Field(type = Asn1Type.OCTET_STRING)
+        public byte[] value;
+    }
+
+    @Asn1Class(type = Asn1Type.CHOICE)
+    public static class ObjectIdentifierChoice {
+        @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER)
+        public String value;
+    }
 }
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index d03b35b..9fd5ea0 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -21,7 +21,7 @@
 import com.android.apksig.apk.ApkFormatException;
 import com.android.apksig.apk.ApkUtils;
 import com.android.apksig.internal.util.ByteBufferDataSource;
-import com.android.apksig.internal.util.DelegatingX509Certificate;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
 import com.android.apksig.internal.util.Pair;
 import com.android.apksig.internal.zip.ZipUtils;
 import com.android.apksig.util.DataSource;
@@ -38,7 +38,6 @@
 import java.security.PublicKey;
 import java.security.Signature;
 import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
@@ -295,8 +294,8 @@
             }
             // Wrap the cert so that the result's getEncoded returns exactly the original encoded
             // form. Without this, getEncoded may return a different form from what was stored in
-            // the signature. This is becase some X509Certificate(Factory) implementations re-encode
-            // certificates.
+            // the signature. This is because some X509Certificate(Factory) implementations
+            // re-encode certificates.
             certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
             result.certs.add(certificate);
         }
@@ -798,24 +797,6 @@
         return result;
     }
 
-    /**
-     * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
-     * time.
-     */
-    private static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
-        private byte[] mEncodedForm;
-
-        public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
-            super(wrapped);
-            this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
-        }
-
-        @Override
-        public byte[] getEncoded() throws CertificateEncodingException {
-            return (mEncodedForm != null) ? mEncodedForm.clone() : null;
-        }
-    }
-
     private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
 
     private static String toHex(byte[] value) {
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
index 6b885d5..33b396c 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -71,6 +71,42 @@
         return parse(containerDataValue, containerClass);
     }
 
+    /**
+     * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
+     * that this method does not care whether the tag number of this data structure is
+     * {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
+     *
+     * <p>Note: The returned type is {@link List} rather than {@link Set} because ASN.1 SET may
+     * contain duplicate elements.
+     *
+     * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
+     *        is advanced to the first position following the end of the consumed structure.
+     * @param elementClass class describing the structure of the values/elements contained in this
+     *        container. The class must meet the following requirements:
+     *        <ul>
+     *        <li>The class must be annotated with {@link Asn1Class}.</li>
+     *        <li>The class must expose a public no-arg constructor.</li>
+     *        <li>Member fields of the class which are populated with parsed input must be
+     *            annotated with {@link Asn1Field} and be public and non-final.</li>
+     *        </ul>
+     *
+     * @throws Asn1DecodingException if the input could not be decoded into the specified Java
+     *         object
+     */
+    public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
+            throws Asn1DecodingException {
+        BerDataValue containerDataValue;
+        try {
+            containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
+        } catch (BerDataValueFormatException e) {
+            throw new Asn1DecodingException("Failed to decode top-level data value", e);
+        }
+        if (containerDataValue == null) {
+            throw new Asn1DecodingException("Empty input");
+        }
+        return parseSetOf(containerDataValue, elementClass);
+    }
+
     private static <T> T parse(BerDataValue container, Class<T> containerClass)
             throws Asn1DecodingException {
         if (container == null) {
@@ -517,8 +553,12 @@
                 switch (type) {
                     case SET_OF:
                     case SEQUENCE_OF:
-                        field.set(obj, parseSetOf(dataValue, getElementType(field)));
-                        break;
+                        if (Asn1OpaqueObject.class.equals(field.getType())) {
+                            field.set(obj, convert(type, dataValue, field.getType()));
+                        } else {
+                            field.set(obj, parseSetOf(dataValue, getElementType(field)));
+                        }
+                        return;
                     default:
                         field.set(obj, convert(type, dataValue, field.getType()));
                         break;
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java b/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java
index 031886b..1a115d5 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java
@@ -24,4 +24,6 @@
 
     public static final String OID_DATA = "1.2.840.113549.1.7.1";
     public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
+    public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
+    public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
 }
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java b/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java
new file mode 100644
index 0000000..4004ee7
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+/**
+ * Indicates that an error was encountered while decoding a PKCS #7 structure.
+ */
+public class Pkcs7DecodingException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public Pkcs7DecodingException(String message) {
+        super(message);
+    }
+
+    public Pkcs7DecodingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java b/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java
index 536227a..b885eb8 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java
@@ -18,6 +18,7 @@
 
 import com.android.apksig.internal.asn1.Asn1Class;
 import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
 import com.android.apksig.internal.asn1.Asn1Type;
 import com.android.apksig.internal.asn1.Asn1Tagging;
 import java.nio.ByteBuffer;
@@ -43,7 +44,7 @@
             type = Asn1Type.SET_OF,
             tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
             optional = true)
-    public List<Attribute> signedAttrs;
+    public Asn1OpaqueObject signedAttrs;
 
     @Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
     public AlgorithmIdentifier signatureAlgorithm;
diff --git a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
index 83d1334..e3e605f 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -30,6 +30,9 @@
     /** Android 4.3. The revenge of the beans. */
     public static final int JELLY_BEAN_MR2 = 18;
 
+    /** Android 4.4. KitKat, another tasty treat. */
+    public static final int KITKAT = 19;
+
     /** Android 5.0. A flat one with beautiful shadows. But still tasty. */
     public static final int LOLLIPOP = 21;
 
diff --git a/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java b/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java
new file mode 100644
index 0000000..8bbda13
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/**
+ * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
+ * time.
+ */
+public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
+    private final byte[] mEncodedForm;
+
+    public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
+        super(wrapped);
+        this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
+    }
+
+    @Override
+    public byte[] getEncoded() throws CertificateEncodingException {
+        return (mEncodedForm != null) ? mEncodedForm.clone() : null;
+    }
+}
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 05ffafb..9daf1dc 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -66,8 +66,6 @@
 
     @Test
     public void testV1OneSignerMD5withRSAAccepted() throws Exception {
-        assumeThatMd5AcceptedInPkcs7Signature();
-
         // APK signed with v1 scheme only, one signer
         assertVerifiedForEach(
                 "v1-only-with-rsa-pkcs1-md5-1.2.840.113549.1.1.1-%s.apk", RSA_KEY_NAMES);
@@ -716,7 +714,17 @@
 
     @Test
     public void testV1SignedAttrs() throws Exception {
-        assertVerified(verify("v1-only-with-signed-attrs.apk"));
+        String apk = "v1-only-with-signed-attrs.apk";
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.JELLY_BEAN_MR2),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.KITKAT));
+
+        apk = "v1-only-with-signed-attrs-signerInfo1-good-signerInfo2-good.apk";
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.JELLY_BEAN_MR2),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.KITKAT));
     }
 
     @Test
@@ -726,43 +734,116 @@
         // treats them as SET OF, but does not re-encode into SET OF during verification if all
         // attributes parsed fine.
         assertVerified(verify("v1-only-with-signed-attrs-wrong-order.apk"));
+        assertVerified(
+                verify("v1-only-with-signed-attrs-signerInfo1-wrong-order-signerInfo2-good.apk"));
     }
 
     @Test
     public void testV1SignedAttrsMissingContentType() throws Exception {
-        // SignedAttributes must contain ContentType
-        assertVerificationFailure(
-                verify("v1-only-with-signed-attrs-missing-content-type.apk"),
-                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        // SignedAttributes must contain ContentType. Pre-N, Android ignores this requirement.
+        // Android N onwards rejects such APKs.
+        String apk = "v1-only-with-signed-attrs-missing-content-type.apk";
+        assertVerified(verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1));
+        assertVerificationFailure(verify(apk), Issue.JAR_SIG_VERIFY_EXCEPTION);
+        // Assert that this issue fails verification of the entire signature block, rather than
+        // skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
+        // verification does not get there.
+        apk = "v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apk";
+        assertVerified(verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1));
+        assertVerificationFailure(verify(apk), Issue.JAR_SIG_VERIFY_EXCEPTION);
     }
 
     @Test
     public void testV1SignedAttrsWrongContentType() throws Exception {
+        // ContentType of SignedAttributes must equal SignedData.encapContentInfo.eContentType.
+        // Pre-N, Android ignores this requirement.
+        // From N onwards, Android rejects such SignerInfos.
+        String apk = "v1-only-with-signed-attrs-wrong-content-type.apk";
+        assertVerified(verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1));
+        assertVerificationFailure(verify(apk), Issue.JAR_SIG_DID_NOT_VERIFY);
+        // First SignerInfo does not verify on Android N and newer, but verification moves on to the
+        // second SignerInfo, which verifies.
+        apk = "v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apk";
+        assertVerified(verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1));
+        assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
+        // Although the APK's signature verifies on pre-N and N+, we reject such APKs because the
+        // APK's verification results in different verified SignerInfos (and thus potentially
+        // different signing certs) between pre-N and N+.
+        assertVerificationFailure(verify(apk), Issue.JAR_SIG_DID_NOT_VERIFY);
+    }
+
+    @Test
+    public void testV1SignedAttrsMissingDigest() throws Exception {
+        // Content digest must be present in SignedAttributes
+        String apk = "v1-only-with-signed-attrs-missing-digest.apk";
         assertVerificationFailure(
-                verify("v1-only-with-signed-attrs-wrong-content-type.apk"),
-                Issue.JAR_SIG_DID_NOT_VERIFY);
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        // Assert that this issue fails verification of the entire signature block, rather than
+        // skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
+        // verification does not get there.
+        apk = "v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apk";
+        assertVerificationFailure(
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
+                Issue.JAR_SIG_VERIFY_EXCEPTION);
     }
 
     @Test
     public void testV1SignedAttrsMultipleGoodDigests() throws Exception {
-        // Only one digest must be present SignedAttributes
+        // Only one content digest must be present in SignedAttributes
+        String apk = "v1-only-with-signed-attrs-multiple-good-digests.apk";
         assertVerificationFailure(
-                verify("v1-only-with-signed-attrs-multiple-good-digests.apk"),
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
                 Issue.JAR_SIG_PARSE_EXCEPTION);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_PARSE_EXCEPTION);
+        // Assert that this issue fails verification of the entire signature block, rather than
+        // skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
+        // verification does not get there.
+        apk = "v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apk";
+        assertVerificationFailure(
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
+                Issue.JAR_SIG_PARSE_EXCEPTION);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_PARSE_EXCEPTION);
     }
 
     @Test
     public void testV1SignedAttrsWrongDigest() throws Exception {
+        // Content digest in SignedAttributes does not match the contents
+        String apk = "v1-only-with-signed-attrs-wrong-digest.apk";
         assertVerificationFailure(
-                verify("v1-only-with-signed-attrs-wrong-digest.apk"),
-                Issue.JAR_SIG_DID_NOT_VERIFY);
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1), Issue.JAR_SIG_DID_NOT_VERIFY);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_DID_NOT_VERIFY);
+        // First SignerInfo does not verify, but Android N and newer moves on to the second
+        // SignerInfo, which verifies.
+        apk = "v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apk";
+        assertVerificationFailure(
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1), Issue.JAR_SIG_DID_NOT_VERIFY);
+        assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
     }
 
     @Test
     public void testV1SignedAttrsWrongSignature() throws Exception {
+        // Signature over SignedAttributes does not verify
+        String apk = "v1-only-with-signed-attrs-wrong-signature.apk";
         assertVerificationFailure(
-                verify("v1-only-with-signed-attrs-wrong-signature.apk"),
-                Issue.JAR_SIG_DID_NOT_VERIFY);
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1), Issue.JAR_SIG_DID_NOT_VERIFY);
+        assertVerificationFailure(
+                verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_DID_NOT_VERIFY);
+        // First SignerInfo does not verify, but Android N and newer moves on to the second
+        // SignerInfo, which verifies.
+        apk = "v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apk";
+        assertVerificationFailure(
+                verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1), Issue.JAR_SIG_DID_NOT_VERIFY);
+        assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
     }
 
     private ApkVerifier.Result verify(String apkFilenameInResources)
@@ -801,6 +882,10 @@
     }
 
     static void assertVerified(ApkVerifier.Result result) {
+        assertVerified(result, "APK");
+    }
+
+    static void assertVerified(ApkVerifier.Result result, String apkId) {
         if (result.isVerified()) {
             return;
         }
@@ -818,7 +903,8 @@
                 if (msg.length() > 0) {
                     msg.append('\n');
                 }
-                msg.append("JAR signer ").append(signerName).append(": ").append(issue);
+                msg.append("JAR signer ").append(signerName).append(": ")
+                        .append(issue.getIssue()).append(": ").append(issue);
             }
         }
         for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -828,11 +914,12 @@
                     msg.append('\n');
                 }
                 msg.append("APK Signature Scheme v2 signer ")
-                        .append(signerName).append(": ").append(issue);
+                        .append(signerName).append(": ")
+                        .append(issue.getIssue()).append(": ").append(issue);
             }
         }
 
-        fail("APK did not verify: " + msg);
+        fail(apkId + " did not verify: " + msg);
     }
 
     private void assertVerified(
@@ -840,7 +927,8 @@
             Integer minSdkVersionOverride,
             Integer maxSdkVersionOverride) throws Exception {
         assertVerified(
-                verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride));
+                verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride),
+                apkFilenameInResources);
     }
 
     static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
@@ -887,7 +975,7 @@
         }
 
         fail("APK failed verification for the wrong reason"
-                + " . Expected: " + expectedIssue + ", actual: " + msg);
+                + ". Expected: " + expectedIssue + ", actual: " + msg);
     }
 
     private void assertVerificationFailure(
@@ -930,13 +1018,4 @@
     private static void assumeThatRsaPssAvailable() throws Exception {
         Assume.assumeTrue(Security.getProviders("Signature.SHA256withRSA/PSS") != null);
     }
-
-    private static void assumeThatMd5AcceptedInPkcs7Signature() throws Exception {
-        String algs = Security.getProperty("jdk.jar.disabledAlgorithms");
-        if ((algs != null) && (algs.toLowerCase(Locale.US).contains("md5"))) {
-            Assume.assumeNoException(
-                    new RuntimeException("MD5 not accepted in PKCS #7 signatures"
-                            + " . jdk.jar.disabledAlgorithms: \"" + algs + "\""));
-        }
-    }
 }
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-missing-digest.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-missing-digest.apk
new file mode 100644
index 0000000..bdab7a9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-missing-digest.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-good-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-good-signerInfo2-good.apk
new file mode 100644
index 0000000..df34bcc
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-good-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apk
new file mode 100644
index 0000000..b237742
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apk
new file mode 100644
index 0000000..bebbe83
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apk
new file mode 100644
index 0000000..d49f337
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apk
new file mode 100644
index 0000000..bc3364d
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apk
new file mode 100644
index 0000000..c684723
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-order-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-order-signerInfo2-good.apk
new file mode 100644
index 0000000..a6c237e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-order-signerInfo2-good.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apk b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apk
new file mode 100644
index 0000000..2cb95dd
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apk
Binary files differ