aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2017-08-15 07:30:02 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2017-08-15 07:30:02 +0000
commit1c40dc60b266ed2f4cc4247128d8feb117ad92c7 (patch)
treefe037f5c4cf893cad024534fb6d3e66c9935daa0
parente44acc01cbaa35bd615aa32559afaa28f360dc99 (diff)
parent83b652ccb6f8bf8465aa0154e43db6b0976b766e (diff)
downloadapksig-1c40dc60b266ed2f4cc4247128d8feb117ad92c7.tar.gz
release-request-e73a0a41-91c3-4249-808e-8d196d54a344-for-git_oc-mr1-release-4273744 snap-temp-L04700000093069831android-wear-8.1.0_r1android-vts-8.1_r9android-vts-8.1_r8android-vts-8.1_r7android-vts-8.1_r6android-vts-8.1_r5android-vts-8.1_r4android-vts-8.1_r3android-vts-8.1_r14android-vts-8.1_r13android-vts-8.1_r12android-vts-8.1_r11android-vts-8.1_r10android-security-8.1.0_r93android-security-8.1.0_r92android-security-8.1.0_r91android-security-8.1.0_r90android-security-8.1.0_r89android-security-8.1.0_r88android-security-8.1.0_r87android-security-8.1.0_r86android-security-8.1.0_r85android-security-8.1.0_r84android-security-8.1.0_r83android-security-8.1.0_r82android-cts-8.1_r9android-cts-8.1_r8android-cts-8.1_r7android-cts-8.1_r6android-cts-8.1_r5android-cts-8.1_r4android-cts-8.1_r3android-cts-8.1_r25android-cts-8.1_r24android-cts-8.1_r23android-cts-8.1_r22android-cts-8.1_r21android-cts-8.1_r20android-cts-8.1_r2android-cts-8.1_r19android-cts-8.1_r18android-cts-8.1_r17android-cts-8.1_r16android-cts-8.1_r15android-cts-8.1_r14android-cts-8.1_r13android-cts-8.1_r12android-cts-8.1_r11android-cts-8.1_r10android-cts-8.1_r1android-8.1.0_r9android-8.1.0_r81android-8.1.0_r80android-8.1.0_r8android-8.1.0_r79android-8.1.0_r78android-8.1.0_r77android-8.1.0_r76android-8.1.0_r75android-8.1.0_r74android-8.1.0_r73android-8.1.0_r72android-8.1.0_r71android-8.1.0_r70android-8.1.0_r7android-8.1.0_r69android-8.1.0_r68android-8.1.0_r67android-8.1.0_r66android-8.1.0_r65android-8.1.0_r64android-8.1.0_r63android-8.1.0_r62android-8.1.0_r61android-8.1.0_r60android-8.1.0_r6android-8.1.0_r53android-8.1.0_r52android-8.1.0_r51android-8.1.0_r50android-8.1.0_r5android-8.1.0_r48android-8.1.0_r47android-8.1.0_r46android-8.1.0_r45android-8.1.0_r43android-8.1.0_r42android-8.1.0_r41android-8.1.0_r40android-8.1.0_r4android-8.1.0_r39android-8.1.0_r38android-8.1.0_r37android-8.1.0_r36android-8.1.0_r35android-8.1.0_r33android-8.1.0_r32android-8.1.0_r31android-8.1.0_r30android-8.1.0_r3android-8.1.0_r29android-8.1.0_r28android-8.1.0_r27android-8.1.0_r26android-8.1.0_r25android-8.1.0_r23android-8.1.0_r22android-8.1.0_r21android-8.1.0_r20android-8.1.0_r2android-8.1.0_r19android-8.1.0_r18android-8.1.0_r17android-8.1.0_r16android-8.1.0_r15android-8.1.0_r14android-8.1.0_r13android-8.1.0_r12android-8.1.0_r11android-8.1.0_r10android-8.1.0_r1security-oc-mr1-releaseoreo-mr1-wear-releaseoreo-mr1-vts-releaseoreo-mr1-security-releaseoreo-mr1-s1-releaseoreo-mr1-releaseoreo-mr1-cuttlefish-testingoreo-mr1-cts-releaseoreo-m8-releaseoreo-m7-releaseoreo-m6-s4-releaseoreo-m6-s3-releaseoreo-m6-s2-releaseoreo-m5-releaseoreo-m4-s9-releaseoreo-m4-s8-releaseoreo-m4-s7-releaseoreo-m4-s6-releaseoreo-m4-s5-releaseoreo-m4-s4-releaseoreo-m4-s3-releaseoreo-m4-s2-releaseoreo-m4-s12-releaseoreo-m4-s11-releaseoreo-m4-s10-releaseoreo-m4-s1-releaseoreo-m3-releaseoreo-m2-s5-releaseoreo-m2-s4-releaseoreo-m2-s3-releaseoreo-m2-s2-releaseoreo-m2-s1-releaseoreo-m2-release
Change-Id: I7d2e75022566c927f73e9b61fb370578f27325b8
-rw-r--r--Android.mk14
-rw-r--r--src/apksigner/java/com/android/apksigner/ApkSignerTool.java36
-rw-r--r--src/apksigner/java/com/android/apksigner/PasswordRetriever.java175
-rw-r--r--src/apksigner/java/com/android/apksigner/help_sign.txt26
-rw-r--r--src/main/java/com/android/apksig/ApkVerifier.java11
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java614
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java25
-rw-r--r--src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java44
-rw-r--r--src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java2
-rw-r--r--src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java32
-rw-r--r--src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java3
-rw-r--r--src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java3
-rw-r--r--src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java38
-rw-r--r--src/test/java/com/android/apksig/ApkVerifierTest.java137
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-missing-digest.apkbin0 -> 4660 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-good-signerInfo2-good.apkbin0 -> 4981 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apkbin0 -> 4973 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apkbin0 -> 4939 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apkbin0 -> 4982 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apkbin0 -> 4980 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apkbin0 -> 4982 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-order-signerInfo2-good.apkbin0 -> 4979 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apkbin0 -> 4977 bytes
23 files changed, 952 insertions, 208 deletions
diff --git a/Android.mk b/Android.mk
index f78e412..bc33f01 100644
--- a/Android.mk
+++ b/Android.mk
@@ -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
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