diff options
23 files changed, 952 insertions, 208 deletions
@@ -21,20 +21,6 @@ include $(CLEAR_VARS) 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.InputStream; 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 @@ import javax.crypto.spec.PBEKeySpec; */ 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 @@ public class ApkSignerTool { 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 @@ public class ApkSignerTool { String keystoreKeyAlias; String keystorePasswordSpec; String keyPasswordSpec; + Charset passwordCharset; String keystoreType; String keystoreProviderName; String keystoreProviderClass; @@ -623,6 +635,7 @@ public class ApkSignerTool { && (keystoreKeyAlias == null) && (keystorePasswordSpec == null) && (keyPasswordSpec == null) + && (passwordCharset == null) && (keystoreType == null) && (keystoreProviderName == null) && (keystoreProviderClass == null) @@ -690,13 +703,19 @@ public class ApkSignerTool { // 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 @@ public class ApkSignerTool { 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 @@ public class ApkSignerTool { 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 @@ public class ApkSignerTool { // 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 @@ import java.util.Map; * 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 @@ class PasswordRetriever implements AutoCloseable { * * <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 @@ class PasswordRetriever implements AutoCloseable { // 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 @@ class PasswordRetriever implements AutoCloseable { // 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 @@ class PasswordRetriever implements AutoCloseable { 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 @@ class PasswordRetriever implements AutoCloseable { "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 @@ class PasswordRetriever implements AutoCloseable { } // 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 @@ class PasswordRetriever implements AutoCloseable { "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 @@ class PasswordRetriever implements AutoCloseable { * 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 @@ class PasswordRetriever implements AutoCloseable { * 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 @@ class PasswordRetriever implements AutoCloseable { * * <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,42 +310,59 @@ class PasswordRetriever implements AutoCloseable { 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 { 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 @@ file in X.509 format (see --key and --cert). 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 @@ file in X.509 format (see --key and --cert). 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 @@ $ apksigner sign --ks release.jks --next-signer --ks magic.jks app.apk 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 @@ public class ApkVerifier { * <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.Issue; 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.Map; 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 @@ public abstract class V1SchemeVerifier { 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 @@ public abstract class V1SchemeVerifier { throw new ApkFormatException( "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e); } + // Obtain the signature file from the APK try { mSigFileBytes = LocalFileRecord.getUncompressedData( @@ -428,95 +450,377 @@ public abstract class V1SchemeVerifier { 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 { - mResult.addError( - Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + 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; } - SignerInfo[] unverifiedSignerInfos = sigBlock.getSignerInfos(); - if ((unverifiedSignerInfos == null) || (unverifiedSignerInfos.length == 0)) { + + if (signedData.signerInfos.isEmpty()) { mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); return; } - 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; - } + // 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 { - verifiedSignerInfo = sigBlock.verify(unverifiedSignerInfo, mSigFileBytes); - } catch (SignatureException e) { + signedDataCertificates = parseCertificates(signedData.certificates); + } catch (CertificateException e) { mResult.addError( - Issue.JAR_SIG_VERIFY_EXCEPTION, - mSignatureBlockEntry.getName(), - mSignatureFileEntry.getName(), - e); + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); return; } - if (verifiedSignerInfo != null) { - // Verified - 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()); + // 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 (verifiedSignerInfo == null) { - mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); + if (firstVerifiedSignerInfo == null) { + // No SignerInfo verified + mResult.addError( + Issue.JAR_SIG_DID_NOT_VERIFY, + mSignatureBlockEntry.getName(), + mSignatureFileEntry.getName()); return; } + // Verified + List<X509Certificate> signingCertChain = + getCertificateChain( + signedDataCertificates, firstVerifiedSignerInfoSigningCertificate); + mResult.certChain.clear(); + mResult.certChain.addAll(signingCertChain); + } - // 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; + /** + * 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; + } + + // 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; + } + } + byte[] expectedSignatureFileDigest = + signedAttrs.getSingleOctetStringValue( + Pkcs7Constants.OID_MESSAGE_DIGEST); + if (expectedSignatureFileDigest == null) { + throw new SignatureException("No content digest in signed attributes"); + } + 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 { - certChain = verifiedSignerInfo.getCertificateChain(sigBlock); - } catch (IOException e) { - throw new RuntimeException( - "Failed to obtain cert chain from " + mSignatureBlockEntry.getName(), e); + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to create X.509 CertificateFactory", e); } - if ((certChain == null) || (certChain.isEmpty())) { - throw new RuntimeException("Verified SignerInfo does not have a certificate chain"); + + 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); } - mResult.certChain.clear(); - mResult.certChain.addAll(certChain); + 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; + } + } + if (!issuerFound) { + break; + } + } + return result; + } + + 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; + } + 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 @@ public abstract class V1SchemeVerifier { 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 @@ public abstract class V1SchemeVerifier { } } } + + 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.ApkVerifier.IssueWithParams; 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.NoSuchAlgorithmException; 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 @@ public abstract class V2SchemeVerifier { } // 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 @@ public abstract class V2SchemeVerifier { 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 @@ public final class Asn1BerParser { 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 @@ public final class Asn1BerParser { 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 abstract class Pkcs7Constants { 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 @@ package com.android.apksig.internal.pkcs7; 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 @@ public class SignerInfo { 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 @@ public abstract class AndroidSdkVersion { /** 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 @@ public class ApkVerifierTest { @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 @@ public class ApkVerifierTest { @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 @@ public class ApkVerifierTest { // 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( + 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( - 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); } @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( + 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( - 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); + assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N)); } private ApkVerifier.Result verify(String apkFilenameInResources) @@ -801,6 +882,10 @@ public class ApkVerifierTest { } 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 @@ public class ApkVerifierTest { 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 @@ public class ApkVerifierTest { 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 @@ public class ApkVerifierTest { 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 @@ public class ApkVerifierTest { } fail("APK failed verification for the wrong reason" - + " . Expected: " + expectedIssue + ", actual: " + msg); + + ". Expected: " + expectedIssue + ", actual: " + msg); } private void assertVerificationFailure( @@ -930,13 +1018,4 @@ public class ApkVerifierTest { 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 Binary files differnew file mode 100644 index 0000000..bdab7a9 --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-only-with-signed-attrs-missing-digest.apk 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 |