Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)
Bug: 166295507
Merged-In: Idf007fabfbfce78f639cfd848cdf895142e7b1c4
Change-Id: I9da5e9139f067ac718757b8090ca22591cf2c3d0
diff --git a/Android.bp b/Android.bp
index 821148a..ec57fb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -18,7 +18,9 @@
// ============================================================
java_library_host {
name: "apksig",
- srcs: ["src/main/java/**/*.java"],
+ srcs: [
+ "src/main/java/**/*.java",
+ ],
java_version: "1.8",
}
@@ -30,6 +32,10 @@
java_resource_dirs: ["src/apksigner/java"],
wrapper: "etc/apksigner",
manifest: "src/apksigner/apksigner.mf",
- static_libs: ["apksig"],
+ static_libs: [
+ "apksig",
+ "conscrypt-unbundled",
+ ],
+ required: ["libconscrypt_openjdk_jni"],
java_version: "1.8",
}
diff --git a/apksig.iml b/apksig.iml
deleted file mode 100644
index e0c31f4..0000000
--- a/apksig.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
- <component name="NewModuleRootManager" inherit-compiler-output="true">
- <exclude-output />
- <content url="file://$MODULE_DIR$">
- <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
- </content>
- <orderEntry type="sourceFolder" forTests="false" />
- <orderEntry type="inheritedJdk" />
- </component>
-</module>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 2590fbb..12c0d32 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,7 @@
// Generic Gradle project
apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
sourceCompatibility = '1.8'
@@ -8,6 +9,31 @@
jcenter()
}
-dependencies {
- testCompile 'junit:junit:4.12'
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.11'
+ }
}
+
+dependencies {
+ implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
+ testImplementation 'junit:junit:4.13'
+}
+
+protobuf {
+ protoc {
+ artifact = 'com.google.protobuf:protoc:3.8.0'
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option "lite"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/etc/apksigner b/etc/apksigner
index 11a7529..d13afc4 100755
--- a/etc/apksigner
+++ b/etc/apksigner
@@ -46,6 +46,8 @@
if [ ! -r "$libdir/$jarfile" ]; then
# set apksigner.jar location for the Android tree case
libdir=`dirname "$progdir"`/framework
+ # also include the library directory for any provider native libraries
+ providerLibdir=`dirname "$progdir"`/lib64
fi
if [ ! -r "$libdir/$jarfile" ]; then
@@ -71,6 +73,8 @@
javaOpts="${javaOpts} -${opt}"
if expr "x${opt}" : "xXmx[0-9]" >/dev/null; then
defaultMx="no"
+ elif expr "x${opt}" : "xDjava.library.path=" >/dev/null; then
+ defaultLibdir="no"
fi
shift
done
@@ -79,6 +83,10 @@
javaOpts="${javaOpts} ${defaultMx}"
fi
+if [ "${defaultLibdir}" != "no" ] && [ -n $providerLibdir ]; then
+ javaOpts="${javaOpts} -Djava.library.path=$providerLibdir"
+fi
+
if [ "$OSTYPE" = "cygwin" ]; then
# For Cygwin, convert the jarfile path into native Windows style.
jarpath=`cygpath -w "$libdir/$jarfile"`
diff --git a/etc/apksigner.bat b/etc/apksigner.bat
index 29106c1..934725f 100755
--- a/etc/apksigner.bat
+++ b/etc/apksigner.bat
@@ -22,12 +22,34 @@
REM and set up progdir to be the fully-qualified pathname of its directory.
set prog=%~f0
-rem Check we have a valid Java.exe in the path.
-set java_exe=
-if exist "%~dp0..\tools\lib\find_java.bat" call "%~dp0..\tools\lib\find_java.bat"
-if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"
-if not defined java_exe goto :EOF
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+exit /b 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+exit /b 1
+
+:init
set jarfile=apksigner.jar
set "frameworkdir=%~dp0"
rem frameworkdir must not end with a dir sep.
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index affa7f5..2f4e680 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -24,6 +24,9 @@
import com.android.apksig.apk.MinSdkVersionException;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
+
+import org.conscrypt.OpenSSLProvider;
+
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
@@ -77,6 +80,8 @@
return;
}
+ addProviders();
+
String cmd = params[0];
try {
if ("sign".equals(cmd)) {
@@ -108,6 +113,19 @@
}
}
+ /**
+ * Adds additional security providers to add support for signature algorithms not covered by
+ * the default providers.
+ */
+ private static void addProviders() {
+ try {
+ Security.addProvider(new OpenSSLProvider());
+ } catch (UnsatisfiedLinkError e) {
+ // This is expected if the library path does not include the native conscrypt library;
+ // the default providers support all but PSS algorithms.
+ }
+ }
+
private static void sign(String[] params) throws Exception {
if (params.length == 0) {
printUsage(HELP_PAGE_SIGN);
@@ -120,18 +138,24 @@
boolean v1SigningEnabled = true;
boolean v2SigningEnabled = true;
boolean v3SigningEnabled = true;
+ boolean v4SigningEnabled = true;
+ boolean forceSourceStampOverwrite = false;
+ boolean verityEnabled = false;
boolean debuggableApkPermitted = true;
int minSdkVersion = 1;
boolean minSdkVersionSpecified = false;
int maxSdkVersion = Integer.MAX_VALUE;
List<SignerParams> signers = new ArrayList<>(1);
SignerParams signerParams = new SignerParams();
+ SignerParams sourceStampSignerParams = new SignerParams();
SigningCertificateLineage lineage = null;
List<ProviderInstallSpec> providers = new ArrayList<>();
ProviderInstallSpec providerParams = new ProviderInstallSpec();
OptionsParser optionsParser = new OptionsParser(params);
String optionName;
String optionOriginalForm = null;
+ boolean v4SigningFlagFound = false;
+ boolean sourceStampFlagFound = false;
while ((optionName = optionsParser.nextOption()) != null) {
optionOriginalForm = optionsParser.getOptionOriginalForm();
if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -152,6 +176,13 @@
v2SigningEnabled = optionsParser.getOptionalBooleanValue(true);
} else if ("v3-signing-enabled".equals(optionName)) {
v3SigningEnabled = optionsParser.getOptionalBooleanValue(true);
+ } else if ("v4-signing-enabled".equals(optionName)) {
+ v4SigningEnabled = optionsParser.getOptionalBooleanValue(true);
+ v4SigningFlagFound = true;
+ } else if ("force-stamp-overwrite".equals(optionName)) {
+ forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true);
+ } else if ("verity-enabled".equals(optionName)) {
+ verityEnabled = optionsParser.getOptionalBooleanValue(true);
} else if ("debuggable-apk-permitted".equals(optionName)) {
debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true);
} else if ("next-signer".equals(optionName)) {
@@ -218,6 +249,9 @@
} else if ("provider-pos".equals(optionName)) {
providerParams.position =
optionsParser.getRequiredIntValue("JCA Provider position");
+ } else if ("stamp-signer".equals(optionName)) {
+ sourceStampFlagFound = true;
+ sourceStampSignerParams = processSignerParams(optionsParser);
} else {
throw new ParameterException(
"Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -267,49 +301,27 @@
providerInstallSpec.installProvider();
}
+ ApkSigner.SignerConfig sourceStampSignerConfig = null;
List<ApkSigner.SignerConfig> signerConfigs = new ArrayList<>(signers.size());
int signerNumber = 0;
try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
for (SignerParams signer : signers) {
signerNumber++;
signer.setName("signer #" + signerNumber);
- try {
- signer.loadPrivateKeyAndCerts(passwordRetriever);
- } catch (ParameterException e) {
- System.err.println(
- "Failed to load signer \"" + signer.getName() + "\": "
- + e.getMessage());
- System.exit(2);
- return;
- } catch (Exception e) {
- System.err.println("Failed to load signer \"" + signer.getName() + "\"");
- e.printStackTrace();
- System.exit(2);
+ ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever);
+ if (signerConfig == null) {
return;
}
- String v1SigBasename;
- if (signer.getV1SigFileBasename() != null) {
- v1SigBasename = signer.getV1SigFileBasename();
- } else if (signer.getKeystoreKeyAlias() != null) {
- v1SigBasename = signer.getKeystoreKeyAlias();
- } else if (signer.getKeyFile() != null) {
- String keyFileName = new File(signer.getKeyFile()).getName();
- int delimiterIndex = keyFileName.indexOf('.');
- if (delimiterIndex == -1) {
- v1SigBasename = keyFileName;
- } else {
- v1SigBasename = keyFileName.substring(0, delimiterIndex);
- }
- } else {
- throw new RuntimeException(
- "Neither KeyStore key alias nor private key file available");
- }
- ApkSigner.SignerConfig signerConfig =
- new ApkSigner.SignerConfig.Builder(
- v1SigBasename, signer.getPrivateKey(), signer.getCerts())
- .build();
signerConfigs.add(signerConfig);
}
+ if (sourceStampFlagFound) {
+ sourceStampSignerParams.setName("stamp signer");
+ sourceStampSignerConfig =
+ getSignerConfig(sourceStampSignerParams, passwordRetriever);
+ if (sourceStampSignerConfig == null) {
+ return;
+ }
+ }
}
if (outputApk == null) {
@@ -330,11 +342,24 @@
.setV1SigningEnabled(v1SigningEnabled)
.setV2SigningEnabled(v2SigningEnabled)
.setV3SigningEnabled(v3SigningEnabled)
+ .setV4SigningEnabled(v4SigningEnabled)
+ .setForceSourceStampOverwrite(forceSourceStampOverwrite)
+ .setVerityEnabled(verityEnabled)
+ .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound)
.setDebuggableApkPermitted(debuggableApkPermitted)
.setSigningCertificateLineage(lineage);
if (minSdkVersionSpecified) {
apkSignerBuilder.setMinSdkVersion(minSdkVersion);
}
+ if (v4SigningEnabled) {
+ final File outputV4SignatureFile =
+ new File(outputApk.getCanonicalPath() + ".idsig");
+ Files.deleteIfExists(outputV4SignatureFile.toPath());
+ apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile);
+ }
+ if (sourceStampSignerConfig != null) {
+ apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig);
+ }
ApkSigner apkSigner = apkSignerBuilder.build();
try {
apkSigner.sign();
@@ -358,6 +383,44 @@
}
}
+ private static ApkSigner.SignerConfig getSignerConfig(
+ SignerParams signer, PasswordRetriever passwordRetriever) {
+ try {
+ signer.loadPrivateKeyAndCerts(passwordRetriever);
+ } catch (ParameterException e) {
+ System.err.println(
+ "Failed to load signer \"" + signer.getName() + "\": " + e.getMessage());
+ System.exit(2);
+ return null;
+ } catch (Exception e) {
+ System.err.println("Failed to load signer \"" + signer.getName() + "\"");
+ e.printStackTrace();
+ System.exit(2);
+ return null;
+ }
+ String v1SigBasename;
+ if (signer.getV1SigFileBasename() != null) {
+ v1SigBasename = signer.getV1SigFileBasename();
+ } else if (signer.getKeystoreKeyAlias() != null) {
+ v1SigBasename = signer.getKeystoreKeyAlias();
+ } else if (signer.getKeyFile() != null) {
+ String keyFileName = new File(signer.getKeyFile()).getName();
+ int delimiterIndex = keyFileName.indexOf('.');
+ if (delimiterIndex == -1) {
+ v1SigBasename = keyFileName;
+ } else {
+ v1SigBasename = keyFileName.substring(0, delimiterIndex);
+ }
+ } else {
+ throw new RuntimeException("Neither KeyStore key alias nor private key file available");
+ }
+ ApkSigner.SignerConfig signerConfig =
+ new ApkSigner.SignerConfig.Builder(
+ v1SigBasename, signer.getPrivateKey(), signer.getCerts())
+ .build();
+ return signerConfig;
+ }
+
private static void verify(String[] params) throws Exception {
if (params.length == 0) {
printUsage(HELP_PAGE_VERIFY);
@@ -372,6 +435,7 @@
boolean printCerts = false;
boolean verbose = false;
boolean warningsTreatedAsErrors = false;
+ File v4SignatureFile = null;
OptionsParser optionsParser = new OptionsParser(params);
String optionName;
String optionOriginalForm = null;
@@ -392,6 +456,9 @@
} else if (("help".equals(optionName)) || ("h".equals(optionName))) {
printUsage(HELP_PAGE_VERIFY);
return;
+ } else if ("v4-signature-file".equals(optionName)) {
+ v4SignatureFile = new File(optionsParser.getRequiredValue(
+ "Input V4 Signature File"));
} else if ("in".equals(optionName)) {
inputApk = new File(optionsParser.getRequiredValue("Input APK file"));
} else {
@@ -435,6 +502,14 @@
if (maxSdkVersionSpecified) {
apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion);
}
+ if (v4SignatureFile != null) {
+ if (!v4SignatureFile.exists()) {
+ throw new ParameterException("V4 signature file does not exist: "
+ + v4SignatureFile.getCanonicalPath());
+ }
+ apkVerifierBuilder.setV4SignatureFile(v4SignatureFile);
+ }
+
ApkVerifier apkVerifier = apkVerifierBuilder.build();
ApkVerifier.Result result;
try {
@@ -465,6 +540,10 @@
System.out.println(
"Verified using v3 scheme (APK Signature Scheme v3): "
+ result.isVerifiedUsingV3Scheme());
+ System.out.println(
+ "Verified using v4 scheme (APK Signature Scheme v4): "
+ + result.isVerifiedUsingV4Scheme());
+ System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified());
System.out.println("Number of signers: " + signerCerts.size());
}
if (printCerts) {
@@ -523,6 +602,16 @@
}
}
+ ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo();
+ if (sourceStampInfo != null) {
+ for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) {
+ System.err.println("ERROR: SourceStamp: " + error);
+ }
+ for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) {
+ warningsOut.println("WARNING: SourceStamp: " + warning);
+ }
+ }
+
if (!verified) {
System.exit(1);
return;
@@ -678,7 +767,6 @@
File outputKeyLineage = null;
String optionName;
OptionsParser optionsParser = new OptionsParser(params);
- SigningCertificateLineage lineage = null;
List<SignerParams> signers = new ArrayList<>(1);
while ((optionName = optionsParser.nextOption()) != null) {
if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -704,7 +792,7 @@
if (inputKeyLineage == null) {
throw new ParameterException("Input lineage file parameter not present");
}
- lineage = getLineageFromInputFile(inputKeyLineage);
+ SigningCertificateLineage lineage = getLineageFromInputFile(inputKeyLineage);
try (PasswordRetriever passwordRetriever = new PasswordRetriever()) {
for (int i = 0; i < signers.size(); i++) {
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index b9d0146..1285810 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -40,6 +40,19 @@
"lineage" option to make sure that the app is signed by
an appropriate signer on all supported platform versions.
+--v4-signing-enabled Whether to enable signing using APK Signature Scheme v4
+ (aka v4 signing scheme) introduced in Android 11,
+ API Level 30. By default, signing using this scheme is
+ enabled based on min and max SDK version (see
+ --min-sdk-version and --max-sdk-version).
+
+--force-stamp-overwrite Whether to overwrite existing source stamp in the
+ APK, if found. By default, it is set to false. It has no
+ effect if no source stamp signer config is provided.
+
+--verity-enabled Whether to enable the verity signature algorithm for the
+ v2 and v3 signature schemes.
+
--min-sdk-version Lowest API Level on which this APK's signatures will be
verified. By default, the value from AndroidManifest.xml
is used. The higher the value, the stronger security
@@ -91,6 +104,9 @@
(aka v1 scheme) signature of this signer. By default,
KeyStore key alias or basename of key file is used.
+--stamp-signer The signing information for the signer of the source stamp
+ to be included in the APK.
+
PER-SIGNER SIGNING KEY & CERTIFICATE OPTIONS
There are two ways to provide the signer's private key and certificate: (1) Java
KeyStore (see --ks), or (2) private key file in PKCS #8 format and certificate
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index e3870e7..154e917 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -16,6 +16,8 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
@@ -31,9 +33,8 @@
import com.android.apksig.util.DataSources;
import com.android.apksig.util.ReadableDataSink;
import com.android.apksig.zip.ZipFormatException;
-import java.io.ByteArrayOutputStream;
+
import java.io.Closeable;
-import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@@ -45,20 +46,19 @@
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.regex.Pattern;
/**
* APK signer.
*
- * <p>The signer preserves as much of the input APK as possible. For example, it preserves the
- * order of APK entries and preserves their contents, including compressed form and alignment of
- * data.
+ * <p>The signer preserves as much of the input APK as possible. For example, it preserves the order
+ * of APK entries and preserves their contents, including compressed form and alignment of data.
*
* <p>Use {@link Builder} to obtain instances of this signer.
*
@@ -81,16 +81,19 @@
private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
- /**
- * Name of the Android manifest ZIP entry in APKs.
- */
+ /** Name of the Android manifest ZIP entry in APKs. */
private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
private final List<SignerConfig> mSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
+ private final boolean mForceSourceStampOverwrite;
private final Integer mMinSdkVersion;
private final boolean mV1SigningEnabled;
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
+ private final boolean mV4SigningEnabled;
+ private final boolean mVerityEnabled;
+ private final boolean mV4ErrorReportingEnabled;
private final boolean mDebuggableApkPermitted;
private final boolean mOtherSignersSignaturesPreserved;
private final String mCreatedBy;
@@ -104,14 +107,21 @@
private final DataSink mOutputApkDataSink;
private final DataSource mOutputApkDataSource;
+ private final File mOutputV4File;
+
private final SigningCertificateLineage mSigningCertificateLineage;
private ApkSigner(
List<SignerConfig> signerConfigs,
+ SignerConfig sourceStampSignerConfig,
+ boolean forceSourceStampOverwrite,
Integer minSdkVersion,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
+ boolean v4SigningEnabled,
+ boolean verityEnabled,
+ boolean v4ErrorReportingEnabled,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
String createdBy,
@@ -121,13 +131,19 @@
File outputApkFile,
DataSink outputApkDataSink,
DataSource outputApkDataSource,
+ File outputV4File,
SigningCertificateLineage signingCertificateLineage) {
mSignerConfigs = signerConfigs;
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ mForceSourceStampOverwrite = forceSourceStampOverwrite;
mMinSdkVersion = minSdkVersion;
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
+ mV4SigningEnabled = v4SigningEnabled;
+ mVerityEnabled = verityEnabled;
+ mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
mDebuggableApkPermitted = debuggableApkPermitted;
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
mCreatedBy = createdBy;
@@ -141,6 +157,8 @@
mOutputApkDataSink = outputApkDataSink;
mOutputApkDataSource = outputApkDataSource;
+ mOutputV4File = outputV4File;
+
mSigningCertificateLineage = signingCertificateLineage;
}
@@ -150,12 +168,12 @@
* @throws IOException if an I/O error is encountered while reading or writing the APKs
* @throws ApkFormatException if the input APK is malformed
* @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
- * a required cryptographic algorithm implementation is missing
+ * a required cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
- * not suitable for generating the signature
+ * not suitable for generating the signature
* @throws SignatureException if an error occurred while generating or verifying a signature
* @throws IllegalStateException if this signer's configuration is missing required information
- * or if the signing engine is in an invalid state.
+ * or if the signing engine is in an invalid state.
*/
public void sign()
throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
@@ -203,12 +221,9 @@
}
}
- private void sign(
- DataSource inputApk,
- DataSink outputApkOut,
- DataSource outputApkIn)
- throws IOException, ApkFormatException, NoSuchAlgorithmException,
- InvalidKeyException, SignatureException {
+ private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException {
// Step 1. Find input APK's main ZIP sections
ApkUtils.ZipSections inputZipSections;
try {
@@ -240,8 +255,8 @@
List<CentralDirectoryRecord> inputCdRecords =
parseZipCentralDirectory(inputCd, inputZipSections);
- List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(
- inputCdRecords, inputApkLfhSection);
+ List<Hints.PatternWithRange> pinPatterns =
+ extractPinPatterns(inputCdRecords, inputApkLfhSection);
List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
// Step 3. Obtain a signer engine instance
@@ -264,9 +279,9 @@
for (SignerConfig signerConfig : mSignerConfigs) {
engineSignerConfigs.add(
new DefaultApkSignerEngine.SignerConfig.Builder(
- signerConfig.getName(),
- signerConfig.getPrivateKey(),
- signerConfig.getCertificates())
+ signerConfig.getName(),
+ signerConfig.getPrivateKey(),
+ signerConfig.getCertificates())
.build());
}
DefaultApkSignerEngine.Builder signerEngineBuilder =
@@ -274,12 +289,21 @@
.setV1SigningEnabled(mV1SigningEnabled)
.setV2SigningEnabled(mV2SigningEnabled)
.setV3SigningEnabled(mV3SigningEnabled)
+ .setVerityEnabled(mVerityEnabled)
.setDebuggableApkPermitted(mDebuggableApkPermitted)
.setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
.setSigningCertificateLineage(mSigningCertificateLineage);
if (mCreatedBy != null) {
signerEngineBuilder.setCreatedBy(mCreatedBy);
}
+ if (mSourceStampSignerConfig != null) {
+ signerEngineBuilder.setStampSignerConfig(
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ mSourceStampSignerConfig.getName(),
+ mSourceStampSignerConfig.getPrivateKey(),
+ mSourceStampSignerConfig.getCertificates())
+ .build());
+ }
signerEngine = signerEngineBuilder.build();
}
@@ -301,12 +325,23 @@
int lastModifiedTimeForNewEntries = -1;
long inputOffset = 0;
long outputOffset = 0;
+ byte[] sourceStampCertificateDigest = null;
Map<String, CentralDirectoryRecord> outputCdRecordsByName =
new HashMap<>(inputCdRecords.size());
for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
String entryName = inputCdRecord.getName();
if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) {
- continue; // We'll re-add below if needed.
+ continue; // We'll re-add below if needed.
+ }
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
+ try {
+ sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+ } catch (ZipFormatException ex) {
+ throw new ApkFormatException("Bad source stamp entry");
+ }
+ continue; // Existing source stamp is handled below as needed.
}
ApkSignerEngine.InputJarEntryInstructions entryInstructions =
signerEngine.inputJarEntry(entryName);
@@ -397,8 +432,8 @@
}
}
if (pinFileHeader) {
- pinByteRanges.add(new Hints.ByteRange(outputLocalFileHeaderOffset,
- outputDataOffset));
+ pinByteRanges.add(
+ new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset));
}
}
@@ -436,94 +471,94 @@
}
}
- // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File
+ if (lastModifiedDateForNewEntries == -1) {
+ lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+ lastModifiedTimeForNewEntries = 0;
+ }
+
+ // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output
+ // more Local File Header + data entries and add to the list of output Central Directory
+ // records.
+ if (signerEngine.isEligibleForSourceStamp()) {
+ byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
+ if (mForceSourceStampOverwrite
+ || sourceStampCertificateDigest == null
+ || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
+ outputOffset +=
+ outputDataToOutputApk(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ } else {
+ throw new ApkFormatException(
+ String.format(
+ "Cannot generate SourceStamp. APK contains an existing entry with"
+ + " the name: %s, and it is different than the provided source"
+ + " stamp certificate",
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
+ }
+ }
+
+ // Step 8. Generate and output JAR signatures, if necessary. This may output more Local File
// Header + data entries and add to the list of output Central Directory records.
ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
signerEngine.outputJarEntries();
if (outputJarSignatureRequest != null) {
- if (lastModifiedDateForNewEntries == -1) {
- lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
- lastModifiedTimeForNewEntries = 0;
- }
for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
outputJarSignatureRequest.getAdditionalJarEntries()) {
String entryName = entry.getName();
byte[] uncompressedData = entry.getData();
- ZipUtils.DeflateResult deflateResult =
- ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
- byte[] compressedData = deflateResult.output;
- long uncompressedDataCrc32 = deflateResult.inputCrc32;
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
signerEngine.outputJarEntry(entryName);
if (inspectEntryRequest != null) {
- inspectEntryRequest.getDataSink().consume(
- uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest
+ .getDataSink()
+ .consume(uncompressedData, 0, uncompressedData.length);
inspectEntryRequest.done();
}
- long localFileHeaderOffset = outputOffset;
outputOffset +=
- LocalFileRecord.outputRecordWithDeflateCompressedData(
+ outputDataToOutputApk(
entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
lastModifiedTimeForNewEntries,
lastModifiedDateForNewEntries,
- compressedData,
- uncompressedDataCrc32,
- uncompressedData.length,
outputApkOut);
-
-
- outputCdRecords.add(
- CentralDirectoryRecord.createWithDeflateCompressedData(
- entryName,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- uncompressedDataCrc32,
- compressedData.length,
- uncompressedData.length,
- localFileHeaderOffset));
}
outputJarSignatureRequest.done();
}
if (pinByteRanges != null) {
- pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir
+ pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir
String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
- ZipUtils.DeflateResult deflateResult =
- ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
- byte[] compressedData = deflateResult.output;
- long uncompressedDataCrc32 = deflateResult.inputCrc32;
- long localFileHeaderOffset = outputOffset;
outputOffset +=
- LocalFileRecord.outputRecordWithDeflateCompressedData(
- entryName,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- compressedData,
- uncompressedDataCrc32,
- uncompressedData.length,
- outputApkOut);
- outputCdRecords.add(
- CentralDirectoryRecord.createWithDeflateCompressedData(
- entryName,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- uncompressedDataCrc32,
- compressedData.length,
- uncompressedData.length,
- localFileHeaderOffset));
+ outputDataToOutputApk(
+ entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
}
- // Step 8. Construct output ZIP Central Directory in an in-memory buffer
+ // Step 9. Construct output ZIP Central Directory in an in-memory buffer
long outputCentralDirSizeBytes = 0;
for (CentralDirectoryRecord record : outputCdRecords) {
outputCentralDirSizeBytes += record.getSize();
}
if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
throw new IOException(
- "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+ "Output ZIP Central Directory too large: "
+ + outputCentralDirSizeBytes
+ " bytes");
}
ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
@@ -535,7 +570,7 @@
long outputCentralDirStartOffset = outputOffset;
int outputCentralDirRecordCount = outputCdRecords.size();
- // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer
+ // Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer
ByteBuffer outputEocd =
EocdRecord.createWithModifiedCentralDirectoryInfo(
inputZipSections.getZipEndOfCentralDirectory(),
@@ -543,7 +578,8 @@
outputCentralDirDataSource.size(),
outputCentralDirStartOffset);
- // Step 10. Generate and output APK Signature Scheme v2 and/or v3 signatures, if necessary.
+ // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or
+ // SourceStamp signatures, if necessary.
// This may insert an APK Signing Block just before the output's ZIP Central Directory
ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest =
signerEngine.outputZipSections2(
@@ -556,22 +592,61 @@
outputApkOut.consume(ByteBuffer.allocate(padding));
byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
- ZipUtils.setZipEocdCentralDirectoryOffset(outputEocd,
+ ZipUtils.setZipEocdCentralDirectoryOffset(
+ outputEocd,
outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
outputApkSigningBlockRequest.done();
}
- // Step 11. Output ZIP Central Directory and ZIP End of Central Directory
+ // Step 12. Output ZIP Central Directory and ZIP End of Central Directory
outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
outputApkOut.consume(outputEocd);
signerEngine.outputDone();
+
+ // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
+ if (mV4SigningEnabled) {
+ signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled);
+ }
+ }
+
+ private static long outputDataToOutputApk(
+ String entryName,
+ byte[] uncompressedData,
+ long localFileHeaderOffset,
+ List<CentralDirectoryRecord> outputCdRecords,
+ int lastModifiedTimeForNewEntries,
+ int lastModifiedDateForNewEntries,
+ DataSink outputApkOut)
+ throws IOException {
+ ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+ byte[] compressedData = deflateResult.output;
+ long uncompressedDataCrc32 = deflateResult.inputCrc32;
+ long numOfDataBytes =
+ LocalFileRecord.outputRecordWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ compressedData,
+ uncompressedDataCrc32,
+ uncompressedData.length,
+ outputApkOut);
+ outputCdRecords.add(
+ CentralDirectoryRecord.createWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ uncompressedDataCrc32,
+ compressedData.length,
+ uncompressedData.length,
+ localFileHeaderOffset));
+ return numOfDataBytes;
}
private static void fulfillInspectInputJarEntryRequest(
DataSource lfhSection,
LocalFileRecord localFileRecord,
ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
- throws IOException, ApkFormatException {
+ throws IOException, ApkFormatException {
try {
localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
} catch (ZipFormatException e) {
@@ -594,7 +669,8 @@
DataSource inputLfhSection,
LocalFileRecord inputRecord,
DataSink outputLfhSection,
- long outputOffset) throws IOException {
+ long outputOffset)
+ throws IOException {
long inputOffset = inputRecord.getStartOffsetInArchive();
if (inputOffset == outputOffset) {
// This record's data will be aligned same as in the input APK.
@@ -628,11 +704,14 @@
inputRecord.getExtra(),
outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
dataAlignmentMultiple);
- long dataOffset = inputRecord.getDataStartOffsetInRecord() +
- aligningExtra.remaining() -
- inputRecord.getExtra().remaining();
- return new OutputSizeAndDataOffset(inputRecord.outputRecordWithModifiedExtra(
- inputLfhSection, aligningExtra, outputLfhSection), dataOffset);
+ long dataOffset =
+ (long) inputRecord.getDataStartOffsetInRecord()
+ + aligningExtra.remaining()
+ - inputRecord.getExtra().remaining();
+ return new OutputSizeAndDataOffset(
+ inputRecord.outputRecordWithModifiedExtra(
+ inputLfhSection, aligningExtra, outputLfhSection),
+ dataOffset);
}
private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
@@ -650,7 +729,7 @@
// * uint16 size
// * 'size' bytes: payload
while (extra.remaining() >= 4) {
- short headerId = extra.getShort();
+ short headerId = extra.getShort();
int dataSize = ZipUtils.getUnsignedInt16(extra);
if (dataSize > extra.remaining()) {
// Malformed field -- insufficient input remaining
@@ -679,9 +758,7 @@
}
private static ByteBuffer createExtraFieldToAlignData(
- ByteBuffer original,
- long extraStartOffset,
- int dataAlignmentMultiple) {
+ ByteBuffer original, long extraStartOffset, int dataAlignmentMultiple) {
if (dataAlignmentMultiple <= 1) {
return original;
}
@@ -696,7 +773,7 @@
// * uint16 size
// * 'size' bytes: payload
while (original.remaining() >= 4) {
- short headerId = original.getShort();
+ short headerId = original.getShort();
int dataSize = ZipUtils.getUnsignedInt16(original);
if (dataSize > original.remaining()) {
// Malformed field -- insufficient input remaining
@@ -726,7 +803,8 @@
// * remaining bytes -- padding to achieve alignment of data which starts after the
// extra field
long dataMinStartOffset =
- extraStartOffset + result.position()
+ extraStartOffset
+ + result.position()
+ ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
int paddingSizeBytes =
(dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
@@ -741,8 +819,8 @@
}
private static ByteBuffer getZipCentralDirectory(
- DataSource apk,
- ApkUtils.ZipSections apkSections) throws IOException, ApkFormatException {
+ DataSource apk, ApkUtils.ZipSections apkSections)
+ throws IOException, ApkFormatException {
long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
if (cdSizeBytes > Integer.MAX_VALUE) {
throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
@@ -754,8 +832,7 @@
}
private static List<CentralDirectoryRecord> parseZipCentralDirectory(
- ByteBuffer cd,
- ApkUtils.ZipSections apkSections) throws ApkFormatException {
+ ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
long cdOffset = apkSections.getZipCentralDirectoryOffset();
int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
@@ -767,8 +844,10 @@
cdRecord = CentralDirectoryRecord.getRecord(cd);
} catch (ZipFormatException e) {
throw new ApkFormatException(
- "Malformed ZIP Central Directory record #" + (i + 1)
- + " at file offset " + (cdOffset + offsetInsideCd),
+ "Malformed ZIP Central Directory record #"
+ + (i + 1)
+ + " at file offset "
+ + (cdOffset + offsetInsideCd),
e);
}
String entryName = cdRecord.getName();
@@ -780,15 +859,17 @@
}
if (cd.hasRemaining()) {
throw new ApkFormatException(
- "Unused space at the end of ZIP Central Directory: " + cd.remaining()
- + " bytes starting at file offset " + (cdOffset + cd.position()));
+ "Unused space at the end of ZIP Central Directory: "
+ + cd.remaining()
+ + " bytes starting at file offset "
+ + (cdOffset + cd.position()));
}
return cdRecords;
}
private static CentralDirectoryRecord findCdRecord(
- List<CentralDirectoryRecord> cdRecords, String name) {
+ List<CentralDirectoryRecord> cdRecords, String name) {
for (CentralDirectoryRecord cdRecord : cdRecords) {
if (name.equals(cdRecord.getName())) {
return cdRecord;
@@ -803,7 +884,7 @@
*/
static ByteBuffer getAndroidManifestFromApk(
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
- throws IOException, ApkFormatException, ZipFormatException {
+ throws IOException, ApkFormatException, ZipFormatException {
CentralDirectoryRecord androidManifestCdRecord =
findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME);
if (androidManifestCdRecord == null) {
@@ -816,12 +897,12 @@
}
/**
- * Return list of pin patterns embedded in the pin pattern asset
- * file. If no such file, return {@code null}.
+ * Return list of pin patterns embedded in the pin pattern asset file. If no such file, return
+ * {@code null}.
*/
private static List<Hints.PatternWithRange> extractPinPatterns(
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
- throws IOException, ApkFormatException {
+ throws IOException, ApkFormatException {
CentralDirectoryRecord pinListCdRecord =
findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
List<Hints.PatternWithRange> pinPatterns = null;
@@ -829,8 +910,9 @@
pinPatterns = new ArrayList<>();
byte[] patternBlob;
try {
- patternBlob = LocalFileRecord.getUncompressedData(
- lhfSection, pinListCdRecord, lhfSection.size());
+ patternBlob =
+ LocalFileRecord.getUncompressedData(
+ lhfSection, pinListCdRecord, lhfSection.size());
} catch (ZipFormatException ex) {
throw new ApkFormatException("Bad " + pinListCdRecord);
}
@@ -845,14 +927,13 @@
*/
private static int getMinSdkVersionFromApk(
List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
- throws IOException, MinSdkVersionException {
+ throws IOException, MinSdkVersionException {
ByteBuffer androidManifest;
try {
androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection);
} catch (ZipFormatException | ApkFormatException e) {
throw new MinSdkVersionException(
- "Failed to determine APK's minimum supported Android platform version",
- e);
+ "Failed to determine APK's minimum supported Android platform version", e);
}
return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
}
@@ -868,24 +949,18 @@
private final List<X509Certificate> mCertificates;
private SignerConfig(
- String name,
- PrivateKey privateKey,
- List<X509Certificate> certificates) {
+ String name, PrivateKey privateKey, List<X509Certificate> certificates) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
}
- /**
- * Returns the name of this signer.
- */
+ /** Returns the name of this signer. */
public String getName() {
return mName;
}
- /**
- * Returns the signing key of this signer.
- */
+ /** Returns the signing key of this signer. */
public PrivateKey getPrivateKey() {
return mPrivateKey;
}
@@ -898,9 +973,7 @@
return mCertificates;
}
- /**
- * Builder of {@link SignerConfig} instances.
- */
+ /** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
@@ -910,15 +983,12 @@
* Constructs a new {@code Builder}.
*
* @param name signer's name. The name is reflected in the name of files comprising the
- * JAR signature of the APK.
+ * JAR signature of the APK.
* @param privateKey signing key
* @param certificates list of one or more X.509 certificates. The subject public key of
- * the first certificate must correspond to the {@code privateKey}.
+ * the first certificate must correspond to the {@code privateKey}.
*/
- public Builder(
- String name,
- PrivateKey privateKey,
- List<X509Certificate> certificates) {
+ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
@@ -932,10 +1002,7 @@
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(
- mName,
- mPrivateKey,
- mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates);
}
}
}
@@ -944,19 +1011,24 @@
* Builder of {@link ApkSigner} instances.
*
* <p>The builder requires the following information to construct a working {@code ApkSigner}:
+ *
* <ul>
- * <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,</li>
- * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,</li>
- * <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
- * variants.
- * </li>
+ * <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+ * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+ * <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+ * variants.
* </ul>
*/
public static class Builder {
private final List<SignerConfig> mSignerConfigs;
+ private SignerConfig mSourceStampSignerConfig;
+ private boolean mForceSourceStampOverwrite = false;
private boolean mV1SigningEnabled = true;
private boolean mV2SigningEnabled = true;
private boolean mV3SigningEnabled = true;
+ private boolean mV4SigningEnabled = true;
+ private boolean mVerityEnabled = false;
+ private boolean mV4ErrorReportingEnabled = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy;
@@ -971,6 +1043,8 @@
private DataSink mOutputApkDataSink;
private DataSource mOutputApkDataSource;
+ private File mOutputV4File;
+
private SigningCertificateLineage mSigningCertificateLineage;
// APK Signature Scheme v3 only supports a single signing certificate, so to move to v3
@@ -985,12 +1059,12 @@
/**
* Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
* signer configurations. The resulting signer may be further customized through this
- * builder's setters, such as {@link #setMinSdkVersion(int)},
- * {@link #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)},
- * {@link #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
+ * builder's setters, such as {@link #setMinSdkVersion(int)}, {@link
+ * #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, {@link
+ * #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}.
*
- * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where
- * more control over low-level details of signing is desired.
+ * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
+ * control over low-level details of signing is desired.
*/
public Builder(List<SignerConfig> signerConfigs) {
if (signerConfigs.isEmpty()) {
@@ -1007,10 +1081,10 @@
}
/**
- * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the
- * provided signing engine. This is meant for advanced use cases where more control is
- * needed over the lower-level details of signing. For typical use cases,
- * {@link #Builder(List)} is more appropriate.
+ * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided
+ * signing engine. This is meant for advanced use cases where more control is needed over
+ * the lower-level details of signing. For typical use cases, {@link #Builder(List)} is more
+ * appropriate.
*/
public Builder(ApkSignerEngine signerEngine) {
if (signerEngine == null) {
@@ -1020,6 +1094,22 @@
mSignerConfigs = null;
}
+ /** Sets the signing configuration of the source stamp to be embedded in the APK. */
+ public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) {
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should overwrite existing source stamp, if found.
+ *
+ * @param force {@code true} to require the APK to be overwrite existing source stamp
+ */
+ public Builder setForceSourceStampOverwrite(boolean force) {
+ mForceSourceStampOverwrite = force;
+ return this;
+ }
+
/**
* Sets the APK to be signed.
*
@@ -1071,8 +1161,8 @@
* the sink.
*
* <p>This variant of {@code setOutputApk} is useful for avoiding writing the output APK to
- * a file. For example, an in-memory data sink, such as
- * {@link DataSinks#newInMemoryDataSink()}, could be used instead of a file.
+ * a file. For example, an in-memory data sink, such as {@link
+ * DataSinks#newInMemoryDataSink()}, could be used instead of a file.
*
* @see #setOutputApk(File)
* @see #setOutputApk(DataSink, DataSource)
@@ -1085,8 +1175,8 @@
}
/**
- * Sets the sink which will receive the output (signed) APK. Data received by the
- * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+ * Sets the sink which will receive the output (signed) APK. Data received by the {@code
+ * outputApkOut} sink must be visible through the {@code outputApkIn} data source.
*
* <p>This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the
* sink and the source to be different objects.
@@ -1108,8 +1198,20 @@
}
/**
- * Sets the minimum Android platform version (API Level) on which APK signatures produced
- * by the signer being built must verify. This method is useful for overriding the default
+ * Sets the location of the V4 output file. {@code ApkSigner} will create this file if it
+ * doesn't exist.
+ */
+ public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) {
+ if (v4SignatureOutputFile == null) {
+ throw new NullPointerException("v4HashRootOutputFile == null");
+ }
+ mOutputV4File = v4SignatureOutputFile;
+ return this;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) on which APK signatures produced by
+ * the signer being built must verify. This method is useful for overriding the default
* behavior where the minimum API Level is obtained from the {@code android:minSdkVersion}
* attribute of the APK's {@code AndroidManifest.xml}.
*
@@ -1119,8 +1221,8 @@
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
* with an {@link ApkSignerEngine}.
*
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
*/
public Builder setMinSdkVersion(int minSdkVersion) {
checkInitializedWithoutEngine();
@@ -1131,21 +1233,21 @@
/**
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
*
- * <p>By default, whether APK is signed using JAR signing is determined by
- * {@code ApkSigner}, based on the platform versions supported by the APK or specified using
- * {@link #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which
- * don't verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
+ * <p>By default, whether APK is signed using JAR signing is determined by {@code
+ * ApkSigner}, based on the platform versions supported by the APK or specified using {@link
+ * #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which don't
+ * verify on Android Marshmallow (Android 6.0, API Level 23) and lower.
*
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
* with an {@link ApkSignerEngine}.
*
- * @param enabled {@code true} to require the APK to be signed using JAR signing,
- * {@code false} to require the APK to not be signed using JAR signing.
- *
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
- *
- * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR signing</a>
+ * @param enabled {@code true} to require the APK to be signed using JAR signing, {@code
+ * false} to require the APK to not be signed using JAR signing.
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
+ * @see <a
+ * href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR
+ * signing</a>
*/
public Builder setV1SigningEnabled(boolean enabled) {
checkInitializedWithoutEngine();
@@ -1165,13 +1267,11 @@
* with an {@link ApkSignerEngine}.
*
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
- * v2, {@code false} to require the APK to not be signed using APK Signature Scheme
- * v2.
- *
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
- *
- * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ * v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2.
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature
+ * Scheme v2</a>
*/
public Builder setV2SigningEnabled(boolean enabled) {
checkInitializedWithoutEngine();
@@ -1189,15 +1289,14 @@
*
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
* with an {@link ApkSignerEngine}.
+ *
* <p><em>Note:</em> APK Signature Scheme v3 only supports a single signing certificate, but
* may take multiple signers mapping to different targeted platform versions.
*
* @param enabled {@code true} to require the APK to be signed using APK Signature Scheme
- * v3, {@code false} to require the APK to not be signed using APK Signature Scheme
- * v3.
- *
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
+ * v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3.
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
*/
public Builder setV3SigningEnabled(boolean enabled) {
checkInitializedWithoutEngine();
@@ -1211,8 +1310,53 @@
}
/**
- * Sets whether the APK should be signed even if it is marked as debuggable
- * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+ * Sets whether the APK should be signed using APK Signature Scheme v4.
+ *
+ * <p>V4 signing requires that the APK be v2 or v3 signed.
+ *
+ * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
+ * or v3 and generate an v4 signature file
+ */
+ public Builder setV4SigningEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV4SigningEnabled = enabled;
+ mV4ErrorReportingEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether errors during v4 signing should be reported and halt the signing process.
+ *
+ * <p>Error reporting for v4 signing is disabled by default, but will be enabled if the
+ * caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful
+ * for tools that enable v4 signing by default but don't want to fail the signing process if
+ * the user did not explicitly request the v4 signing.
+ *
+ * @param enabled {@code false} to prevent errors encountered during the V4 signing from
+ * halting the signing process
+ */
+ public Builder setV4ErrorReportingEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV4ErrorReportingEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether to enable the verity signature algorithm for the v2 and v3 signature
+ * schemes.
+ *
+ * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
+ * v2 and v3 signature blocks.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mVerityEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed even if it is marked as debuggable ({@code
+ * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
* compatibility reasons, the default value of this setting is {@code true}.
*
* <p>It is dangerous to sign debuggable APKs with production/release keys because Android
@@ -1237,8 +1381,8 @@
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
* with an {@link ApkSignerEngine}.
*
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
*/
public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
checkInitializedWithoutEngine();
@@ -1252,8 +1396,8 @@
* <p><em>Note:</em> This method may only be invoked when this builder is not initialized
* with an {@link ApkSignerEngine}.
*
- * @throws IllegalStateException if this builder was initialized with an
- * {@link ApkSignerEngine}
+ * @throws IllegalStateException if this builder was initialized with an {@link
+ * ApkSignerEngine}
*/
public Builder setCreatedBy(String createdBy) {
checkInitializedWithoutEngine();
@@ -1272,7 +1416,7 @@
}
/**
- * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+ * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
* structure provides proof of signing certificate rotation linking {@link SignerConfig}
* objects to previous ones.
*/
@@ -1290,10 +1434,10 @@
* this builder.
*/
public ApkSigner build() {
-
if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
- throw new IllegalStateException("Builder configured to both enable and disable APK "
- + "Signature Scheme v3 signing");
+ throw new IllegalStateException(
+ "Builder configured to both enable and disable APK "
+ + "Signature Scheme v3 signing");
}
if (mV3SigningExplicitlyDisabled) {
@@ -1304,14 +1448,31 @@
mV3SigningEnabled = true;
}
+ // If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing
+ // must be disabled as well as it is dependent on V2/V3.
+ if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) {
+ if (!mV4ErrorReportingEnabled) {
+ mV4SigningEnabled = false;
+ } else {
+ throw new IllegalStateException(
+ "APK Signature Scheme v4 signing requires at least "
+ + "v2 or v3 signing to be enabled");
+ }
+ }
+
// TODO - if v3 signing is enabled, check provided signers and history to see if valid
return new ApkSigner(
mSignerConfigs,
+ mSourceStampSignerConfig,
+ mForceSourceStampOverwrite,
mMinSdkVersion,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
+ mV4SigningEnabled,
+ mVerityEnabled,
+ mV4ErrorReportingEnabled,
mDebuggableApkPermitted,
mOtherSignersSignaturesPreserved,
mCreatedBy,
@@ -1321,6 +1482,7 @@
mOutputApkFile,
mOutputApkDataSink,
mOutputApkDataSource,
+ mOutputV4File,
mSigningCertificateLineage);
}
}
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 138bc38..c79f232 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -20,9 +20,10 @@
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.Closeable;
+import java.io.File;
import java.io.IOException;
-import java.lang.UnsupportedOperationException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
@@ -220,7 +221,7 @@
*/
OutputJarSignatureRequest outputJarEntries()
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
- SignatureException, IllegalStateException;
+ SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
@@ -258,8 +259,8 @@
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
- throws IOException, ApkFormatException, NoSuchAlgorithmException,
- InvalidKeyException, SignatureException, IllegalStateException;
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
@@ -293,8 +294,8 @@
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
- throws IOException, ApkFormatException, NoSuchAlgorithmException,
- InvalidKeyException, SignatureException, IllegalStateException;
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the signed APK was output.
@@ -308,6 +309,35 @@
void outputDone() throws IllegalStateException;
/**
+ * Generates a V4 signature proto and write to output file.
+ *
+ * @param data Input data to calculate a verity hash tree and hash root
+ * @param outputFile To store the serialized V4 Signature.
+ * @param ignoreFailures Whether any failures will be silently ignored.
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IOException if protobuf fails to be serialized and written to file
+ */
+ void signV4(DataSource data, File outputFile, boolean ignoreFailures)
+ throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
+
+ /**
+ * Checks if the signing configuration provided to the engine is capable of creating a
+ * SourceStamp.
+ */
+ default boolean isEligibleForSourceStamp() {
+ return false;
+ }
+
+ /** Generates the digest of the certificate used to sign the source stamp. */
+ default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ return new byte[0];
+ }
+
+ /**
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
* engine is OK.
*
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 3e1e7da..f2d0fbc 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -16,21 +16,32 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v1.V1SchemeSigner.MANIFEST_ENTRY_NAME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
-import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.util.RunnablesExecutor;
import com.android.apksig.zip.ZipFormatException;
+
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -75,6 +86,7 @@
private final File mApkFile;
private final DataSource mApkDataSource;
+ private final File mV4SignatureFile;
private final Integer mMinSdkVersion;
private final int mMaxSdkVersion;
@@ -82,10 +94,12 @@
private ApkVerifier(
File apkFile,
DataSource apkDataSource,
+ File v4SignatureFile,
Integer minSdkVersion,
int maxSdkVersion) {
mApkFile = apkFile;
mApkDataSource = apkDataSource;
+ mV4SignatureFile = v4SignatureFile;
mMinSdkVersion = minSdkVersion;
mMaxSdkVersion = maxSdkVersion;
}
@@ -186,6 +200,8 @@
}
Result result = new Result();
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
// The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
// name, but the verifiers use this parameter as the schemes supported by the target SDK
@@ -205,7 +221,7 @@
SUPPORTED_APK_SIG_SCHEME_NAMES.get(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
} else {
- supportedSchemeNames = Collections.EMPTY_MAP;
+ supportedSchemeNames = Collections.emptyMap();
}
// Android N and newer attempts to verify APKs using the APK Signing Block, which can
// include v2 and/or v3 signatures. If none is found, it falls back to JAR signature
@@ -225,6 +241,9 @@
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
result.mergeFrom(v3Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+ getApkContentDigestsFromSigningSchemeResult(v3Result));
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v3 signature not required
}
@@ -250,6 +269,9 @@
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
result.mergeFrom(v2Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ getApkContentDigestsFromSigningSchemeResult(v2Result));
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v2 signature not required
}
@@ -257,6 +279,18 @@
return result;
}
}
+
+ // If v4 file is specified, use additional verification on it
+ if (mV4SignatureFile != null) {
+ final ApkSigningBlockUtils.Result v4Result =
+ V4SchemeVerifier.verify(apk, mV4SignatureFile);
+ foundApkSigSchemeIds.add(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ result.mergeFrom(v4Result);
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
}
// Android O and newer requires that APKs targeting security sandbox version 2 and higher
@@ -276,6 +310,9 @@
}
}
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
// Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N
// ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures.
// Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer
@@ -290,6 +327,46 @@
minSdkVersion,
maxSdkVersion);
result.mergeFrom(v1Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verify the SourceStamp, if found in the APK.
+ try {
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+ cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+ // If SourceStamp file is found inside the APK, there must be a SourceStamp
+ // block in the APK signing block as well.
+ if (sourceStampCdRecord != null) {
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ ApkSigningBlockUtils.Result sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ Math.max(minSdkVersion, AndroidSdkVersion.R),
+ maxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ }
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
}
if (result.containsErrors()) {
return result;
@@ -308,7 +385,7 @@
try {
v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
} catch (CertificateEncodingException e) {
- throw new RuntimeException(
+ throw new IllegalStateException(
"Failed to encode JAR signer " + signer.getName() + " certs", e);
}
}
@@ -316,7 +393,7 @@
try {
v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
} catch (CertificateEncodingException e) {
- throw new RuntimeException(
+ throw new IllegalStateException(
"Failed to encode APK Signature Scheme v2 signer (index: "
+ signer.getIndex() + ") certs",
e);
@@ -400,6 +477,85 @@
}
}
+
+ // If there is a v4 scheme signer, make sure that their certificates match.
+ // The apkDigest field in the v4 signature should match the selected v2/v3.
+ if (result.isVerifiedUsingV4Scheme()) {
+ List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
+ if (v4Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
+ v4Signers.get(0).getContentDigests();
+ if (digestsFromV4.size() != 1) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners();
+ if (v3Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV3 = pickBestDigestForV4(
+ v3Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV3)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ } else if (result.isVerifiedUsingV2Scheme()) {
+ List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
+ if (v2Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV2 = pickBestDigestForV4(
+ v2Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV2)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH);
+ }
+ } else {
+ throw new RuntimeException("V4 signature must be also verified with V2/V3");
+ }
+ }
+
+ // If the targetSdkVersion has a minimum required signature scheme version then verify
+ // that the APK was signed with at least that version.
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+ androidManifest.slice());
+ int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+ // The platform currently only enforces a single minimum signature scheme version, but when
+ // later platform versions support another minimum version this will need to be expanded to
+ // verify the minimum based on the target and maximum SDK version.
+ if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME && maxSdkVersion >= targetSdkVersion) {
+ switch(minSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ if (result.isVerifiedUsingV2Scheme()) {
+ break;
+ }
+ // Allow this case to fall through to the next as a signature satisfying a later
+ // scheme version will also satisfy this requirement.
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ if (result.isVerifiedUsingV3Scheme()) {
+ break;
+ }
+ result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion,
+ minSchemeVersion);
+ }
+ }
+
if (result.containsErrors()) {
return result;
}
@@ -419,12 +575,84 @@
}
} else {
throw new RuntimeException(
- "APK verified, but has not verified using any of v1, v2 or v3schemes");
+ "APK verified, but has not verified using any of v1, v2 or v3 schemes");
}
return result;
}
+ private static void checkV4Certificate(List<X509Certificate> v4Certs, List<X509Certificate> v2v3Certs, Result result) {
+ try {
+ byte[] v4Cert = v4Certs.get(0).getEncoded();
+ byte[] cert = v2v3Certs.get(0).getEncoded();
+ if (!Arrays.equals(cert, v4Cert)) {
+ result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH);
+ }
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException("Failed to encode APK signer cert", e);
+ }
+ }
+
+ private static byte[] pickBestDigestForV4(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ collectApkContentDigests(contentDigests, apkContentDigests);
+ return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+ ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+ collectApkContentDigests(signerInfo.contentDigests, apkContentDigests);
+ }
+ return apkContentDigests;
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new HashMap<>();
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ manifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+ }
+
+ }
+
private static ByteBuffer getAndroidManifestFromApk(
DataSource apk, ApkUtils.ZipSections zipSections)
throws IOException, ApkFormatException {
@@ -444,6 +672,15 @@
* AndroidManifest.xml.
*/
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+ private static final String TARGET_SANDBOX_VERSION_ELEMENT_NAME = "manifest";
+
+ /**
+ * Android resource ID of the {@code android:targetSdkVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
+ private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
+ private static final String USES_SDK_ELEMENT_NAME = "uses-sdk";
/**
* Returns the security sandbox version targeted by an APK with the provided
@@ -456,21 +693,61 @@
*/
private static int getTargetSandboxVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
- // Return the value of the android:targetSandboxVersion attribute of the top-level manifest
- // element
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ TARGET_SANDBOX_VERSION_ELEMENT_NAME, TARGET_SANDBOX_VERSION_ATTR_ID);
+ }
+
+ /**
+ * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ * @throws ApkFormatException if an error occurred while determining the version
+ */
+ private static int getTargetSdkVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) {
+ // If the targetSdkVersion is not specified then the platform will use the value of the
+ // minSdkVersion; if neither is specified then the platform will use a value of 1.
+ int minSdkVersion = 1;
+ try {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ USES_SDK_ELEMENT_NAME, TARGET_SDK_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
+ // element is not specified at all.
+ }
+ androidManifestContents.rewind();
+ try {
+ minSdkVersion = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ USES_SDK_ELEMENT_NAME, MIN_SDK_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // Similar to above, expected if the APK does not contain a minSdkVersion attribute or
+ // the uses-sdk element is not specified at all.
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the integer value of the requested {@code attributeId} in the specified {@code
+ * elementName} from the provided {@code androidManifestContents} in binary Android resource
+ * format.
+ *
+ * @throws ApkFormatException if an error occurred while attempting to obtain the attribute
+ */
+ private static int getAttributeValueFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents, String elementName, int attributeId)
+ throws ApkFormatException {
+ // Return the value of the requested attribute from the specified element.
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
- && (parser.getDepth() == 1)
- && ("manifest".equals(parser.getName()))
+ && (elementName.equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
- // In each manifest element, targetSandboxVersion defaults to 1
int result = 1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
- if (parser.getAttributeNameResourceId(i)
- == TARGET_SANDBOX_VERSION_ATTR_ID) {
+ if (parser.getAttributeNameResourceId(i) == attributeId) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
@@ -478,10 +755,11 @@
break;
default:
throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
+ "Failed to determine APK's "
+ + elementName + " attribute"
+ ": unsupported value type of"
- + " AndroidManifest.xml"
- + " android:targetSandboxVersion"
+ + " AndroidManifest.xml "
+ + String.format("0x%08X", attributeId)
+ ". Only integer values supported.");
}
break;
@@ -492,16 +770,24 @@
eventType = parser.next();
}
throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
- + " : no manifest element in AndroidManifest.xml");
+ "Failed to determine APK's " + elementName + " attribute "
+ + String.format("0x%08X", attributeId)
+ + " : no " + elementName + " element in AndroidManifest.xml");
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
- "Failed to determine APK's target sandbox version"
- + ": malformed AndroidManifest.xml",
- e);
+ "Failed to determine APK's " + elementName + " attribute "
+ + String.format("0x%08X", attributeId)
+ + ": malformed AndroidManifest.xml", e);
}
}
+ private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
+ if (targetSdkVersion >= AndroidSdkVersion.R) {
+ return VERSION_APK_SIGNATURE_SCHEME_V2;
+ }
+ return VERSION_JAR_SIGNATURE_SCHEME;
+ }
+
/**
* Result of verifying an APKs signatures. The APK can be considered verified iff
* {@link #isVerified()} returns {@code true}.
@@ -514,11 +800,15 @@
private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
+ private SourceStampInfo mSourceStampInfo;
private boolean mVerified;
private boolean mVerifiedUsingV1Scheme;
private boolean mVerifiedUsingV2Scheme;
private boolean mVerifiedUsingV3Scheme;
+ private boolean mVerifiedUsingV4Scheme;
+ private boolean mSourceStampVerified;
private SigningCertificateLineage mSigningCertificateLineage;
/**
@@ -554,6 +844,20 @@
}
/**
+ * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
+ */
+ public boolean isVerifiedUsingV4Scheme() {
+ return mVerifiedUsingV4Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's SourceStamp signature verified.
+ */
+ public boolean isSourceStampVerified() {
+ return mSourceStampVerified;
+ }
+
+ /**
* Returns the verified signers' certificates, one per signer.
*/
public List<X509Certificate> getSignerCertificates() {
@@ -605,6 +909,17 @@
return mV3SchemeSigners;
}
+ private List<V4SchemeSignerInfo> getV4SchemeSigners() {
+ return mV4SchemeSigners;
+ }
+
+ /**
+ * Returns information about SourceStamp associated with the APK's signature.
+ */
+ public SourceStampInfo getSourceStampInfo() {
+ return mSourceStampInfo;
+ }
+
/**
* Returns the combined SigningCertificateLineage associated with this APK's APK Signature
* Scheme v3 signing block.
@@ -617,6 +932,10 @@
mErrors.add(new IssueWithParams(msg, parameters));
}
+ void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
/**
* Returns errors encountered while verifying the APK's signatures.
*/
@@ -658,6 +977,18 @@
}
mSigningCertificateLineage = source.signingCertificateLineage;
break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+ mVerifiedUsingV4Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.signers.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
+ }
+ break;
default:
throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
}
@@ -694,6 +1025,9 @@
}
}
}
+ if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) {
+ return true;
+ }
return false;
}
@@ -799,12 +1133,15 @@
private final List<IssueWithParams> mErrors;
private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
mIndex = result.index;
mCerts = result.certs;
mErrors = result.getErrors();
mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
}
/**
@@ -850,6 +1187,10 @@
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
}
/**
@@ -861,12 +1202,15 @@
private final List<IssueWithParams> mErrors;
private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
mIndex = result.index;
mCerts = result.certs;
mErrors = result.getErrors();
mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
}
/**
@@ -908,13 +1252,122 @@
public List<IssueWithParams> getWarnings() {
return mWarnings;
}
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme V4 signer associated with the APK's
+ * signature.
+ */
+ public static class V4SchemeSignerInfo {
+ private final int mIndex;
+ private final List<X509Certificate> mCerts;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
+
+ private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v3 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * <p>This certificate contains the signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List<X509Certificate> getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * Information about SourceStamp associated with the APK's signature.
+ */
+ public static class SourceStampInfo {
+ private final List<X509Certificate> mCertificates;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+
+ private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mCertificates = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * <p>This certificate contains the SourceStamp's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificates.isEmpty() ? null : mCertificates.get(0);
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
}
}
/**
* Error or warning encountered while verifying an APK's signatures.
*/
- public static enum Issue {
+ public enum Issue {
/**
* APK is not JAR-signed.
@@ -1254,6 +1707,19 @@
+ " %1$d"),
/**
+ * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+ * APK is not signed with that version or later.
+ *
+ * <ul>
+ * <li>Parameter 1: target SDK Version (@code Integer})</li>
+ * <li>Parameter 2: minimum signature scheme version ((@code Integer})</li>
+ * </ul>
+ */
+ MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+ "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+ + " not signed with this or a later signature scheme"),
+
+ /**
* APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR
* signature from this signer, but does not contain an APK Signature Scheme v2 signature
* from this signer.
@@ -1401,7 +1867,7 @@
/**
* This APK Signature Scheme v2 signer offers signatures but none of them are supported.
*/
- V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+ V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"),
/**
* This APK Signature Scheme v2 signer offers no certificates.
@@ -1724,11 +2190,221 @@
* <li>Parameter 1: entry ID ({@code Integer})</li>
* </ul>
*/
- APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
+ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme
+ * V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_SIGNERS(
+ "V4 signature has malformed signer block"),
+
+ /**
+ * This APK Signature Scheme V4 signer contains a signature produced using an
+ * unknown algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+ * </ul>
+ */
+ V4_SIG_UNKNOWN_SIG_ALGORITHM(
+ "V4 signature has unknown signing algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no signatures.
+ */
+ V4_SIG_NO_SIGNATURES(
+ "V4 signature has no signature found"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers signatures but none of them are
+ * supported.
+ */
+ V4_SIG_NO_SUPPORTED_SIGNATURES(
+ "V4 signature has no supported signature"),
+
+ /**
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * </ul>
+ */
+ V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+ * parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme V4 signer's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})</li>
+ * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})</li>
+ * <li>Parameter 3: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_CERTIFICATE(
+ "V4 signature has malformed certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no certificate.
+ */
+ V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer's public key listed in the signer's
+ * certificate does not match the public key listed in the signature proto.
+ *
+ * <ul>
+ * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+ * <li>Parameter 2: hex-encoded public key from signature proto ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK's hash root (aka digest) does not match the hash root contained in the Signature
+ * Scheme V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_APK_ROOT_DID_NOT_VERIFY(
+ "V4 signature's hash tree root (content digest) did not verity"),
+
+ /**
+ * The APK's hash tree does not match the hash tree contained in the Signature
+ * Scheme V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected hash tree of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual hash tree of the APK ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_APK_TREE_DID_NOT_VERIFY(
+ "V4 signature's hash tree did not verity"),
+
+ /**
+ * Using more than one Signer to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_MULTIPLE_SIGNERS(
+ "V4 signature only supports one signer"),
+
+ /**
+ * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
+ * used to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_V2_V3_SIGNERS_MISMATCH(
+ "V4 signature and V2/V3 signature have mismatched certificates"),
+
+ V4_SIG_V2_V3_DIGESTS_MISMATCH(
+ "V4 signature and V2/V3 signature have mismatched digests"),
+
+ /**
+ * The v4 signature format version isn't the same as the tool's current version, something
+ * may go wrong.
+ */
+ V4_SIG_VERSION_NOT_CURRENT(
+ "V4 signature format version %1$d is different from the tool's current "
+ + "version %2$d"),
+
+ /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
+ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
+
+ /**
+ * SourceStamp's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})
+ * </ul>
+ */
+ SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"),
+
+ /** Failed to parse SourceStamp's signature. */
+ SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"),
+
+ /**
+ * SourceStamp contains a signature produced using an unknown algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})
+ * </ul>
+ */
+ SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * An exception was encountered while verifying SourceStamp signature.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * <li>Parameter 2: exception ({@code Throwable})
+ * </ul>
+ */
+ SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * SourceStamp signature block did not verify.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * </ul>
+ */
+ SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /** SourceStamp offers no signatures. */
+ SOURCE_STAMP_NO_SIGNATURE("No signature"),
+
+ /** SourceStamp offers an unsupported signature. */
+ SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
+
+ /**
+ * SourceStamp's certificate listed in the APK signing block does not match the certificate
+ * listed in the SourceStamp file in the APK.
+ *
+ * <ul>
+ * <li>Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing
+ * block ({@code String})
+ * <li>Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code
+ * String})
+ * </ul>
+ */
+ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
+ "Certificate mismatch between SourceStamp block in APK signing block and"
+ + " SourceStamp file in APK: <%1$s> vs <%2$s>");
private final String mFormat;
- private Issue(String format) {
+ Issue(String format) {
mFormat = format;
}
@@ -1804,10 +2480,7 @@
if (this == obj) {
return true;
}
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
+ if (!(obj instanceof ByteArray)) {
return false;
}
ByteArray other = (ByteArray) obj;
@@ -1832,6 +2505,7 @@
public static class Builder {
private final File mApkFile;
private final DataSource mApkDataSource;
+ private File mV4SignatureFile;
private Integer mMinSdkVersion;
private int mMaxSdkVersion = Integer.MAX_VALUE;
@@ -1894,6 +2568,11 @@
return this;
}
+ public Builder setV4SignatureFile(File v4SignatureFile) {
+ mV4SignatureFile = v4SignatureFile;
+ return this;
+ }
+
/**
* Returns an {@link ApkVerifier} initialized according to the configuration of this
* builder.
@@ -1902,6 +2581,7 @@
return new ApkVerifier(
mApkFile,
mApkDataSource,
+ mV4SignatureFile,
mMinSdkVersion,
mMaxSdkVersion);
}
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index c88239e..f0796fb 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -16,15 +16,25 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
import com.android.apksig.internal.apk.v1.V1SchemeSigner;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4Signature;
import com.android.apksig.internal.jar.ManifestParser;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.Pair;
@@ -32,10 +42,12 @@
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
-
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -43,15 +55,16 @@
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
/**
@@ -79,10 +92,12 @@
private final boolean mV1SigningEnabled;
private final boolean mV2SigningEnabled;
private final boolean mV3SigningEnabled;
+ private final boolean mVerityEnabled;
private final boolean mDebuggableApkPermitted;
private final boolean mOtherSignersSignaturesPreserved;
private final String mCreatedBy;
private final List<SignerConfig> mSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
@@ -93,9 +108,7 @@
private boolean mV1SignaturePending;
- /**
- * Names of JAR entries which this engine is expected to output as part of v1 signing.
- */
+ /** Names of JAR entries which this engine is expected to output as part of v1 signing. */
private Set<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
/** Requests for digests of output JAR entries. */
@@ -123,8 +136,8 @@
private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest;
/**
- * Whether the package being signed is marked as {@code android:debuggable} or {@code null}
- * if this is not yet known.
+ * Whether the package being signed is marked as {@code android:debuggable} or {@code null} if
+ * this is not yet known.
*/
private Boolean mDebuggable;
@@ -142,19 +155,21 @@
*/
private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest;
-
- private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+ private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
private DefaultApkSignerEngine(
List<SignerConfig> signerConfigs,
+ SignerConfig sourceStampSignerConfig,
int minSdkVersion,
boolean v1SigningEnabled,
boolean v2SigningEnabled,
boolean v3SigningEnabled,
+ boolean verityEnabled,
boolean debuggableApkPermitted,
boolean otherSignersSignaturesPreserved,
String createdBy,
- SigningCertificateLineage signingCertificateLineage) throws InvalidKeyException {
+ SigningCertificateLineage signingCertificateLineage)
+ throws InvalidKeyException {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
@@ -166,6 +181,7 @@
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
mV3SigningEnabled = v3SigningEnabled;
+ mVerityEnabled = verityEnabled;
mV1SignaturePending = v1SigningEnabled;
mV2SignaturePending = v2SigningEnabled;
mV3SignaturePending = v3SigningEnabled;
@@ -173,6 +189,7 @@
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
mCreatedBy = createdBy;
mSignerConfigs = signerConfigs;
+ mSourceStampSignerConfig = sourceStampSignerConfig;
mMinSdkVersion = minSdkVersion;
mSigningCertificateLineage = signingCertificateLineage;
@@ -191,13 +208,12 @@
oldestConfig.mCertificates.get(0));
if (subLineage.size() != 1) {
throw new IllegalArgumentException(
- "v1 signing enabled but the oldest signer in the "
- + "SigningCertificateLineage is missing. Please provide the oldest"
- + " signer to enable v1 signing");
+ "v1 signing enabled but the oldest signer in the"
+ + " SigningCertificateLineage is missing. Please provide the"
+ + " oldest signer to enable v1 signing");
}
}
- createV1SignerConfigs(
- Collections.singletonList(oldestConfig), minSdkVersion);
+ createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
} else {
createV1SignerConfigs(signerConfigs, minSdkVersion);
}
@@ -216,19 +232,20 @@
String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName());
// Check whether the signer's name is unique among all v1 signers
- Integer indexOfOtherSignerWithSameName =
- v1SignerNameToSignerIndex.put(v1SignerName, i);
+ Integer indexOfOtherSignerWithSameName = v1SignerNameToSignerIndex.put(v1SignerName, i);
if (indexOfOtherSignerWithSameName != null) {
throw new IllegalArgumentException(
- "Signers #" + (indexOfOtherSignerWithSameName + 1)
- + " and #" + (i + 1)
- + " have the same name: " + v1SignerName
- + ". v1 signer names must be unique");
+ "Signers #"
+ + (indexOfOtherSignerWithSameName + 1)
+ + " and #"
+ + (i + 1)
+ + " have the same name: "
+ + v1SignerName
+ + ". v1 signer names must be unique");
}
DigestAlgorithm v1SignatureDigestAlgorithm =
- V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
- publicKey, minSdkVersion);
+ V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion);
V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
v1SignerConfig.name = v1SignerName;
v1SignerConfig.privateKey = signerConfig.getPrivateKey();
@@ -243,7 +260,8 @@
v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
} else {
if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
- v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) > 0) {
+ v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+ > 0) {
v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
}
}
@@ -260,8 +278,7 @@
// v3 signing only supports single signers, of which the oldest (first) will be the one
// to use for v1 and v2 signing
- List<ApkSigningBlockUtils.SignerConfig> signerConfig =
- new ArrayList<>();
+ List<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
SignerConfig oldestConfig = mSignerConfigs.get(0);
@@ -271,18 +288,21 @@
SigningCertificateLineage subLineage =
mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0));
if (subLineage.size() != 1) {
- throw new IllegalArgumentException("v2 signing enabled but the oldest signer in"
+ throw new IllegalArgumentException(
+ "v2 signing enabled but the oldest signer in"
+ " the SigningCertificateLineage is missing. Please provide"
+ " the oldest signer to enable v2 signing.");
}
}
signerConfig.add(
createSigningBlockSignerConfig(
- mSignerConfigs.get(0), apkSigningBlockPaddingSupported,
+ mSignerConfigs.get(0),
+ apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
return signerConfig;
} else {
- return createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ return createSigningBlockSignerConfigs(
+ apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
}
}
@@ -290,7 +310,8 @@
private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
List<ApkSigningBlockUtils.SignerConfig> rawConfigs =
- createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ createSigningBlockSignerConfigs(
+ apkSigningBlockPaddingSupported,
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>();
@@ -304,8 +325,11 @@
// no valid algorithm was found for this signer, and we haven't yet covered all
// platform versions, something's wrong
String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm();
- throw new InvalidKeyException("Unsupported key algorithm " + keyAlgorithm + " is "
- + "not supported for APK Signature Scheme v3 signing");
+ throw new InvalidKeyException(
+ "Unsupported key algorithm "
+ + keyAlgorithm
+ + " is "
+ + "not supported for APK Signature Scheme v3 signing");
}
if (i == rawConfigs.size() - 1) {
// first go through the loop, config should support all future platform versions.
@@ -333,12 +357,32 @@
}
if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) {
// we can't cover all desired SDK versions, abort
- throw new InvalidKeyException("Provided key algorithms not supported on all desired "
- + "Android SDK versions");
+ throw new InvalidKeyException(
+ "Provided key algorithms not supported on all desired "
+ + "Android SDK versions");
}
return processedConfigs;
}
+ private ApkSigningBlockUtils.SignerConfig createV4SignerConfig()
+ throws InvalidKeyException, IllegalStateException {
+ List<ApkSigningBlockUtils.SignerConfig> configs =
+ createSigningBlockSignerConfigs(
+ true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ if (configs.size() != 1) {
+ throw new IllegalStateException("Only accepting one signer config for V4 Signature.");
+ }
+ return configs.get(0);
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
+ throws InvalidKeyException {
+ return createSigningBlockSignerConfig(
+ mSourceStampSignerConfig,
+ /* apkSigningBlockPaddingSupported= */ false,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ }
+
private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) {
int min = Integer.MAX_VALUE;
for (SignatureAlgorithm algorithm : algorithms) {
@@ -370,26 +414,29 @@
private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
- throws InvalidKeyException {
+ throws InvalidKeyException {
List<X509Certificate> certificates = signerConfig.getCertificates();
PublicKey publicKey = certificates.get(0).getPublicKey();
- ApkSigningBlockUtils.SignerConfig newSignerConfig =
- new ApkSigningBlockUtils.SignerConfig();
+ ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
newSignerConfig.privateKey = signerConfig.getPrivateKey();
newSignerConfig.certificates = certificates;
switch (schemeId) {
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
newSignerConfig.signatureAlgorithms =
- V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, mMinSdkVersion,
- apkSigningBlockPaddingSupported);
+ V2SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled);
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
try {
newSignerConfig.signatureAlgorithms =
V3SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled);
} catch (InvalidKeyException e) {
// It is possible for a signer used for v1/v2 signing to not be allowed for use
@@ -399,6 +446,21 @@
newSignerConfig.signatureAlgorithms = null;
}
break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V4SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ } catch (InvalidKeyException e) {
+ // V4 is an optional signing schema, ok to proceed without.
+ newSignerConfig.signatureAlgorithms = null;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ newSignerConfig.signatureAlgorithms =
+ Collections.singletonList(
+ SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+ break;
default:
throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested");
}
@@ -416,13 +478,13 @@
* without recalculation. This step has a significant performance benefit in case of incremental
* build.
*
- * This method extracts and stored computed digest for every entry that it would compute it for
- * in the {@link #outputJarEntry(String)} method
+ * <p>This method extracts and stored computed digest for every entry that it would compute it
+ * for in the {@link #outputJarEntry(String)} method
*
* @param manifestBytes raw representation of MANIFEST.MF file
* @param entryNames a set of expected entries names
* @return set of entry names which were processed by the engine during the initialization, a
- * subset of entryNames
+ * subset of entryNames
*/
@Override
@SuppressWarnings("AndroidJdkLibsChecker")
@@ -431,20 +493,24 @@
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
- for (Map.Entry<String, ManifestParser.Section> entry: sections.getSecond().entrySet()) {
+ for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
String entryName = entry.getKey();
- if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) &&
- isDebuggable(entryName)) {
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
+ && isDebuggable(entryName)) {
- Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
+ V1SchemeVerifier.NamedDigest extractedDigest = null;
+ Collection<V1SchemeVerifier.NamedDigest> digestsToVerify =
V1SchemeVerifier.getDigestsToVerify(
- entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE)
- .stream()
- .filter(d -> d.jcaDigestAlgorithm == alg)
- .findFirst();
-
- extractedDigest.ifPresent(
- namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest));
+ entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE);
+ for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) {
+ if (digestToVerify.jcaDigestAlgorithm.equals(alg)) {
+ extractedDigest = digestToVerify;
+ break;
+ }
+ }
+ if (extractedDigest != null) {
+ mOutputJarEntryDigests.put(entryName, extractedDigest.digest);
+ }
}
}
return mOutputJarEntryDigests.keySet();
@@ -559,7 +625,8 @@
// check whether the output entry's data matches what the engine emitted.
dataRequest =
(mEmittedSignatureJarEntryData.containsKey(entryName))
- ? new GetJarEntryDataRequest(entryName) : null;
+ ? new GetJarEntryDataRequest(entryName)
+ : null;
}
if (dataRequest != null) {
@@ -619,8 +686,7 @@
+ mInputJarManifestEntryDataRequest.getEntryName());
}
- for (GetJarEntryDataDigestRequest digestRequest
- : mOutputJarEntryDigestRequests.values()) {
+ for (GetJarEntryDataDigestRequest digestRequest : mOutputJarEntryDigestRequests.values()) {
String entryName = digestRequest.getEntryName();
if (!digestRequest.isDone()) {
throw new IllegalStateException(
@@ -628,6 +694,14 @@
}
mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
}
+ if (isEligibleForSourceStamp()) {
+ MessageDigest messageDigest =
+ MessageDigest.getInstance(
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ messageDigest.update(generateSourceStampCertificateDigest());
+ mOutputJarEntryDigests.put(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+ }
mOutputJarEntryDigestRequests.clear();
for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
@@ -646,7 +720,16 @@
}
byte[] inputJarManifest =
(mInputJarManifestEntryDataRequest != null)
- ? mInputJarManifestEntryDataRequest.getData() : null;
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ if (isEligibleForSourceStamp()) {
+ inputJarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ }
// Check whether the most recently used signature (if present) is still fine.
checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
@@ -667,9 +750,7 @@
} else {
V1SchemeSigner.OutputManifestFile newManifest =
V1SchemeSigner.generateManifestFile(
- mV1ContentDigestAlgorithm,
- mOutputJarEntryDigests,
- inputJarManifest);
+ mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
byte[] emittedSignatureManifest =
mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
@@ -689,8 +770,8 @@
// Emitted v1 signature is still valid. Check whether the signature is there in the
// output.
signatureZipEntries = new ArrayList<>();
- for (Map.Entry<String, byte[]> expectedOutputEntry
- : mEmittedSignatureJarEntryData.entrySet()) {
+ for (Map.Entry<String, byte[]> expectedOutputEntry :
+ mEmittedSignatureJarEntryData.entrySet()) {
String entryName = expectedOutputEntry.getKey();
byte[] expectedData = expectedOutputEntry.getValue();
GetJarEntryDataRequest actualDataRequest =
@@ -734,21 +815,15 @@
@Deprecated
@Override
public OutputApkSigningBlockRequest outputZipSections(
- DataSource zipEntries,
- DataSource zipCentralDirectory,
- DataSource zipEocd)
- throws IOException, InvalidKeyException, SignatureException,
- NoSuchAlgorithmException {
+ DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false);
}
@Override
public OutputApkSigningBlockRequest2 outputZipSections2(
- DataSource zipEntries,
- DataSource zipCentralDirectory,
- DataSource zipEocd)
- throws IOException, InvalidKeyException, SignatureException,
- NoSuchAlgorithmException {
+ DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd)
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true);
}
@@ -757,59 +832,103 @@
DataSource zipCentralDirectory,
DataSource zipEocd,
boolean apkSigningBlockPaddingSupported)
- throws IOException, InvalidKeyException, SignatureException,
- NoSuchAlgorithmException {
+ throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException {
checkNotClosed();
checkV1SigningDoneIfEnabled();
- if (!mV2SigningEnabled && !mV3SigningEnabled) {
+ if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) {
return null;
}
checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
// adjust to proper padding
Pair<DataSource, Integer> paddingPair =
- ApkSigningBlockUtils.generateApkSigningBlockPadding(zipEntries,
- apkSigningBlockPaddingSupported);
+ ApkSigningBlockUtils.generateApkSigningBlockPadding(
+ zipEntries, apkSigningBlockPaddingSupported);
DataSource beforeCentralDir = paddingPair.getFirst();
- int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
- DataSource eocd =
- ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+ int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+ DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
// create APK Signature Scheme V2 Signature if requested
if (mV2SigningEnabled) {
invalidateV2Signature();
List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
createV2SignerConfigs(apkSigningBlockPaddingSupported);
- signingSchemeBlocks.add(
+ v2SigningSchemeBlockAndDigests =
V2SchemeSigner.generateApkSignatureSchemeV2Block(
mExecutor,
beforeCentralDir,
zipCentralDirectory,
eocd,
v2SignerConfigs,
- mV3SigningEnabled));
+ mV3SigningEnabled);
+ signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (mV3SigningEnabled) {
invalidateV3Signature();
List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
createV3SignerConfigs(apkSigningBlockPaddingSupported);
- signingSchemeBlocks.add(
+ v3SigningSchemeBlockAndDigests =
V3SchemeSigner.generateApkSignatureSchemeV3Block(
mExecutor,
beforeCentralDir,
zipCentralDirectory,
eocd,
- v3SignerConfigs));
+ v3SignerConfigs);
+ signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ if (isEligibleForSourceStamp()) {
+ ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
+ createSourceStampSignerConfig();
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos =
+ new HashMap<>();
+ if (mV3SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV2SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV1SigningEnabled) {
+ Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>();
+ try {
+ // Jar signing related variables must have been already populated at this point
+ // if V1 signing is enabled since it is happening before computations on the APK
+ // signing block (V2/V3/V4/SourceStamp signing).
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ byte[] jarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ // The digest of the jar manifest does not need to be computed in chunks due to
+ // the small size of the manifest.
+ v1SigningSchemeDigests.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+ } catch (ApkFormatException e) {
+ throw new RuntimeException("Failed to generate manifest file", e);
+ }
+ signatureSchemeDigestInfos.put(
+ VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+ }
+ signingSchemeBlocks.add(
+ V2SourceStampSigner.generateSourceStampBlock(
+ sourceStampSignerConfig, signatureSchemeDigestInfos));
}
- // create APK Signing Block with v2 and/or v3 blocks
- byte[] apkSigningBlock =
- ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+ // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
+ byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
- mAddSigningBlockRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock,
- padSizeBeforeApkSigningBlock);
+ mAddSigningBlockRequest =
+ new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock);
return mAddSigningBlockRequest;
}
@@ -821,6 +940,62 @@
}
@Override
+ public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+ throws SignatureException {
+ if (outputFile == null) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("Missing V4 output file.");
+ }
+ try {
+ ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ /** For external use only to generate V4 & tree separately. */
+ public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+ throws SignatureException {
+ if (sigOutput == null) {
+ throw new SignatureException("Missing V4 output streams.");
+ }
+ try {
+ ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig();
+ Pair<V4Signature, byte[]> pair =
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+ pair.getFirst().writeTo(sigOutput);
+ return pair.getSecond();
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ @Override
+ public boolean isEligibleForSourceStamp() {
+ return mSourceStampSignerConfig != null
+ && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
+ }
+
+ @Override
+ public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+ throw new SignatureException("No certificates configured for stamp");
+ }
+ try {
+ return computeSha256DigestBytes(
+ mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode source stamp certificate", e);
+ }
+ }
+
+ @Override
public void close() {
mClosed = true;
@@ -877,15 +1052,17 @@
"v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
+ " been fulfilled");
}
- for (Map.Entry<String, byte[]> expectedOutputEntry
- : mEmittedSignatureJarEntryData.entrySet()) {
+ for (Map.Entry<String, byte[]> expectedOutputEntry :
+ mEmittedSignatureJarEntryData.entrySet()) {
String entryName = expectedOutputEntry.getKey();
byte[] expectedData = expectedOutputEntry.getValue();
GetJarEntryDataRequest actualDataRequest =
mOutputSignatureJarEntryDataRequests.get(entryName);
if (actualDataRequest == null) {
throw new IllegalStateException(
- "APK entry " + entryName + " not yet output despite this having been"
+ "APK entry "
+ + entryName
+ + " not yet output despite this having been"
+ " requested");
} else if (!actualDataRequest.isDone()) {
throw new IllegalStateException(
@@ -918,8 +1095,7 @@
mV3SignaturePending = false;
}
- private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected()
- throws SignatureException {
+ private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() throws SignatureException {
if (mDebuggableApkPermitted) {
return;
}
@@ -936,8 +1112,8 @@
}
/**
- * Returns whether the output APK is debuggable according to its
- * {@code android:debuggable} declaration.
+ * Returns whether the output APK is debuggable according to its {@code android:debuggable}
+ * declaration.
*/
private boolean isOutputApkDebuggable() throws ApkFormatException {
if (mDebuggable != null) {
@@ -966,9 +1142,7 @@
mDebuggable = null;
}
- /**
- * Returns the output policy for the provided input JAR entry.
- */
+ /** Returns the output policy for the provided input JAR entry. */
private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
@@ -1036,9 +1210,7 @@
}
}
- /**
- * JAR entry inspection request which obtain the entry's uncompressed data.
- */
+ /** JAR entry inspection request which obtain the entry's uncompressed data. */
private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
private final String mEntryName;
private final Object mLock = new Object();
@@ -1104,9 +1276,7 @@
}
}
- /**
- * JAR entry inspection request which obtains the digest of the entry's uncompressed data.
- */
+ /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */
private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
private final String mEntryName;
private final String mJcaDigestAlgorithm;
@@ -1189,9 +1359,7 @@
}
}
- /**
- * JAR entry inspection request which transparently satisfies multiple such requests.
- */
+ /** JAR entry inspection request which transparently satisfies multiple such requests. */
private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest {
private final String mEntryName;
private final InspectJarEntryRequest[] mRequests;
@@ -1243,24 +1411,18 @@
private final List<X509Certificate> mCertificates;
private SignerConfig(
- String name,
- PrivateKey privateKey,
- List<X509Certificate> certificates) {
+ String name, PrivateKey privateKey, List<X509Certificate> certificates) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
}
- /**
- * Returns the name of this signer.
- */
+ /** Returns the name of this signer. */
public String getName() {
return mName;
}
- /**
- * Returns the signing key of this signer.
- */
+ /** Returns the signing key of this signer. */
public PrivateKey getPrivateKey() {
return mPrivateKey;
}
@@ -1273,9 +1435,7 @@
return mCertificates;
}
- /**
- * Builder of {@link SignerConfig} instances.
- */
+ /** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
@@ -1285,15 +1445,12 @@
* Constructs a new {@code Builder}.
*
* @param name signer's name. The name is reflected in the name of files comprising the
- * JAR signature of the APK.
+ * JAR signature of the APK.
* @param privateKey signing key
* @param certificates list of one or more X.509 certificates. The subject public key of
- * the first certificate must correspond to the {@code privateKey}.
+ * the first certificate must correspond to the {@code privateKey}.
*/
- public Builder(
- String name,
- PrivateKey privateKey,
- List<X509Certificate> certificates) {
+ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
@@ -1307,24 +1464,21 @@
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(
- mName,
- mPrivateKey,
- mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates);
}
}
}
- /**
- * Builder of {@link DefaultApkSignerEngine} instances.
- */
+ /** Builder of {@link DefaultApkSignerEngine} instances. */
public static class Builder {
private List<SignerConfig> mSignerConfigs;
+ private SignerConfig mStampSignerConfig;
private final int mMinSdkVersion;
private boolean mV1SigningEnabled = true;
private boolean mV2SigningEnabled = true;
private boolean mV3SigningEnabled = true;
+ private boolean mVerityEnabled = false;
private boolean mDebuggableApkPermitted = true;
private boolean mOtherSignersSignaturesPreserved;
private String mCreatedBy = "1.0 (Android)";
@@ -1344,15 +1498,13 @@
* Constructs a new {@code Builder}.
*
* @param signerConfigs information about signers with which the APK will be signed. At
- * least one signer configuration must be provided.
+ * least one signer configuration must be provided.
* @param minSdkVersion API Level of the oldest Android platform on which the APK is
- * supposed to be installed. See {@code minSdkVersion} attribute in the APK's
- * {@code AndroidManifest.xml}. The higher the version, the stronger signing features
- * will be enabled.
+ * supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code
+ * AndroidManifest.xml}. The higher the version, the stronger signing features will be
+ * enabled.
*/
- public Builder(
- List<SignerConfig> signerConfigs,
- int minSdkVersion) {
+ public Builder(List<SignerConfig> signerConfigs, int minSdkVersion) {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
@@ -1373,8 +1525,9 @@
public DefaultApkSignerEngine build() throws InvalidKeyException {
if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) {
- throw new IllegalStateException("Builder configured to both enable and disable APK "
- + "Signature Scheme v3 signing");
+ throw new IllegalStateException(
+ "Builder configured to both enable and disable APK "
+ + "Signature Scheme v3 signing");
}
if (mV3SigningExplicitlyDisabled) {
mV3SigningEnabled = false;
@@ -1391,31 +1544,43 @@
// this is a strange situation: we've provided a valid rotation history, but
// are only signing with v1/v2. blow up, since we don't know for sure with
// which signer the user intended to sign
- throw new IllegalStateException("Provided multiple signers which are part "
- + "of the SigningCertificateLineage, but not signing with APK "
- + "Signature Scheme v3");
+ throw new IllegalStateException(
+ "Provided multiple signers which are part of the"
+ + " SigningCertificateLineage, but not signing with APK"
+ + " Signature Scheme v3");
}
} catch (IllegalArgumentException e) {
- throw new IllegalStateException("Provided signer configs do not match the "
- + "provided SigningCertificateLineage", e);
+ throw new IllegalStateException(
+ "Provided signer configs do not match the "
+ + "provided SigningCertificateLineage",
+ e);
}
} else if (mV3SigningEnabled && mSignerConfigs.size() > 1) {
- throw new IllegalStateException("Multiple signing certificates provided for use "
- + "with APK Signature Scheme v3 without an accompanying SigningCertificateLineage");
+ throw new IllegalStateException(
+ "Multiple signing certificates provided for use with APK Signature Scheme"
+ + " v3 without an accompanying SigningCertificateLineage");
}
return new DefaultApkSignerEngine(
mSignerConfigs,
+ mStampSignerConfig,
mMinSdkVersion,
mV1SigningEnabled,
mV2SigningEnabled,
mV3SigningEnabled,
+ mVerityEnabled,
mDebuggableApkPermitted,
mOtherSignersSignaturesPreserved,
mCreatedBy,
mSigningCertificateLineage);
}
+ /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */
+ public Builder setStampSignerConfig(SignerConfig stampSignerConfig) {
+ mStampSignerConfig = stampSignerConfig;
+ return this;
+ }
+
/**
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
*
@@ -1454,8 +1619,20 @@
}
/**
- * Sets whether the APK should be signed even if it is marked as debuggable
- * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
+ * Sets whether the APK should be signed using the verity signature algorithm in the v2 and
+ * v3 signature blocks.
+ *
+ * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and
+ * v3 signature schemes.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ mVerityEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed even if it is marked as debuggable ({@code
+ * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward
* compatibility reasons, the default value of this setting is {@code true}.
*
* <p>It is dangerous to sign debuggable APKs with production/release keys because Android
@@ -1478,9 +1655,7 @@
return this;
}
- /**
- * Sets the value of the {@code Created-By} field in JAR signature files.
- */
+ /** Sets the value of the {@code Created-By} field in JAR signature files. */
public Builder setCreatedBy(String createdBy) {
if (createdBy == null) {
throw new NullPointerException();
@@ -1490,7 +1665,7 @@
}
/**
- * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
+ * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This
* structure provides proof of signing certificate rotation linking {@link SignerConfig}
* objects to previous ones.
*/
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 135d815..c6cbf5f 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -27,6 +27,8 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -41,6 +43,9 @@
*/
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
+ /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+
private ApkUtils() {}
/**
@@ -601,4 +606,15 @@
e);
}
}
+
+ public static byte[] computeSha256DigestBytes(byte[] data) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is not found", e);
+ }
+ messageDigest.update(data);
+ return messageDigest.digest();
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 7c13586..f027525 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,10 @@
package com.android.apksig.internal.apk;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
+
import com.android.apksig.ApkVerifier;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
@@ -25,6 +29,15 @@
import com.android.apksig.internal.asn1.Asn1DecodingException;
import com.android.apksig.internal.asn1.Asn1DerEncoder;
import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.Pkcs7Constants;
+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.ByteBufferDataSource;
import com.android.apksig.internal.util.ChainedDataSource;
import com.android.apksig.internal.util.Pair;
@@ -36,8 +49,8 @@
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
-
import com.android.apksig.util.RunnablesExecutor;
+
import java.io.IOException;
import java.math.BigInteger;
import java.nio.BufferUnderflowException;
@@ -60,6 +73,7 @@
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -67,24 +81,29 @@
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
-import java.util.stream.Collectors;
+
+import javax.security.auth.x500.X500Principal;
public class ApkSigningBlockUtils {
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
- public static final byte[] APK_SIGNING_BLOCK_MAGIC =
+ private static final byte[] APK_SIGNING_BLOCK_MAGIC =
new byte[] {
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
};
private static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
+ private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
+ {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
+
+ public static final int VERSION_SOURCE_STAMP = 0;
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
-
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
/**
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
@@ -191,7 +210,7 @@
centralDir,
new ByteBufferDataSource(modifiedEocd));
// Special checks for the verity algorithm requirements.
- if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) {
if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
throw new RuntimeException(
"APK Signing Block is not aligned on 4k boundary: " +
@@ -418,17 +437,20 @@
DataSource centralDir,
DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>();
- Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = digestAlgorithms.stream()
- .filter(a -> a == ContentDigestAlgorithm.CHUNKED_SHA256 ||
- a == ContentDigestAlgorithm.CHUNKED_SHA512)
- .collect(Collectors.toSet());
+ Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>();
+ for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) {
+ if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+ || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+ oneMbChunkBasedAlgorithm.add(digestAlgorithm);
+ }
+ }
computeOneMbChunkContentDigests(
executor,
oneMbChunkBasedAlgorithm,
new DataSource[] { beforeCentralDir, centralDir, eocd },
contentDigests);
- if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
}
return contentDigests;
@@ -627,17 +649,17 @@
for (ChunkSupplier.Chunk chunk = dataSupplier.get();
chunk != null;
chunk = dataSupplier.get()) {
- long size = chunk.dataSource.size();
+ int size = chunk.size;
if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
throw new RuntimeException("Chunk size greater than expected: " + size);
}
// First update with the chunk prefix.
- setUnsignedInt32LittleEndian((int)size, chunkContentPrefix, 1);
+ setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
// Then update with the chunk data.
- chunk.dataSource.feed(0, size, mdSink);
+ mdSink.consume(chunk.data);
// Now finalize chunk for all algorithms.
for (int i = 0; i < chunkDigests.size(); i++) {
@@ -685,7 +707,7 @@
i));
}
chunkCounts[i] = (int)chunkCount;
- totalChunkCount += chunkCount;
+ totalChunkCount = (int) (totalChunkCount + chunkCount);
}
this.totalChunkCount = totalChunkCount;
nextIndex = new AtomicInteger(0);
@@ -705,7 +727,7 @@
}
int dataSourceIndex = 0;
- int dataSourceChunkOffset = index;
+ long dataSourceChunkOffset = index;
for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
break;
@@ -717,49 +739,90 @@
dataSources[dataSourceIndex].size() -
dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
- // Note that slicing may involve its own locking. We may wish to reimplement the
- // underlying mechanism to get rid of that lock (e.g. ByteBufferDataSource should
- // probably get reimplemented to a delegate model, such that grabbing a slice
- // doesn't incur a lock).
- return new Chunk(
- dataSources[dataSourceIndex].slice(
- dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
- remainingSize),
- index);
+
+ final int size = (int)remainingSize;
+ final ByteBuffer buffer = ByteBuffer.allocate(size);
+ try {
+ dataSources[dataSourceIndex].copyTo(
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
+ buffer);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read chunk", e);
+ }
+ buffer.rewind();
+
+ return new Chunk(index, buffer, size);
}
static class Chunk {
private final int chunkIndex;
- private final DataSource dataSource;
+ private final ByteBuffer data;
+ private final int size;
- private Chunk(DataSource parentSource, int chunkIndex) {
+ private Chunk(int chunkIndex, ByteBuffer data, int size) {
this.chunkIndex = chunkIndex;
- dataSource = parentSource;
+ this.data = data;
+ this.size = size;
}
}
}
+ @SuppressWarnings("ByteBufferBackingArray")
private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
throws IOException, NoSuchAlgorithmException {
+ ByteBuffer encoded = createVerityDigestBuffer(true);
+ // Use 0s as salt for now. This also needs to be consistent in the fsverify header for
+ // kernel to use.
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) {
+ byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir,
+ eocd);
+ encoded.put(rootHash);
+ encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
+ outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array());
+ }
+ }
+
+ private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint8[32] Merkle tree root hash of SHA-256
- // * @+32 bytes int64 Length of source data
+ // * @+32 bytes int64 (optional) Length of source data
int backBufferSize =
- ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes() +
- Long.SIZE / Byte.SIZE;
+ VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+ if (includeSourceDataSize) {
+ backBufferSize += Long.SIZE / Byte.SIZE;
+ }
ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
encoded.order(ByteOrder.LITTLE_ENDIAN);
+ return encoded;
+ }
+ public static class VerityTreeAndDigest {
+ public final ContentDigestAlgorithm contentDigestAlgorithm;
+ public final byte[] rootHash;
+ public final byte[] tree;
+
+ VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash,
+ byte[] tree) {
+ this.contentDigestAlgorithm = contentDigestAlgorithm;
+ this.rootHash = rootHash;
+ this.tree = tree;
+ }
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
+ throws IOException, NoSuchAlgorithmException {
+ ByteBuffer encoded = createVerityDigestBuffer(false);
// Use 0s as salt for now. This also needs to be consistent in the fsverify header for
// kernel to use.
- VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8]);
- byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd);
- encoded.put(rootHash);
- encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size());
-
- outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array());
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
+ ByteBuffer tree = builder.generateVerityTree(dataSource);
+ byte[] rootHash = builder.getRootHashFromTree(tree);
+ encoded.put(rootHash);
+ return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array());
+ }
}
private static long getChunkCount(long inputSize, long chunkSize) {
@@ -1145,15 +1208,17 @@
if (minSdkVersion < minProvidedSignaturesVersion) {
throw new NoSupportedSignaturesException(
"Minimum provided signature version " + minProvidedSignaturesVersion +
- " < minSdkVersion " + minSdkVersion);
+ " > minSdkVersion " + minSdkVersion);
}
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
throw new NoSupportedSignaturesException("No supported signature");
}
- return bestSigAlgorithmOnSdkVersion.values().stream()
- .sorted((sig1, sig2) -> Integer.compare(
- sig1.algorithm.getId(), sig2.algorithm.getId()))
- .collect(Collectors.toList());
+ List<SupportedSignature> signaturesToVerify =
+ new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+ Collections.sort(
+ signaturesToVerify,
+ (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+ return signaturesToVerify;
}
public static class NoSupportedSignaturesException extends Exception {
@@ -1235,6 +1300,70 @@
}
/**
+ * Wrap the signature according to CMS PKCS #7 RFC 5652.
+ * The high-level simplified structure is as follows:
+ * // ContentInfo
+ * // digestAlgorithm
+ * // SignedData
+ * // bag of certificates
+ * // SignerInfo
+ * // signing cert issuer and serial number (for locating the cert in the above bag)
+ * // digestAlgorithm
+ * // signatureAlgorithm
+ * // signature
+ *
+ * @throws Asn1EncodingException if the ASN.1 structure could not be encoded
+ */
+ public static byte[] generatePkcs7DerEncodedMessage(
+ byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts,
+ AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)
+ throws Asn1EncodingException, CertificateEncodingException {
+ SignerInfo signerInfo = new SignerInfo();
+ signerInfo.version = 1;
+ X509Certificate signingCert = signerCerts.get(0);
+ X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
+ signerInfo.sid =
+ new SignerIdentifier(
+ new IssuerAndSerialNumber(
+ new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
+ signingCert.getSerialNumber()));
+
+ signerInfo.digestAlgorithm = digestAlgorithmId;
+ signerInfo.signatureAlgorithm = signatureAlgorithmId;
+ signerInfo.signature = ByteBuffer.wrap(signatureBytes);
+
+ SignedData signedData = new SignedData();
+ signedData.certificates = new ArrayList<>(signerCerts.size());
+ for (X509Certificate cert : signerCerts) {
+ signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
+ }
+ signedData.version = 1;
+ signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
+ signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
+ // If data is not null, data will be embedded as is in the result -- an attached pcsk7
+ signedData.encapContentInfo.content = data;
+ signedData.signerInfos = Collections.singletonList(signerInfo);
+ ContentInfo contentInfo = new ContentInfo();
+ contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
+ contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
+ return Asn1DerEncoder.encode(contentInfo);
+ }
+
+ /**
+ * Picks the correct v2/v3 digest for v4 signature verification.
+ *
+ * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils.
+ */
+ public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) {
+ for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) {
+ if (contentDigests.containsKey(algo)) {
+ return contentDigests.get(algo);
+ }
+ }
+ return null;
+ }
+
+ /**
* Signer configuration.
*/
public static class SignerConfig {
@@ -1263,7 +1392,7 @@
/** Whether the APK's APK Signature Scheme signature verifies. */
public boolean verified;
- public final List<SignerInfo> signers = new ArrayList<>();
+ public final List<Result.SignerInfo> signers = new ArrayList<>();
public SigningCertificateLineage signingCertificateLineage = null;
private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
@@ -1277,7 +1406,7 @@
return true;
}
if (!signers.isEmpty()) {
- for (SignerInfo signer : signers) {
+ for (Result.SignerInfo signer : signers) {
if (signer.containsErrors()) {
return true;
}
@@ -1286,6 +1415,20 @@
return false;
}
+ public boolean containsWarnings() {
+ if (!mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (Result.SignerInfo signer : signers) {
+ if (signer.containsWarnings()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public void addError(ApkVerifier.Issue msg, Object... parameters) {
mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
}
@@ -1330,6 +1473,10 @@
return !mErrors.isEmpty();
}
+ public boolean containsWarnings() {
+ return !mWarnings.isEmpty();
+ }
+
public List<ApkVerifier.IssueWithParams> getErrors() {
return mErrors;
}
@@ -1403,4 +1550,16 @@
this.signature = signature;
}
}
+
+ public static class SigningSchemeBlockAndDigests {
+ public final Pair<byte[], Integer> signingSchemeBlock;
+ public final Map<ContentDigestAlgorithm, byte[]> digestInfo;
+
+ public SigningSchemeBlockAndDigests(
+ Pair<byte[], Integer> signingSchemeBlock,
+ Map<ContentDigestAlgorithm, byte[]> digestInfo) {
+ this.signingSchemeBlock = signingSchemeBlock;
+ this.digestInfo = digestInfo;
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
index b222474..b806d1e 100644
--- a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
@@ -16,28 +16,36 @@
package com.android.apksig.internal.apk;
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
+/** APK Signature Scheme v2 content digest algorithm. */
public enum ContentDigestAlgorithm {
/** SHA2-256 over 1 MB chunks. */
- CHUNKED_SHA256("SHA-256", 256 / 8),
+ CHUNKED_SHA256(1, "SHA-256", 256 / 8),
/** SHA2-512 over 1 MB chunks. */
- CHUNKED_SHA512("SHA-512", 512 / 8),
+ CHUNKED_SHA512(2, "SHA-512", 512 / 8),
/** SHA2-256 over 4 KB chunks for APK verity. */
- VERITY_CHUNKED_SHA256("SHA-256", 256 / 8);
+ VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
+ /** Non-chunk SHA2-256. */
+ SHA256(4, "SHA-256", 256 / 8);
+
+ private final int mId;
private final String mJcaMessageDigestAlgorithm;
private final int mChunkDigestOutputSizeBytes;
private ContentDigestAlgorithm(
- String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+ int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+ mId = id;
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
}
+ /** Returns the ID of the digest algorithm used on the APK. */
+ public int getId() {
+ return mId;
+ }
+
/**
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
* chunks by this content digest algorithm.
@@ -46,10 +54,8 @@
return mJcaMessageDigestAlgorithm;
}
- /**
- * Returns the size (in bytes) of the digest of a chunk of content.
- */
+ /** Returns the size (in bytes) of the digest of a chunk of content. */
int getChunkDigestOutputSizeBytes() {
return mChunkDigestOutputSizeBytes;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
new file mode 100644
index 0000000..2f4c3ba
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+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.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ */
+class SourceStampVerifier {
+ /** Hidden constructor to prevent instantiation. */
+ private SourceStampVerifier() {}
+
+ /**
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digest provided.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+ * maxSdkVersion]} range.
+ */
+ public static void verifyV1SourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result,
+ byte[] apkDigest,
+ byte[] sourceStampCertificateDigest,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+
+ verifySourceStampSignature(
+ apkDigest,
+ minSdkVersion,
+ maxSdkVersion,
+ sourceStampCertificate,
+ sourceStampBlockData,
+ result);
+ }
+
+ /**
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digest of multiple signature schemes provided.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+ * maxSdkVersion]} range.
+ */
+ public static void verifyV2SourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result,
+ Map<Integer, byte[]> signatureSchemeApkDigests,
+ byte[] sourceStampCertificateDigest,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+
+ // Parse signed signature schemes block.
+ ByteBuffer signedSignatureSchemes =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+ Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
+ while (signedSignatureSchemes.hasRemaining()) {
+ ByteBuffer signedSignatureScheme =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes);
+ int signatureSchemeId = signedSignatureScheme.getInt();
+ signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme);
+ }
+
+ for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
+ signatureSchemeApkDigests.entrySet()) {
+ if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+ verifySourceStampSignature(
+ signatureSchemeApkDigest.getValue(),
+ minSdkVersion,
+ maxSdkVersion,
+ sourceStampCertificate,
+ signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
+ result);
+ if (result.containsWarnings() || result.containsWarnings()) {
+ return;
+ }
+ }
+ }
+
+ private static X509Certificate verifySourceStampCertificate(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ byte[] sourceStampCertificateDigest,
+ ApkSigningBlockUtils.Result.SignerInfo result)
+ throws NoSuchAlgorithmException, ApkFormatException {
+ // Parse the SourceStamp certificate.
+ byte[] sourceStampEncodedCertificate =
+ ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+ X509Certificate sourceStampCertificate;
+ try {
+ sourceStampCertificate =
+ X509CertificateUtils.generateCertificate(
+ sourceStampEncodedCertificate, certFactory);
+ } catch (CertificateException e) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+ return null;
+ }
+ // 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.
+ sourceStampCertificate =
+ new GuaranteedEncodedFormX509Certificate(
+ sourceStampCertificate, sourceStampEncodedCertificate);
+ result.certs.add(sourceStampCertificate);
+ // Verify the SourceStamp certificate found in the signing block is the same as the
+ // SourceStamp certificate found in the APK.
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(sourceStampEncodedCertificate);
+ byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+ if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+ result.addWarning(
+ ApkVerifier.Issue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
+ ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
+ return null;
+ }
+ return sourceStampCertificate;
+ }
+
+ private static void verifySourceStampSignature(
+ byte[] apkDigest,
+ int minSdkVersion,
+ int maxSdkVersion,
+ X509Certificate sourceStampCertificate,
+ ByteBuffer signedData,
+ ApkSigningBlockUtils.Result.SignerInfo result)
+ throws ApkFormatException {
+ // Parse the signatures block and identify supported signatures
+ ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+ int signatureCount = 0;
+ List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+ int sigAlgorithmId = signature.getInt();
+ byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ continue;
+ }
+ supportedSignatures.add(
+ new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+ return;
+ }
+ }
+ if (supportedSignatures.isEmpty()) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+ // Verify signatures over digests using the SourceStamp's certificate.
+ List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+ try {
+ signaturesToVerify =
+ ApkSigningBlockUtils.getSignaturesToVerify(
+ supportedSignatures, minSdkVersion, maxSdkVersion);
+ } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ return;
+ }
+ for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+ SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ PublicKey publicKey = sourceStampCertificate.getPublicKey();
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(apkDigest);
+ byte[] sigBytes = signature.signature;
+ if (!sig.verify(sigBytes)) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+ return;
+ }
+ } catch (InvalidKeyException
+ | InvalidAlgorithmParameterException
+ | SignatureException
+ | NoSuchAlgorithmException e) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
new file mode 100644
index 0000000..dacd0be
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
+ */
+public abstract class V1SourceStampSigner {
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SourceStampSigner() {}
+
+ public static Pair<byte[], Integer> generateSourceStampBlock(
+ SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
+ throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+ if (sourceStampSignerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+ digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+ SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+ try {
+ sourceStampBlock.stampCertificate =
+ sourceStampSignerConfig.certificates.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException(
+ "Retrieving the encoded form of the stamp certificate failed", e);
+ }
+
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+ sourceStampBlock.signedDigests =
+ ApkSigningBlockUtils.generateSignaturesOverData(
+ sourceStampSignerConfig, digestBytes);
+
+ // FORMAT:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+ // * length-prefixed sequence of length-prefixed signatures:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: signature of signed data
+ byte[] sourceStampSignerBlock =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ });
+
+ // FORMAT:
+ // * length-prefixed stamp block.
+ return Pair.of(
+ encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private static final class SourceStampBlock {
+ public byte[] stampCertificate;
+ public List<Pair<Integer, byte[]>> signedDigests;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
new file mode 100644
index 0000000..8a3e776
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.V1SourceStampSigner.V1_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
+ */
+public abstract class V1SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigningBlockUtils.Result verify(
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(
+ apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ apkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result)
+ throws NoSuchAlgorithmException {
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.signers.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ getApkDigests(apkContentDigests));
+ SourceStampVerifier.verifyV1SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ digestBytes,
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+ return digests;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
new file mode 100644
index 0000000..16062bf
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
+ */
+public abstract class V2SourceStampSigner {
+
+ public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SourceStampSigner() {}
+
+ public static Pair<byte[], Integer> generateSourceStampBlock(
+ SignerConfig sourceStampSignerConfig,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
+ throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+ if (sourceStampSignerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+
+ // Extract the digests for signature schemes.
+ List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ signatureSchemeDigestInfos,
+ sourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ signatureSchemeDigestInfos,
+ sourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_JAR_SIGNATURE_SCHEME,
+ signatureSchemeDigestInfos,
+ sourceStampSignerConfig,
+ signatureSchemeDigests);
+ signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst));
+
+ SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+ try {
+ sourceStampBlock.stampCertificate =
+ sourceStampSignerConfig.certificates.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException(
+ "Retrieving the encoded form of the stamp certificate failed", e);
+ }
+
+ sourceStampBlock.signedDigests = signatureSchemeDigests;
+
+ // FORMAT:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ byte[] sourceStampSignerBlock =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ });
+
+ // FORMAT:
+ // * length-prefixed stamp block.
+ return Pair.of(
+ encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private static void getSignedDigestsFor(
+ int signatureSchemeVersion,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos,
+ SignerConfig sourceStampSignerConfig,
+ List<Pair<Integer, byte[]>> signatureSchemeDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+ return;
+ }
+
+ Map<ContentDigestAlgorithm, byte[]> digestInfo =
+ signatureSchemeDigestInfos.get(signatureSchemeVersion);
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+ digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+ }
+ digests.sort(Comparator.comparing(Pair::getFirst));
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: digest algorithm id
+ // * length-prefixed bytes: digest of the respective digest algorithm
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed digests:
+ // * uint32: signature algorithm id
+ // * length-prefixed bytes: signed digest for the respective signature algorithm
+ List<Pair<Integer, byte[]>> signedDigest =
+ ApkSigningBlockUtils.generateSignaturesOverData(
+ sourceStampSignerConfig, digestBytes);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ signatureSchemeDigests.add(
+ Pair.of(
+ signatureSchemeVersion,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedDigest)));
+ }
+
+ private static final class SourceStampBlock {
+ public byte[] stampCertificate;
+ public List<Pair<Integer, byte[]>> signedDigests;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
new file mode 100644
index 0000000..8a776fc
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 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.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
+ */
+public abstract class V2SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigningBlockUtils.Result verify(
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(
+ apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result)
+ throws NoSuchAlgorithmException {
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.signers.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+ SourceStampVerifier.verifyV2SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ getSignatureSchemeDigests(signatureSchemeApkContentDigests),
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static Map<Integer, byte[]> getSignatureSchemeDigests(
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
+ Map<Integer, byte[]> digests = new HashMap<>();
+ for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
+ signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
+ List<Pair<Integer, byte[]>> apkDigests =
+ getApkDigests(signatureSchemeApkContentDigest.getValue());
+ digests.put(
+ signatureSchemeApkContentDigest.getKey(),
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
+ }
+ return digests;
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+ return digests;
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
index f900211..89f16d5 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
@@ -16,26 +16,20 @@
package com.android.apksig.internal.apk.v1;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
+
import com.android.apksig.apk.ApkFormatException;
-import com.android.apksig.internal.asn1.Asn1DerEncoder;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.asn1.Asn1EncodingException;
-import com.android.apksig.internal.asn1.Asn1OpaqueObject;
-import com.android.apksig.internal.asn1.ber.BerEncoding;
import com.android.apksig.internal.jar.ManifestWriter;
import com.android.apksig.internal.jar.SignatureFileWriter;
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
-import com.android.apksig.internal.pkcs7.ContentInfo;
-import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
-import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
-import com.android.apksig.internal.pkcs7.Pkcs7Constants;
-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.Pair;
+
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -43,6 +37,7 @@
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.X509Certificate;
import java.util.ArrayList;
@@ -57,7 +52,6 @@
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
-import javax.security.auth.x500.X500Principal;
/**
* APK signer which uses JAR signing (aka v1 signing scheme).
@@ -487,9 +481,7 @@
return out.toByteArray();
}
- /** ASN.1 DER-encoded {@code NULL}. */
- private static final Asn1OpaqueObject ASN1_DER_NULL =
- new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
+
/**
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
@@ -541,126 +533,21 @@
e);
}
- // Wrap the signature into the JAR signature block which is created according to CMS PKCS #7
- // RFC 5652.
- // The high-level simplified structure is as follows:
- // ContentInfo
- // digestAlgorithm
- // SignedData
- // bag of certificates
- // SignerInfo
- // signing cert issuer and serial number (for locating the cert in the above bag)
- // digestAlgorithm
- // signatureAlgorithm
- // signature
+ AlgorithmIdentifier digestAlgorithmId =
+ getSignerInfoDigestAlgorithmOid(digestAlgorithm);
+ AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
try {
- SignerInfo signerInfo = new SignerInfo();
- signerInfo.version = 1;
- X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
- signerInfo.sid =
- new SignerIdentifier(
- new IssuerAndSerialNumber(
- new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
- signingCert.getSerialNumber()));
- AlgorithmIdentifier digestAlgorithmId =
- getSignerInfoDigestAlgorithmOid(digestAlgorithm);
- AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
- signerInfo.digestAlgorithm = digestAlgorithmId;
- signerInfo.signatureAlgorithm = signatureAlgorithmId;
- signerInfo.signature = ByteBuffer.wrap(signatureBytes);
-
- SignedData signedData = new SignedData();
- signedData.certificates = new ArrayList<>(signerCerts.size());
- for (X509Certificate cert : signerCerts) {
- signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
- }
- signedData.version = 1;
- signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
- signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
- signedData.signerInfos = Collections.singletonList(signerInfo);
-
- ContentInfo contentInfo = new ContentInfo();
- contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
- contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
- return Asn1DerEncoder.encode(contentInfo);
- } catch (Asn1EncodingException e) {
- throw new SignatureException("Failed to encode signature block", e);
+ return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
+ signatureBytes,
+ null,
+ signerCerts, digestAlgorithmId,
+ signatureAlgorithmId);
+ } catch (Asn1EncodingException | CertificateEncodingException ex) {
+ throw new SignatureException("Failed to encode signature block");
}
}
- /**
- * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
- * algorithm.
- */
- private static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
- DigestAlgorithm digestAlgorithm) {
- switch (digestAlgorithm) {
- case SHA1:
- return new AlgorithmIdentifier(
- V1SchemeVerifier.Signer.OID_DIGEST_SHA1, ASN1_DER_NULL);
- case SHA256:
- return new AlgorithmIdentifier(
- V1SchemeVerifier.Signer.OID_DIGEST_SHA256, ASN1_DER_NULL);
- default:
- throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
- }
- }
- /**
- * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
- * when signing with the specified key and digest algorithm.
- */
- private static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
- PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
- String keyAlgorithm = publicKey.getAlgorithm();
- String jcaDigestPrefixForSigAlg;
- switch (digestAlgorithm) {
- case SHA1:
- jcaDigestPrefixForSigAlg = "SHA1";
- break;
- case SHA256:
- jcaDigestPrefixForSigAlg = "SHA256";
- break;
- default:
- throw new IllegalArgumentException(
- "Unexpected digest algorithm: " + digestAlgorithm);
- }
- if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
- return Pair.of(
- jcaDigestPrefixForSigAlg + "withRSA",
- new AlgorithmIdentifier(V1SchemeVerifier.Signer.OID_SIG_RSA, ASN1_DER_NULL));
- } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
- AlgorithmIdentifier sigAlgId;
- switch (digestAlgorithm) {
- case SHA1:
- sigAlgId =
- new AlgorithmIdentifier(
- V1SchemeVerifier.Signer.OID_SIG_DSA, ASN1_DER_NULL);
- break;
- case SHA256:
- // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
- // 21 and higher. However, there are two ways to specify their SignedData
- // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
- // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
- // the former.
- sigAlgId =
- new AlgorithmIdentifier(
- V1SchemeVerifier.Signer.OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
- break;
- default:
- throw new IllegalArgumentException(
- "Unexpected digest algorithm: " + digestAlgorithm);
- }
- return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
- } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
- return Pair.of(
- jcaDigestPrefixForSigAlg + "withECDSA",
- new AlgorithmIdentifier(
- V1SchemeVerifier.Signer.OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
- } else {
- throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
- }
- }
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
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 47d5b01..111ac71 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
@@ -16,6 +16,12 @@
package com.android.apksig.internal.apk.v1;
+import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
+import static com.android.apksig.internal.x509.Certificate.findCertificate;
+import static com.android.apksig.internal.x509.Certificate.parseCertificates;
+
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.ApkVerifier.IssueWithParams;
import com.android.apksig.apk.ApkFormatException;
@@ -27,18 +33,15 @@
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.oid.OidConstants;
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.X509CertificateUtils;
-import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.InclusiveIntRange;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -48,7 +51,6 @@
import com.android.apksig.zip.ZipFormatException;
import java.io.IOException;
-import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
@@ -74,8 +76,6 @@
import java.util.StringTokenizer;
import java.util.jar.Attributes;
-import javax.security.auth.x500.X500Principal;
-
/**
* APK verifier which uses JAR signing (aka v1 signing scheme).
*
@@ -606,13 +606,13 @@
desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
String digestAlgorithmUserFriendly =
- OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+ OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
digestAlgorithmOid);
if (digestAlgorithmUserFriendly == null) {
digestAlgorithmUserFriendly = digestAlgorithmOid;
}
String signatureAlgorithmUserFriendly =
- OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+ OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
signatureAlgorithmOid);
if (signatureAlgorithmUserFriendly == null) {
signatureAlgorithmUserFriendly = signatureAlgorithmOid;
@@ -768,42 +768,7 @@
return signingCertificate;
}
- private static List<X509Certificate> parseCertificates(
- List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
- if (encodedCertificates.isEmpty()) {
- return Collections.emptyList();
- }
- 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 = X509CertificateUtils.generateCertificate(encodedForm);
- } catch (CertificateException e) {
- throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
- }
- // Wrap the cert so that the result's getEncoded returns exactly the original
- // encoded form. Without this, getEncoded may return a different form from what was
- // stored in the signature. This is because some X509Certificate(Factory)
- // implementations re-encode certificates and/or some implementations of
- // X509Certificate.getEncoded() re-encode certificates.
- certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
- result.add(certificate);
- }
- return result;
- }
-
- public static X509Certificate findCertificate(
- Collection<X509Certificate> certs, SignerIdentifier id) {
- for (X509Certificate cert : certs) {
- if (isMatchingCerticicate(cert, id)) {
- return cert;
- }
- }
- return null;
- }
public static List<X509Certificate> getCertificateChain(
List<X509Certificate> certs, X509Certificate leaf) {
@@ -832,495 +797,8 @@
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";
- static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
- private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
- static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
- private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
- private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
-
- static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
- private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
- private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
- private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
- private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
- private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
- private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
-
- static final String OID_SIG_DSA = "1.2.840.10040.4.1";
- private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
- private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
- static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
- static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
- static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
-
- static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
- private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
- private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
- private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
- private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
- private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
-
- private static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
- new HashMap<>();
- {
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_RSA,
- InclusiveIntRange.from(0));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_RSA,
- InclusiveIntRange.from(0));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.from(0));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_RSA,
- InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(21, 21));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_RSA,
- InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.fromTo(21, 21));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_RSA,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_RSA,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
- InclusiveIntRange.fromTo(21, 21));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
- InclusiveIntRange.from(21));
-
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_DSA,
- InclusiveIntRange.from(0));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.from(9));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_DSA,
- InclusiveIntRange.from(22));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_DSA,
- InclusiveIntRange.from(22));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.from(21));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
- InclusiveIntRange.from(18));
-
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.from(18));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.from(21));
- addSupportedSigAlg(
- OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
-
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
- InclusiveIntRange.fromTo(21, 23));
- addSupportedSigAlg(
- OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
- InclusiveIntRange.from(21));
- }
-
- private static void addSupportedSigAlg(
- String digestAlgorithmOid,
- String signatureAlgorithmOid,
- InclusiveIntRange... supportedApiLevels) {
- SUPPORTED_SIG_ALG_OIDS.put(
- digestAlgorithmOid + "with" + signatureAlgorithmOid,
- Arrays.asList(supportedApiLevels));
- }
-
- private List<InclusiveIntRange> getSigAlgSupportedApiLevels(
- String digestAlgorithmOid,
- String signatureAlgorithmOid) {
- List<InclusiveIntRange> result =
- SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
- 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_SHA384_WITH_DSA, "SHA-384 with DSA");
- OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 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,
@@ -1827,13 +1305,10 @@
Collections.sort(
cdRecordsSortedByLocalFileHeaderOffset,
CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
- Set<String> manifestEntryNamesMissingFromApk =
- new HashSet<>(entryNameToManifestSection.keySet());
List<Signer> firstSignedEntrySigners = null;
String firstSignedEntryName = null;
for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
String entryName = cdRecord.getName();
- manifestEntryNamesMissingFromApk.remove(entryName);
if (!isJarEntryDigestNeededInManifest(entryName)) {
continue;
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
index f325f8b..e812c3f 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
@@ -80,14 +80,12 @@
* provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
- * AndroidManifest.xml minSdkVersion attribute).
- *
- * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
- * APK Signature Scheme v2
+ * AndroidManifest.xml minSdkVersion attribute).
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+ * Signature Scheme v2
*/
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
- throws InvalidKeyException {
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -100,7 +98,7 @@
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
@@ -113,7 +111,7 @@
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
@@ -124,7 +122,7 @@
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
@@ -138,28 +136,30 @@
}
}
- public static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
- RunnablesExecutor executor,
- DataSource beforeCentralDir,
- DataSource centralDir,
- DataSource eocd,
- List<SignerConfig> signerConfigs,
- boolean v3SigningEnabled)
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV2Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ boolean v3SigningEnabled)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
- Pair<List<SignerConfig>,
- Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+ Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
- return generateApkSignatureSchemeV2Block(
- digestInfo.getFirst(), digestInfo.getSecond(),v3SigningEnabled);
+ return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV2Block(
+ digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled),
+ digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
@@ -178,17 +178,19 @@
signerBlocks.add(signerBlock);
}
- return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
- }), APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ return Pair.of(
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+ }),
+ APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
@@ -211,7 +213,9 @@
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
- contentDigestAlgorithm + " content digest for " + signatureAlgorithm
+ contentDigestAlgorithm
+ + " content digest for "
+ + signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
@@ -230,12 +234,15 @@
// * uint32: ID
// * (length - 4) bytes: value
- signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
- encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
- encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
- signedData.additionalAttributes,
- new byte[0],
- });
+ signer.signedData =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedData.digests),
+ encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
+ signedData.additionalAttributes,
+ new byte[0],
+ });
signer.publicKey = encodedPublicKey;
signer.signatures = new ArrayList<>();
signer.signatures =
@@ -290,5 +297,4 @@
public byte[] additionalAttributes;
}
}
-
}
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 0ef74a6..9b821a7 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
@@ -175,7 +175,7 @@
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
- private static void parseSigners(
+ public static void parseSigners(
ByteBuffer apkSignatureSchemeV2Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
Map<Integer, String> supportedApkSigSchemeNames,
@@ -294,7 +294,7 @@
ApkSigningBlockUtils.getSignaturesToVerify(
supportedSignatures, minSdkVersion, maxSdkVersion);
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
- result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
+ result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
return;
}
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
index 722b304..56ab60e 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -52,10 +52,9 @@
* Signature Scheme v2 goals.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
- *
- * <p> The main contribution of APK Signature Scheme v3 is the introduction of the
- * {@link SigningCertificateLineage}, which enables an APK to change its signing
- * certificate as long as it can prove the new siging certificate was signed by the old.
+ * <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
+ * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
+ * it can prove the new siging certificate was signed by the old.
*/
public abstract class V3SchemeSigner {
@@ -69,14 +68,12 @@
* provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
- * AndroidManifest.xml minSdkVersion attribute).
- *
- * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
- * APK Signature Scheme v3
+ * AndroidManifest.xml minSdkVersion attribute).
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+ * Signature Scheme v3
*/
- public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
- PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
- throws InvalidKeyException {
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -89,7 +86,7 @@
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
@@ -102,7 +99,7 @@
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
@@ -113,7 +110,7 @@
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
- if (apkSigningBlockPaddingSupported) {
+ if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
@@ -127,28 +124,28 @@
}
}
- public static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
- RunnablesExecutor executor,
- DataSource beforeCentralDir,
- DataSource centralDir,
- DataSource eocd,
- List<SignerConfig> signerConfigs)
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV3Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
- Pair<List<SignerConfig>,
- Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+ Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
- return generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond());
+ return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
+ digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
- List<SignerConfig> signerConfigs,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
-
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
@@ -164,16 +161,17 @@
signerBlocks.add(signerBlock);
}
- return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
- new byte[][] {
- encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
- }), APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ return Pair.of(
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+ }),
+ APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
private static byte[] generateSignerBlock(
- SignerConfig signerConfig,
- Map<ContentDigestAlgorithm, byte[]> contentDigests)
- throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
@@ -196,7 +194,9 @@
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
- contentDigestAlgorithm + " content digest for " + signatureAlgorithm
+ contentDigestAlgorithm
+ + " content digest for "
+ + signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
@@ -216,7 +216,6 @@
signer.signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
-
return encodeSigner(signer);
}
@@ -236,12 +235,7 @@
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
- int payloadSize =
- signedData.length
- + 4
- + 4
- + signatures.length
- + publicKey.length;
+ int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
@@ -277,12 +271,7 @@
// * (length - 4) bytes: value
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
// * length-prefixed roof-of-rotation structure
- int payloadSize =
- digests.length
- + certs.length
- + 4
- + 4
- + attributes.length;
+ int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
@@ -321,5 +310,4 @@
public byte[] additionalAttributes;
}
}
-
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index f263323..659d379 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -53,6 +53,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -228,7 +229,7 @@
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
- private static void parseSigners(
+ public static void parseSigners(
ByteBuffer apkSignatureSchemeV3Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
@@ -521,5 +522,4 @@
}
}
}
-
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
new file mode 100644
index 0000000..73ba46f
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.v2.V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
+ * The fields:
+ * <p>
+ * 1. hashingInfo - verity root hash and hashing info,
+ * 2. signingInfo - certificate, public key and signature,
+ * For more details see V4Signature.
+ * </p>
+ * (optional) verityTree: integer size prepended bytes of the verity hash tree.
+ * <p>
+ * TODO(schfan): Add v4 unit tests
+ */
+public abstract class V4SchemeSigner {
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private V4SchemeSigner() {
+ }
+
+ /**
+ * Based on a public key, return a signing algorithm that supports verity.
+ */
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+ throws InvalidKeyException {
+ List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ signingKey, minSdkVersion,
+ apkSigningBlockPaddingSupported);
+ // Keeping only supported algorithms.
+ for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+ final SignatureAlgorithm algorithm = iter.next();
+ if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
+ iter.remove();
+ }
+ }
+ return algorithms;
+ }
+
+ /**
+ * Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
+ * output file.
+ */
+ public static void generateV4Signature(
+ DataSource apkContent, SignerConfig signerConfig, File outputFile)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+ Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
+ try (final OutputStream output = new FileOutputStream(outputFile)) {
+ pair.getFirst().writeTo(output);
+ V4Signature.writeBytes(output, pair.getSecond());
+ } catch (IOException e) {
+ outputFile.delete();
+ throw e;
+ }
+ }
+
+ /** Generate v4 signature and hash tree for a given APK. */
+ public static Pair<V4Signature, byte[]> generateV4Signature(
+ DataSource apkContent,
+ SignerConfig signerConfig)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+ // Salt has to stay empty for fs-verity compatibility.
+ final byte[] salt = null;
+ // Not used by apksigner.
+ final byte[] additionalData = null;
+
+ final long fileSize = apkContent.size();
+
+ // Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512).
+ final byte[] apkDigest = getApkDigest(apkContent);
+
+ // Obtaining the merkle tree and the root hash in verity format.
+ ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ final ContentDigestAlgorithm verityContentDigestAlgorithm =
+ verityContentDigestInfo.contentDigestAlgorithm;
+ final byte[] rootHash = verityContentDigestInfo.rootHash;
+ final byte[] tree = verityContentDigestInfo.tree;
+
+ final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
+ verityContentDigestAlgorithm);
+ final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
+ hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
+ salt, rootHash);
+
+ // Generating SigningInfo and combining everything into V4Signature.
+ final V4Signature signature;
+ try {
+ signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData,
+ fileSize);
+ } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
+ throw new InvalidKeyException("Signer failed", e);
+ }
+
+ return Pair.of(signature, tree);
+ }
+
+ private static V4Signature generateSignature(
+ SignerConfig signerConfig,
+ V4Signature.HashingInfo hashingInfo,
+ byte[] apkDigest, byte[] additionaData, long fileSize)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+ CertificateEncodingException {
+ if (signerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+ if (signerConfig.certificates.size() != 1) {
+ throw new CertificateEncodingException("Should only have one certificate");
+ }
+
+ // Collecting data for signing.
+ final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+ final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+ final byte[] encodedCertificate = encodedCertificates.get(0);
+
+ final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
+
+ final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+ signingInfoNoSignature);
+
+ // Signing.
+ final List<Pair<Integer, byte[]>> signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
+ if (signatures.size() != 1) {
+ throw new SignatureException("Should only be one signature generated");
+ }
+
+ final int signatureAlgorithmId = signatures.get(0).getFirst();
+ final byte[] signature = signatures.get(0).getSecond();
+
+ final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
+ signature);
+
+ return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+ signingInfo.toByteArray());
+ }
+
+ // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
+ private static byte[] getApkDigest(DataSource apk) throws IOException {
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new IOException("Malformed APK: not a ZIP archive", e);
+ }
+
+ final SignatureException v3Exception;
+ try {
+ return getBestV3Digest(apk, zipSections);
+ } catch (SignatureException e) {
+ v3Exception = e;
+ }
+
+ final SignatureException v2Exception;
+ try {
+ return getBestV2Digest(apk, zipSections);
+ } catch (SignatureException e) {
+ v2Exception = e;
+ }
+
+ throw new IOException(
+ "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+ + v2Exception);
+ }
+
+ private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws SignatureException {
+ final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ try {
+ final SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
+ final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+ V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
+ result);
+ } catch (Exception e) {
+ throw new SignatureException("Failed to extract and parse v3 block", e);
+ }
+
+ if (result.signers.size() != 1) {
+ throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+ }
+
+ ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+ if (signer.containsErrors()) {
+ throw new SignatureException("Parsing failed: " + signer.getErrors());
+ }
+
+ final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+ result.signers.get(0).contentDigests;
+ return pickBestDigest(contentDigests);
+ }
+
+ private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws SignatureException {
+ final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ try {
+ final SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
+ final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
+ V2SchemeVerifier.parseSigners(
+ apkSignatureSchemeV2Block,
+ contentDigestsToVerify,
+ Collections.emptyMap(),
+ foundApkSigSchemeIds,
+ Integer.MAX_VALUE,
+ Integer.MAX_VALUE,
+ result);
+ } catch (Exception e) {
+ throw new SignatureException("Failed to extract and parse v2 block", e);
+ }
+
+ if (result.signers.size() != 1) {
+ throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+ }
+
+ ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+ if (signer.containsErrors()) {
+ throw new SignatureException("Parsing failed: " + signer.getErrors());
+ }
+
+ final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+ signer.contentDigests;
+ return pickBestDigest(contentDigests);
+ }
+
+ private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
+ if (contentDigests == null || contentDigests.isEmpty()) {
+ throw new SignatureException("Should have at least one digest");
+ }
+
+ int bestAlgorithmOrder = -1;
+ byte[] bestDigest = null;
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+ final SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ final ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ if (!isSupported(contentDigestAlgorithm, true)) {
+ continue;
+ }
+ final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
+ if (bestAlgorithmOrder < algorithmOrder) {
+ bestAlgorithmOrder = algorithmOrder;
+ bestDigest = contentDigest.getValue();
+ }
+ }
+ if (bestDigest == null) {
+ throw new SignatureException("Failed to find a supported digest in the source APK");
+ }
+ return bestDigest;
+ }
+
+ // Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest
+ // verification in framework works.
+ public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
+ switch (contentDigestAlgorithm) {
+ case CHUNKED_SHA256:
+ return 0;
+ case VERITY_CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 2;
+ }
+ return -1;
+ }
+
+ private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
+ boolean forV3Digest) {
+ if (contentDigestAlgorithm == null) {
+ return false;
+ }
+ if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+ || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
+ || (forV3Digest
+ && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
+ throws NoSuchAlgorithmException {
+ switch (algorithm) {
+ case VERITY_CHUNKED_SHA256:
+ return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
+ V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
+ default:
+ throw new NoSuchAlgorithmException(
+ "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
+ }
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
new file mode 100644
index 0000000..0a8484b
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+/**
+ * APK Signature Scheme V4 verifier.
+ * <p>
+ * Verifies the serialized V4Signature file against an APK.
+ */
+public abstract class V4SchemeVerifier {
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private V4SchemeVerifier() {
+ }
+
+ /**
+ * <p>
+ * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
+ * signature block against the raw root hash bytes in the proto field 3) verifies that the raw
+ * root hash matches with the actual hash tree root of the give APK 4) if the file contains a
+ * verity tree, verifies that it matches with the actual verity tree computed from the given
+ * APK.
+ * </p>
+ */
+ public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
+ throws IOException, NoSuchAlgorithmException {
+ final V4Signature signature;
+ final byte[] tree;
+ try (InputStream input = new FileInputStream(v4SignatureFile)) {
+ signature = V4Signature.readFrom(input);
+ tree = V4Signature.readBytes(input);
+ }
+
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+
+ if (signature == null) {
+ result.addError(Issue.V4_SIG_NO_SIGNATURES,
+ "Signature file does not contain a v4 signature.");
+ return result;
+ }
+
+ if (signature.version != V4Signature.CURRENT_VERSION) {
+ result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
+ V4Signature.CURRENT_VERSION);
+ }
+
+ V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+ signature.hashingInfo);
+ V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+ signature.signingInfo);
+
+ final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+
+ // First, verify the signature over signedData.
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
+ signingInfo, signedData);
+ result.signers.add(signerInfo);
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Second, check if the root hash and the tree are correct.
+ verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
+ if (!result.containsErrors()) {
+ result.verified = true;
+ }
+
+ return result;
+ }
+
+ /**
+ * Parses the provided signature block and populates the {@code result}.
+ * <p>
+ * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
+ * contained in the signature block. This method adds one or more errors to the {@code result}.
+ */
+ private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
+ V4Signature.SigningInfo signingInfo,
+ final byte[] signedData) throws NoSuchAlgorithmException {
+ final ApkSigningBlockUtils.Result.SignerInfo result =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.index = 0;
+
+ final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
+ final byte[] sigBytes = signingInfo.signature;
+ result.signatures.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ return result;
+ }
+
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+
+ String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+
+ final byte[] publicKeyBytes = signingInfo.publicKey;
+ PublicKey publicKey;
+ try {
+ publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
+ new X509EncodedKeySpec(publicKeyBytes));
+ } catch (Exception e) {
+ result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
+ return result;
+ }
+
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ if (!sig.verify(sigBytes)) {
+ result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+ return result;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return result;
+ }
+
+ if (signingInfo.certificate == null) {
+ result.addError(Issue.V4_SIG_NO_CERTIFICATE);
+ return result;
+ }
+
+ final X509Certificate certificate;
+ try {
+ // 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.
+ certificate = new GuaranteedEncodedFormX509Certificate(
+ X509CertificateUtils.generateCertificate(signingInfo.certificate),
+ signingInfo.certificate);
+ } catch (CertificateException e) {
+ result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
+ return result;
+ }
+ result.certs.add(certificate);
+
+ byte[] certificatePublicKeyBytes;
+ try {
+ certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+ certificate.getPublicKey());
+ } catch (InvalidKeyException e) {
+ System.out.println("Caught an exception encoding the public key: " + e);
+ e.printStackTrace();
+ certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+ }
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+ ApkSigningBlockUtils.toHex(publicKeyBytes));
+ return result;
+ }
+
+ // Add apk digest from the file to the result.
+ ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
+ new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+ 0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
+ result.contentDigests.add(contentDigest);
+
+ return result;
+ }
+
+ private static void verifyRootHashAndTree(DataSource apkContent,
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
+ byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
+ ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
+ final byte[] actualDigest = actualContentDigestInfo.rootHash;
+ final byte[] actualTree = actualContentDigestInfo.tree;
+
+ if (!Arrays.equals(expectedDigest, actualDigest)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
+ }
+ // Only check verity tree if it is not empty
+ if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
+ }
+
+ signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
new file mode 100644
index 0000000..e36ed60
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020 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.apk.v4;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class V4Signature {
+ public static final int CURRENT_VERSION = 2;
+
+ public static final int HASHING_ALGORITHM_SHA256 = 1;
+ public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+
+ public static class HashingInfo {
+ public final int hashAlgorithm; // only 1 == SHA256 supported
+ public final byte log2BlockSize; // only 12 (block size 4096) supported now
+ public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
+ public final byte[] rawRootHash; // salted digest of the first Merkle tree page
+
+ HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
+ this.hashAlgorithm = hashAlgorithm;
+ this.log2BlockSize = log2BlockSize;
+ this.salt = salt;
+ this.rawRootHash = rawRootHash;
+ }
+
+ static HashingInfo fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ final int hashAlgorithm = buffer.getInt();
+ final byte log2BlockSize = buffer.get();
+ byte[] salt = readBytes(buffer);
+ byte[] rawRootHash = readBytes(buffer);
+ return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
+ }
+
+ byte[] toByteArray() {
+ final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+ + bytesSize(this.rawRootHash);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(this.hashAlgorithm);
+ buffer.put(this.log2BlockSize);
+ writeBytes(buffer, this.salt);
+ writeBytes(buffer, this.rawRootHash);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfo {
+ public final byte[] apkDigest; // used to match with the corresponding APK
+ public final byte[] certificate; // ASN.1 DER form
+ public final byte[] additionalData; // a free-form binary data blob
+ public final byte[] publicKey; // ASN.1 DER, must match the certificate
+ public final int signatureAlgorithmId; // see the APK v2 doc for the list
+ public final byte[] signature;
+
+ SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
+ byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+ this.apkDigest = apkDigest;
+ this.certificate = certificate;
+ this.additionalData = additionalData;
+ this.publicKey = publicKey;
+ this.signatureAlgorithmId = signatureAlgorithmId;
+ this.signature = signature;
+ }
+
+ static SigningInfo fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ byte[] apkDigest = readBytes(buffer);
+ byte[] certificate = readBytes(buffer);
+ byte[] additionalData = readBytes(buffer);
+ byte[] publicKey = readBytes(buffer);
+ int signatureAlgorithmId = buffer.getInt();
+ byte[] signature = readBytes(buffer);
+ return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
+ signatureAlgorithmId, signature);
+ }
+
+ byte[] toByteArray() {
+ final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
+ this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+ + bytesSize(this.signature);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ writeBytes(buffer, this.apkDigest);
+ writeBytes(buffer, this.certificate);
+ writeBytes(buffer, this.additionalData);
+ writeBytes(buffer, this.publicKey);
+ buffer.putInt(this.signatureAlgorithmId);
+ writeBytes(buffer, this.signature);
+ return buffer.array();
+ }
+ }
+
+ public final int version; // Always 2 for now.
+ public final byte[] hashingInfo;
+ public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
+
+ V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
+ this.version = version;
+ this.hashingInfo = hashingInfo;
+ this.signingInfo = signingInfo;
+ }
+
+ static V4Signature readFrom(InputStream stream) throws IOException {
+ final int version = readIntLE(stream);
+ if (version != CURRENT_VERSION) {
+ throw new IOException("Invalid signature version.");
+ }
+ final byte[] hashingInfo = readBytes(stream);
+ final byte[] signingInfo = readBytes(stream);
+ return new V4Signature(version, hashingInfo, signingInfo);
+ }
+
+ public void writeTo(OutputStream stream) throws IOException {
+ writeIntLE(stream, this.version);
+ writeBytes(stream, this.hashingInfo);
+ writeBytes(stream, this.signingInfo);
+ }
+
+ static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+ final int size =
+ 4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
+ hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
+ signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
+ signingInfo.additionalData);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(size);
+ buffer.putLong(fileSize);
+ buffer.putInt(hashingInfo.hashAlgorithm);
+ buffer.put(hashingInfo.log2BlockSize);
+ writeBytes(buffer, hashingInfo.salt);
+ writeBytes(buffer, hashingInfo.rawRootHash);
+ writeBytes(buffer, signingInfo.apkDigest);
+ writeBytes(buffer, signingInfo.certificate);
+ writeBytes(buffer, signingInfo.additionalData);
+ return buffer.array();
+ }
+
+ // Utility methods.
+ static int bytesSize(byte[] bytes) {
+ return 4/*length*/ + (bytes == null ? 0 : bytes.length);
+ }
+
+ static void readFully(InputStream stream, byte[] buffer) throws IOException {
+ int len = buffer.length;
+ int n = 0;
+ while (n < len) {
+ int count = stream.read(buffer, n, len - n);
+ if (count < 0) {
+ throw new EOFException();
+ }
+ n += count;
+ }
+ }
+
+ static int readIntLE(InputStream stream) throws IOException {
+ final byte[] buffer = new byte[4];
+ readFully(stream, buffer);
+ return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ }
+
+ static void writeIntLE(OutputStream stream, int v) throws IOException {
+ final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
+ stream.write(buffer);
+ }
+
+ static byte[] readBytes(InputStream stream) throws IOException {
+ try {
+ final int size = readIntLE(stream);
+ final byte[] bytes = new byte[size];
+ readFully(stream, bytes);
+ return bytes;
+ } catch (EOFException ignored) {
+ return null;
+ }
+ }
+
+ static byte[] readBytes(ByteBuffer buffer) throws IOException {
+ if (buffer.remaining() < 4) {
+ throw new EOFException();
+ }
+ final int size = buffer.getInt();
+ if (buffer.remaining() < size) {
+ throw new EOFException();
+ }
+ final byte[] bytes = new byte[size];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
+ if (bytes == null) {
+ writeIntLE(stream, 0);
+ return;
+ }
+ writeIntLE(stream, bytes.length);
+ stream.write(bytes);
+ }
+
+ static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+ if (bytes == null) {
+ buffer.putInt(0);
+ return;
+ }
+ buffer.putInt(bytes.length);
+ buffer.put(bytes);
+ }
+}
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 d4a6fb6..160dc4e 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -507,23 +507,22 @@
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
- try {
- return value.intValueExact();
- } catch (ArithmeticException e) {
+ if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
- String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e);
+ String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
}
+ return value.intValue();
}
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
- try {
- return value.longValueExact();
- } catch (ArithmeticException e) {
+ if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
- String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value),
- e);
+ String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
}
+ return value.longValue();
}
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
index 22a432f..901f5f3 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
@@ -590,4 +590,7 @@
"Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
}
}
+ /** ASN.1 DER-encoded {@code NULL}. */
+ public static final Asn1OpaqueObject ASN1_DER_NULL =
+ new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
}
diff --git a/src/main/java/com/android/apksig/internal/oid/OidConstants.java b/src/main/java/com/android/apksig/internal/oid/OidConstants.java
new file mode 100644
index 0000000..d80cbaa
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/oid/OidConstants.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2020 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.oid;
+
+import com.android.apksig.internal.util.InclusiveIntRange;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OidConstants {
+ public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
+ public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
+ public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
+ public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
+ public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
+ public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
+
+ public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
+ public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
+ public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
+ public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
+ public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
+ public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
+ public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
+
+ public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
+ public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
+ public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
+ public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+ public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+ public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
+
+ public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
+ public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
+ public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
+ public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
+ public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
+ public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
+
+ public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
+ new HashMap<>();
+ static {
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_RSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_RSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.from(21));
+
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_DSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.from(9));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_DSA,
+ InclusiveIntRange.from(22));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_DSA,
+ InclusiveIntRange.from(22));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.from(21));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ }
+
+ public static void addSupportedSigAlg(
+ String digestAlgorithmOid,
+ String signatureAlgorithmOid,
+ InclusiveIntRange... supportedApiLevels) {
+ SUPPORTED_SIG_ALG_OIDS.put(
+ digestAlgorithmOid + "with" + signatureAlgorithmOid,
+ Arrays.asList(supportedApiLevels));
+ }
+
+ public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
+ String digestAlgorithmOid,
+ String signatureAlgorithmOid) {
+ List<InclusiveIntRange> result =
+ SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
+ return (result != null) ? result : Collections.emptyList();
+ }
+
+ public 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_SHA384_WITH_DSA, "SHA-384 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 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);
+ }
+ }
+
+ public 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");
+ }
+
+ public 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 OidConstants() {}
+}
diff --git a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
index 39bce94..c27c487 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
@@ -16,10 +16,27 @@
package com.android.apksig.internal.pkcs7;
+import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
+
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
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.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
/**
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
@@ -39,4 +56,114 @@
this.algorithm = algorithmOid;
this.parameters = parameters;
}
+
+ /**
+ * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
+ * algorithm.
+ */
+ public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
+ DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
+ case SHA256:
+ return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
+ }
+ throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+ }
+
+ /**
+ * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
+ * when signing with the specified key and digest algorithm.
+ */
+ public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
+ PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+ String keyAlgorithm = publicKey.getAlgorithm();
+ String jcaDigestPrefixForSigAlg;
+ switch (digestAlgorithm) {
+ case SHA1:
+ jcaDigestPrefixForSigAlg = "SHA1";
+ break;
+ case SHA256:
+ jcaDigestPrefixForSigAlg = "SHA256";
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withRSA",
+ new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ AlgorithmIdentifier sigAlgId;
+ switch (digestAlgorithm) {
+ case SHA1:
+ sigAlgId =
+ new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
+ break;
+ case SHA256:
+ // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
+ // 21 and higher. However, there are two ways to specify their SignedData
+ // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
+ // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
+ // the former.
+ sigAlgId =
+ new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withECDSA",
+ new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ public 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 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;
+ }
}
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 615d251..4ef67c7 100644
--- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
+++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
@@ -47,4 +47,7 @@
/** Android P. */
public static final int P = 28;
+
+ /** Android R. */
+ public static final int R = 30;
}
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 75497a7..81026ba 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -16,6 +16,8 @@
package com.android.apksig.internal.util;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
@@ -24,31 +26,71 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.util.ArrayList;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
/**
* VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
* The root hash can be used on device for on-access verification. The tree itself is reproducible
* on device, and is not shipped with the APK.
*/
-public class VerityTreeBuilder {
+public class VerityTreeBuilder implements AutoCloseable {
- /** Maximum size (in bytes) of each node of the tree. */
+ /**
+ * Maximum size (in bytes) of each node of the tree.
+ */
private final static int CHUNK_SIZE = 4096;
+ /**
+ * Maximum parallelism while calculating digests.
+ */
+ private final static int DIGEST_PARALLELISM = Math.min(32,
+ Runtime.getRuntime().availableProcessors());
+ /**
+ * Queue size.
+ */
+ private final static int MAX_OUTSTANDING_CHUNKS = 4;
+ /**
+ * Typical prefetch size.
+ */
+ private final static int MAX_PREFETCH_CHUNKS = 1024;
+ /**
+ * Minimum chunks to be processed by a single worker task.
+ */
+ private final static int MIN_CHUNKS_PER_WORKER = 8;
- /** Digest algorithm (JCA Digest algorithm name) used in the tree. */
+ /**
+ * Digest algorithm (JCA Digest algorithm name) used in the tree.
+ */
private final static String JCA_ALGORITHM = "SHA-256";
- /** Optional salt to apply before each digestion. */
+ /**
+ * Optional salt to apply before each digestion.
+ */
private final byte[] mSalt;
private final MessageDigest mMd;
+ private final ExecutorService mExecutor =
+ new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
+ 0L, MILLISECONDS,
+ new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+
public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
mSalt = salt;
- mMd = MessageDigest.getInstance(JCA_ALGORITHM);
+ mMd = getNewMessageDigest();
+ }
+
+ @Override
+ public void close() {
+ mExecutor.shutdownNow();
}
/**
@@ -81,6 +123,14 @@
/**
* Returns the root hash of the verity tree built from the data source.
+ */
+ public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+ ByteBuffer verityBuffer = generateVerityTree(fileSource);
+ return getRootHashFromTree(verityBuffer);
+ }
+
+ /**
+ * Returns the byte buffer that contains the whole verity tree.
*
* The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
* input file. If the total size is larger than 4 KB, take this level as input and repeat the
@@ -91,10 +141,8 @@
*
* The tree is currently stored only in memory and is never written out. Nevertheless, it is
* the actual verity tree format on disk, and is supposed to be re-generated on device.
- *
- * This is package-private for testing purpose.
*/
- byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+ public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException {
int digestSize = mMd.getDigestLength();
// Calculate the summed area table of level size. In other word, this is the offset
@@ -113,7 +161,7 @@
digestDataByChunks(src, middleBufferSink);
} else {
src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
- levelOffset[i + 1], levelOffset[i + 2]));
+ levelOffset[i + 1], levelOffset[i + 2]));
digestDataByChunks(src, middleBufferSink);
}
@@ -125,8 +173,13 @@
middleBufferSink.consume(padding, 0, padding.length);
}
}
+ return verityBuffer;
+ }
- // Finally, calculate the root hash from the top level (only page).
+ /**
+ * Returns the digested root hash from the top level (only page) of a verity tree.
+ */
+ public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException {
ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE);
return saltedDigest(firstPage);
}
@@ -167,37 +220,70 @@
* chunk before digesting.
*/
private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
- long size = dataSource.size();
- long offset = 0;
- for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
- ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE);
- dataSource.copyTo(offset, CHUNK_SIZE, buffer);
+ final long size = dataSource.size();
+ final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
+
+ /** Single IO operation size, in chunks. */
+ final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
+
+ final byte[][] hashes = new byte[chunks][];
+
+ Phaser tasks = new Phaser(1);
+
+ // Reading the input file as fast as we can.
+ final long maxReadSize = ioSizeChunks * CHUNK_SIZE;
+
+ long readOffset = 0;
+ int startChunkIndex = 0;
+ while (readOffset < size) {
+ final long readLimit = Math.min(readOffset + maxReadSize, size);
+ final int readSize = (int) (readLimit - readOffset);
+ final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE);
+
+ // Overllocating to zero-pad last chunk.
+ // With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB.
+ final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE);
+ dataSource.copyTo(readOffset, readSize, buffer);
buffer.rewind();
- byte[] hash = saltedDigest(buffer);
- dataSink.consume(hash, 0, hash.length);
+
+ final int readChunkIndex = startChunkIndex;
+ Runnable task = () -> {
+ final MessageDigest md = cloneMessageDigest();
+ for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex;
+ offset < finish; offset += CHUNK_SIZE, ++chunkIndex) {
+ ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE);
+ hashes[chunkIndex] = saltedDigest(md, chunk);
+ }
+ tasks.arriveAndDeregister();
+ };
+ tasks.register();
+ mExecutor.execute(task);
+
+ startChunkIndex += bufferSizeChunks;
+ readOffset += readSize;
}
- // Send the last incomplete chunk with 0 padding to the sink at once.
- int remaining = (int) (size % CHUNK_SIZE);
- if (remaining > 0) {
- ByteBuffer buffer;
- buffer = ByteBuffer.allocate(CHUNK_SIZE); // initialized to 0.
- dataSource.copyTo(offset, remaining, buffer);
- buffer.rewind();
+ // Waiting for the tasks to complete.
+ tasks.arriveAndAwaitAdvance();
- byte[] hash = saltedDigest(buffer);
+ // Streaming hashes back.
+ for (byte[] hash : hashes) {
dataSink.consume(hash, 0, hash.length);
}
}
- /** Returns the digest of data with salt prepanded. */
+ /** Returns the digest of data with salt prepended. */
private byte[] saltedDigest(ByteBuffer data) {
- mMd.reset();
+ return saltedDigest(mMd, data);
+ }
+
+ private byte[] saltedDigest(MessageDigest md, ByteBuffer data) {
+ md.reset();
if (mSalt != null) {
- mMd.update(mSalt);
+ md.update(mSalt);
}
- mMd.update(data);
- return mMd.digest();
+ md.update(data);
+ return md.digest();
}
/** Divides a number and round up to the closest integer. */
@@ -213,4 +299,27 @@
b.position(begin);
return b.slice();
}
+
+ /**
+ * Obtains a new instance of the message digest algorithm.
+ */
+ private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(JCA_ALGORITHM);
+ }
+
+ /**
+ * Clones the existing message digest, or creates a new instance if clone is unavailable.
+ */
+ private MessageDigest cloneMessageDigest() {
+ try {
+ return (MessageDigest) mMd.clone();
+ } catch (CloneNotSupportedException ignored) {
+ try {
+ return getNewMessageDigest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(
+ "Failed to obtain an instance of a previously available message digest", e);
+ }
+ }
+ }
}
diff --git a/src/main/java/com/android/apksig/internal/x509/Certificate.java b/src/main/java/com/android/apksig/internal/x509/Certificate.java
index abb3c15..70ff6a1 100644
--- a/src/main/java/com/android/apksig/internal/x509/Certificate.java
+++ b/src/main/java/com/android/apksig/internal/x509/Certificate.java
@@ -18,10 +18,25 @@
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.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import java.math.BigInteger;
import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
/**
* X509 {@code Certificate} as specified in RFC 5280.
@@ -36,4 +51,55 @@
@Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
public ByteBuffer signature;
+
+ public static X509Certificate findCertificate(
+ Collection<X509Certificate> certs, SignerIdentifier id) {
+ for (X509Certificate cert : certs) {
+ if (isMatchingCerticicate(cert, id)) {
+ return cert;
+ }
+ }
+ return null;
+ }
+
+ 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());
+ }
+
+ public static List<X509Certificate> parseCertificates(
+ List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
+ if (encodedCertificates.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ 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 = X509CertificateUtils.generateCertificate(encodedForm);
+ } catch (CertificateException e) {
+ throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original
+ // encoded form. Without this, getEncoded may return a different form from what was
+ // stored in the signature. This is because some X509Certificate(Factory)
+ // implementations re-encode certificates and/or some implementations of
+ // X509Certificate.getEncoded() re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
+ result.add(certificate);
+ }
+ return result;
+ }
}
diff --git a/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
index 4215810..74017f8 100644
--- a/src/main/java/com/android/apksig/util/RunnablesExecutor.java
+++ b/src/main/java/com/android/apksig/util/RunnablesExecutor.java
@@ -16,8 +16,46 @@
package com.android.apksig.util;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+
public interface RunnablesExecutor {
- RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+ static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+
+ static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
+ private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors());
+ private final int QUEUE_SIZE = 4;
+
+ @Override
+ public void execute(RunnablesProvider provider) {
+ final ExecutorService mExecutor =
+ new ThreadPoolExecutor(PARALLELISM, PARALLELISM,
+ 0L, MILLISECONDS,
+ new ArrayBlockingQueue<>(QUEUE_SIZE),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+
+ Phaser tasks = new Phaser(1);
+
+ for (int i = 0; i < PARALLELISM; ++i) {
+ Runnable task = () -> {
+ Runnable r = provider.createRunnable();
+ r.run();
+ tasks.arriveAndDeregister();
+ };
+ tasks.register();
+ mExecutor.execute(task);
+ }
+
+ // Waiting for the tasks to complete.
+ tasks.arriveAndAwaitAdvance();
+
+ mExecutor.shutdownNow();
+ }
+ };
void execute(RunnablesProvider provider);
}
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 65d9149..560202c 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -16,6 +16,13 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.findZipSections;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -24,23 +31,35 @@
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.stamp.V2SourceStampSigner;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.asn1.Asn1BerParser;
-import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.util.ReadableDataSink;
+import com.android.apksig.zip.ZipFormatException;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
import java.io.File;
import java.io.IOException;
+import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
@@ -48,11 +67,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.math.BigInteger;
@RunWith(JUnit4.class)
public class ApkSignerTest {
@@ -83,19 +97,25 @@
private static void generateGoldenFiles(File outDir) throws Exception {
System.out.println(
- "Generating golden files " + ApkSignerTest.class.getSimpleName()
- + " into " + outDir);
- if (!outDir.mkdirs()) {
- throw new IOException("Failed to create directory: " + outDir);
+ "Generating golden files "
+ + ApkSignerTest.class.getSimpleName()
+ + " into "
+ + outDir);
+ if (!outDir.exists()) {
+ if (!outDir.mkdirs()) {
+ throw new IOException("Failed to create directory: " + outDir);
+ }
}
List<ApkSigner.SignerConfig> rsa2048SignerConfig =
Collections.singletonList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = Arrays.asList(
- rsa2048SignerConfig.get(0),
- getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
- SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(
- ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ rsa2048SignerConfig.get(0),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
signGolden(
"golden-unaligned-in.apk",
@@ -316,30 +336,38 @@
.setSigningCertificateLineage(lineage));
signGolden(
- "original.apk", new File(outDir, "golden-rsa-out.apk"),
+ "original.apk",
+ new File(outDir, "golden-rsa-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig));
signGolden(
- "original.apk", new File(outDir, "golden-rsa-minSdkVersion-1-out.apk"),
+ "original.apk",
+ new File(outDir, "golden-rsa-minSdkVersion-1-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(1));
signGolden(
- "original.apk", new File(outDir, "golden-rsa-minSdkVersion-18-out.apk"),
+ "original.apk",
+ new File(outDir, "golden-rsa-minSdkVersion-18-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(18));
signGolden(
- "original.apk", new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"),
+ "original.apk",
+ new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(24));
+ signGolden(
+ "original.apk",
+ new File(outDir, "golden-rsa-verity-out.apk"),
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
}
private static void signGolden(
String inResourceName, File outFile, ApkSigner.Builder apkSignerBuilder)
- throws Exception {
+ throws Exception {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName)));
- apkSignerBuilder
- .setInputApk(in)
- .setOutputApk(outFile)
- .build()
- .sign();
+ apkSignerBuilder.setInputApk(in).setOutputApk(outFile).build().sign();
}
@Test
@@ -350,70 +378,82 @@
List<ApkSigner.SignerConfig> rsa2048SignerConfig =
Collections.singletonList(
getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = Arrays.asList(
- rsa2048SignerConfig.get(0),
- getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
- SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
- LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
+ List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage =
+ Arrays.asList(
+ rsa2048SignerConfig.get(0),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(
+ getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME);
// Uncompressed entries in this input file are not aligned -- the file was created using
// the jar utility. temp4.txt entry was then manually added into the archive. This entry's
// ZIP Local File Header "extra" field declares that the entry's data must be aligned to
// 4 kB boundary, but the data isn't actually aligned in the file.
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v1-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v1-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
.setV3SigningEnabled(false));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v2-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v3-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v3-lineage-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v1v2-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v2v3-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v2v3-lineage-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v1v2v3-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v1v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-unaligned-in.apk", "golden-unaligned-v1v2v3-lineage-out.apk",
+ "golden-unaligned-in.apk",
+ "golden-unaligned-v1v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
@@ -425,60 +465,70 @@
// archives whose "extra" field are not compliant with APPNOTE.TXT. Hence, this technique
// was deprecated.
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v1-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
.setV3SigningEnabled(false));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v3-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v3-lineage-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2v3-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2v3-lineage-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2v3-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v1v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2v3-lineage-out.apk",
+ "golden-legacy-aligned-in.apk",
+ "golden-legacy-aligned-v1v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
@@ -489,60 +539,70 @@
// generated by signapk and apksigner. This padding technique produces "extra" fields which
// are compliant with APPNOTE.TXT.
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v1-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v1-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false)
.setV3SigningEnabled(false));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v2-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v3-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v3-lineage-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(false)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v1v2-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(false));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v2v3-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v2v3-lineage-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
.setSigningCertificateLineage(lineage));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v1v2v3-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v1v2v3-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
.setV3SigningEnabled(true));
assertGolden(
- "golden-aligned-in.apk", "golden-aligned-v1v2v3-lineage-out.apk",
+ "golden-aligned-in.apk",
+ "golden-aligned-v1v2v3-lineage-out.apk",
new ApkSigner.Builder(rsa2048SignerConfigWithLineage)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true)
@@ -555,17 +615,21 @@
// Regression tests for minSdkVersion-based signature/digest algorithm selection
// NOTE: Expected output files can be re-generated by running the "main" method.
- List<ApkSigner.SignerConfig> rsaSignerConfig = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> rsaSignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
assertGolden("original.apk", "golden-rsa-out.apk", new ApkSigner.Builder(rsaSignerConfig));
assertGolden(
- "original.apk", "golden-rsa-minSdkVersion-1-out.apk",
+ "original.apk",
+ "golden-rsa-minSdkVersion-1-out.apk",
new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(1));
assertGolden(
- "original.apk", "golden-rsa-minSdkVersion-18-out.apk",
+ "original.apk",
+ "golden-rsa-minSdkVersion-18-out.apk",
new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(18));
assertGolden(
- "original.apk", "golden-rsa-minSdkVersion-24-out.apk",
+ "original.apk",
+ "golden-rsa-minSdkVersion-24-out.apk",
new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(24));
// TODO: Add tests for DSA and ECDSA. This is non-trivial because the default
@@ -574,9 +638,26 @@
}
@Test
+ public void testVerityEnabled_Golden() throws Exception {
+ List<ApkSigner.SignerConfig> rsaSignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertGolden(
+ "original.apk",
+ "golden-rsa-verity-out.apk",
+ new ApkSigner.Builder(rsaSignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
+ }
+
+ @Test
public void testRsaSignedVerifies() throws Exception {
- List<ApkSigner.SignerConfig> signers = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
String in = "original.apk";
// Sign so that the APK is guaranteed to verify on API Level 1+
@@ -628,25 +709,28 @@
public void testV1SigningRejectsInvalidZipEntryNames() throws Exception {
// ZIP/JAR entry name cannot contain CR, LF, or NUL characters when the APK is being
// JAR-signed.
- List<ApkSigner.SignerConfig> signers = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- try {
- sign("v1-only-with-cr-in-entry-name.apk",
- new ApkSigner.Builder(signers).setV1SigningEnabled(true));
- fail();
- } catch (ApkFormatException expected) {}
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
- try {
- sign("v1-only-with-lf-in-entry-name.apk",
- new ApkSigner.Builder(signers).setV1SigningEnabled(true));
- fail();
- } catch (ApkFormatException expected) {}
-
- try {
- sign("v1-only-with-nul-in-entry-name.apk",
- new ApkSigner.Builder(signers).setV1SigningEnabled(true));
- fail();
- } catch (ApkFormatException expected) {}
+ assertThrows(
+ ApkFormatException.class,
+ () ->
+ sign(
+ "v1-only-with-cr-in-entry-name.apk",
+ new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
+ assertThrows(
+ ApkFormatException.class,
+ () ->
+ sign(
+ "v1-only-with-lf-in-entry-name.apk",
+ new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
+ assertThrows(
+ ApkFormatException.class,
+ () ->
+ sign(
+ "v1-only-with-nul-in-entry-name.apk",
+ new ApkSigner.Builder(signers).setV1SigningEnabled(true)));
}
@Test
@@ -654,8 +738,9 @@
// Any ZIP compression method other than STORED is treated as DEFLATED by Android.
// This APK declares compression method 21 (neither STORED nor DEFLATED) for CERT.RSA entry,
// but the entry is actually Deflate-compressed.
- List<ApkSigner.SignerConfig> signers = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
sign("weird-compression-method.apk", new ApkSigner.Builder(signers));
}
@@ -665,53 +750,67 @@
// uses the compressionMethod from Central Directory instead.
// In this APK, compression method of CERT.RSA is declared as STORED in Local File Header
// and as DEFLATED in Central Directory. The entry is actually Deflate-compressed.
- List<ApkSigner.SignerConfig> signers = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
sign("mismatched-compression-method.apk", new ApkSigner.Builder(signers));
}
@Test
public void testDebuggableApk() throws Exception {
// APK which uses a boolean value "true" in its android:debuggable
- String apk = "debuggable-boolean.apk";
- List<ApkSigner.SignerConfig> signers = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ final String debuggableBooleanApk = "debuggable-boolean.apk";
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
// Signing debuggable APKs is permitted by default
- sign(apk, new ApkSigner.Builder(signers));
+ sign(debuggableBooleanApk, new ApkSigner.Builder(signers));
// Signing debuggable APK succeeds when explicitly requested
- sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+ sign(debuggableBooleanApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+
// Signing debuggable APK fails when requested
- try {
- sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false));
- fail();
- } catch (SignatureException expected) {}
+ assertThrows(
+ SignatureException.class,
+ () ->
+ sign(
+ debuggableBooleanApk,
+ new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)));
// APK which uses a reference value, pointing to boolean "false", in its android:debuggable
- apk = "debuggable-resource.apk";
+ final String debuggableResourceApk = "debuggable-resource.apk";
// When we permit signing regardless of whether the APK is debuggable, the value of
// android:debuggable should be ignored.
- sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
+ sign(debuggableResourceApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true));
// When we disallow signing debuggable APKs, APKs with android:debuggable being a resource
// reference must be rejected, because there's no easy way to establish whether the resolved
// boolean value is the same for all resource configurations.
- try {
- sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false));
- fail();
- } catch (SignatureException expected) {}
+ assertThrows(
+ SignatureException.class,
+ () ->
+ sign(
+ debuggableResourceApk,
+ new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)));
}
- @Test(expected = IllegalStateException.class)
+ @Test
public void testV3SigningWithSignersNotInLineageFails() throws Exception {
// APKs signed with the v3 scheme after a key rotation must specify the lineage containing
// the proof of rotation. This test verifies that the signing will fail if the provided
// signers are not in the specified lineage.
- List<ApkSigner.SignerConfig> signers = Arrays.asList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
- getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
- SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
- "rsa-1024-lineage-2-signers");
- sign("original.apk", new ApkSigner.Builder(signers).setSigningCertificateLineage(lineage));
+ List<ApkSigner.SignerConfig> signers =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(getClass(), "rsa-1024-lineage-2-signers");
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setSigningCertificateLineage(lineage)));
}
@Test
@@ -719,104 +818,131 @@
// After a key rotation the oldest signer must still be specified for v1 and v2 signing.
// The lineage contains the proof of rotation and will be used to determine the oldest
// signer.
- ApkSigner.SignerConfig firstSigner = getDefaultSignerConfigFromResources(
- FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
- ApkSigner.SignerConfig secondSigner = getDefaultSignerConfigFromResources(
- SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- ApkSigner.SignerConfig thirdSigner = getDefaultSignerConfigFromResources(
- THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
- SigningCertificateLineage lineage = Resources.toSigningCertificateLineage(getClass(),
- "rsa-2048-lineage-3-signers");
+ ApkSigner.SignerConfig firstSigner =
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ ApkSigner.SignerConfig secondSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ ApkSigner.SignerConfig thirdSigner =
+ getDefaultSignerConfigFromResources(THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+ SigningCertificateLineage lineage =
+ Resources.toSigningCertificateLineage(getClass(), "rsa-2048-lineage-3-signers");
// Verifies that the v1 signing scheme requires the oldest signer after a key rotation.
List<ApkSigner.SignerConfig> signers = Collections.singletonList(thirdSigner);
try {
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(false)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
- fail("The signing should have failed due to the oldest signer in the lineage not being"
- + " provided for v1 signing");
- } catch (IllegalArgumentException expected) {}
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
+ fail(
+ "The signing should have failed due to the oldest signer in the lineage not"
+ + " being provided for v1 signing");
+ } catch (IllegalArgumentException expected) {
+ }
// Verifies that the v2 signing scheme requires the oldest signer after a key rotation.
try {
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(false)
- .setV2SigningEnabled(true)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
- fail("The signing should have failed due to the oldest signer in the lineage not being"
- + " provided for v2 signing");
- } catch (IllegalArgumentException expected) {}
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
+ fail(
+ "The signing should have failed due to the oldest signer in the lineage not"
+ + " being provided for v2 signing");
+ } catch (IllegalArgumentException expected) {
+ }
// Verifies that when only the v3 signing scheme is requested the oldest signer does not
// need to be provided.
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(false)
- .setV2SigningEnabled(false)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
// Verifies that an intermediate signer in the lineage is not sufficient to satisfy the
// requirement that the oldest signer be provided for v1 and v2 signing.
signers = Arrays.asList(secondSigner, thirdSigner);
try {
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(true)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
- fail("The signing should have failed due to the oldest signer in the lineage not being"
- + " provided for v1/v2 signing");
- } catch (IllegalArgumentException expected) {}
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
+ fail(
+ "The signing should have failed due to the oldest signer in the lineage not"
+ + " being provided for v1/v2 signing");
+ } catch (IllegalArgumentException expected) {
+ }
// Verifies that the signing is successful when the oldest and newest signers are provided
// and that intermediate signers are not required.
signers = Arrays.asList(firstSigner, thirdSigner);
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(true)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
}
- @Test(expected = IllegalStateException.class)
+ @Test
public void testV3SigningWithMultipleSignersAndNoLineageFails() throws Exception {
// The v3 signing scheme does not support multiple signers; if multiple signers are provided
// it is assumed these signers are part of the lineage. This test verifies v3 signing
// fails if multiple signers are provided without a lineage.
- ApkSigner.SignerConfig firstSigner = getDefaultSignerConfigFromResources(
- FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
- ApkSigner.SignerConfig secondSigner = getDefaultSignerConfigFromResources(
- SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ ApkSigner.SignerConfig firstSigner =
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ ApkSigner.SignerConfig secondSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
List<ApkSigner.SignerConfig> signers = Arrays.asList(firstSigner, secondSigner);
- sign("original.apk", new ApkSigner.Builder(signers)
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(true)
- .setV3SigningEnabled(true));
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)));
}
@Test
public void testLineageCanBeReadAfterV3Signing() throws Exception {
- SigningCertificateLineage.SignerConfig firstSigner = Resources.toLineageSignerConfig(
- getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
- SigningCertificateLineage.SignerConfig secondSigner = Resources.toLineageSignerConfig(
- getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
- SigningCertificateLineage lineage = new SigningCertificateLineage.Builder(firstSigner,
- secondSigner).build();
- List<ApkSigner.SignerConfig> signerConfigs = Arrays.asList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
- getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
- DataSource out = sign("original.apk", new ApkSigner.Builder(signerConfigs)
- .setV3SigningEnabled(true)
- .setSigningCertificateLineage(lineage));
- SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource(
- out);
- assertTrue("The first signer was not in the lineage from the signed APK",
+ SigningCertificateLineage.SignerConfig firstSigner =
+ Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ SigningCertificateLineage.SignerConfig secondSigner =
+ Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ SigningCertificateLineage lineage =
+ new SigningCertificateLineage.Builder(firstSigner, secondSigner).build();
+ List<ApkSigner.SignerConfig> signerConfigs =
+ Arrays.asList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME),
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ DataSource out =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signerConfigs)
+ .setV3SigningEnabled(true)
+ .setSigningCertificateLineage(lineage));
+ SigningCertificateLineage lineageFromApk =
+ SigningCertificateLineage.readFromApkDataSource(out);
+ assertTrue(
+ "The first signer was not in the lineage from the signed APK",
lineageFromApk.isSignerInLineage((firstSigner)));
- assertTrue("The second signer was not in the lineage from the signed APK",
+ assertTrue(
+ "The second signer was not in the lineage from the signed APK",
lineageFromApk.isSignerInLineage((secondSigner)));
}
@@ -829,23 +955,251 @@
// the correct encoding for the modulus. This test uses an improperly encoded certificate to
// sign an APK and verifies that the public key in the signing block is corrected with a
// positive modulus to allow on device installs / updates.
- List<ApkSigner.SignerConfig> signersList = Collections.singletonList(
- getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
- FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
- DataSource signedApk = sign("original.apk", new ApkSigner.Builder(signersList)
- .setV1SigningEnabled(true)
- .setV2SigningEnabled(true)
- .setV3SigningEnabled(true));
- RSAPublicKey v2PublicKey = getRSAPublicKeyFromSigningBlock(signedApk,
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
- assertTrue("The modulus in the public key in the V2 signing block must not be negative",
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS));
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true));
+ RSAPublicKey v2PublicKey =
+ getRSAPublicKeyFromSigningBlock(
+ signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ assertTrue(
+ "The modulus in the public key in the V2 signing block must not be negative",
v2PublicKey.modulus.compareTo(BigInteger.ZERO) > 0);
- RSAPublicKey v3PublicKey = getRSAPublicKeyFromSigningBlock(signedApk,
- ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
- assertTrue("The modulus in the public key in the V3 signing block must not be negative",
+ RSAPublicKey v3PublicKey =
+ getRSAPublicKeyFromSigningBlock(
+ signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ assertTrue(
+ "The modulus in the public key in the V3 signing block must not be negative",
v3PublicKey.modulus.compareTo(BigInteger.ZERO) > 0);
}
+ @Test
+ public void testV4State_disableV2V3EnableV4_fails() throws Exception {
+ ApkSigner.SignerConfig signer =
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(Collections.singletonList(signer))
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(true)));
+ }
+
+ @Test
+ public void testSignApk_stampFile() throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded());
+ byte[] expectedStampCertificateDigest = messageDigest.digest();
+
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkUtils.ZipSections zipSections = findZipSections(signedApk);
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections);
+ CentralDirectoryRecord stampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ stampCdRecord = cdRecord;
+ break;
+ }
+ }
+ assertNotNull(stampCdRecord);
+ byte[] actualStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset());
+ assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest);
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_sameSourceStamp() throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ DataSource signedApk =
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_differentSourceStamp() throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ Exception exception =
+ assertThrows(
+ ApkFormatException.class,
+ () ->
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner)));
+ assertEquals(
+ String.format(
+ "Cannot generate SourceStamp. APK contains an existing entry with the"
+ + " name: %s, and it is different than the provided source stamp"
+ + " certificate",
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME),
+ exception.getMessage());
+ }
+
+ @Test
+ public void testSignApk_existingStampFile_differentSourceStamp_forceOverwrite()
+ throws Exception {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ DataSource signedApk =
+ sign(
+ "original-with-stamp-file.apk",
+ new ApkSigner.Builder(signers)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setForceSourceStampOverwrite(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_stampBlock_noStampGenerated() throws Exception {
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true));
+
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk);
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ assertThrows(
+ ApkSigningBlockUtils.SignatureNotFoundException.class,
+ () ->
+ ApkSigningBlockUtils.findSignature(
+ signedApk,
+ zipSections,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+ result));
+ }
+
+ @Test
+ public void testSignApk_stampBlock_whenV1SignaturePresent() throws Exception {
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verify(signedApk, /* minSdkVersionOverride= */ null);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_stampBlock_whenV2SignaturePresent() throws Exception {
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
+ @Test
+ public void testSignApk_stampBlock_whenV3SignaturePresent() throws Exception {
+ List<ApkSigner.SignerConfig> signersList =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ ApkSigner.SignerConfig sourceStampSigner =
+ getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+
+ DataSource signedApk =
+ sign(
+ "original.apk",
+ new ApkSigner.Builder(signersList)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setSourceStampSignerConfig(sourceStampSigner));
+
+ ApkVerifier.Result sourceStampVerificationResult =
+ verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N);
+ assertSourceStampVerified(signedApk, sourceStampVerificationResult);
+ }
+
private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId)
throws Exception {
int signatureVersionBlockId;
@@ -860,11 +1214,8 @@
throw new Exception(
"Invalid signature version ID specified: " + signatureVersionId);
}
- ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
- ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
- signatureVersionId);
- SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
- signatureVersionBlockId, result);
+ SignatureInfo signatureInfo =
+ getSignatureInfoFromApk(apk, signatureVersionId, signatureVersionBlockId);
// FORMAT:
// * length prefixed sequence of length prefixed signers
// * length-prefixed signed data
@@ -872,8 +1223,8 @@
// * V3+ only - maxSDK (uint32)
// * length-prefixed sequence of length-prefixed signatures:
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
- ByteBuffer signers = ApkSigningBlockUtils.getLengthPrefixedSlice(
- signatureInfo.signatureBlock);
+ ByteBuffer signers =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(signatureInfo.signatureBlock);
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
// Since all the data is read from the signer block the signedData and signatures are
// discarded.
@@ -885,25 +1236,35 @@
}
ApkSigningBlockUtils.getLengthPrefixedSlice(signer);
ByteBuffer publicKey = ApkSigningBlockUtils.getLengthPrefixedSlice(signer);
- SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse(publicKey,
- SubjectPublicKeyInfo.class);
+ SubjectPublicKeyInfo subjectPublicKeyInfo =
+ Asn1BerParser.parse(publicKey, SubjectPublicKeyInfo.class);
ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey;
// The SubjectPublicKey is stored as a bit string in the SubjectPublicKeyInfo with the first
// byte indicating the number of padding bits in the public key. Read this first byte to
// allow parsing the rest of the RSAPublicKey as a sequence.
subjectPublicKeyBuffer.get();
- RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer,
- RSAPublicKey.class);
- return rsaPublicKey;
+ return Asn1BerParser.parse(subjectPublicKeyBuffer, RSAPublicKey.class);
+ }
+
+ private static SignatureInfo getSignatureInfoFromApk(
+ DataSource apk, int signatureVersionId, int signatureVersionBlockId)
+ throws IOException, ZipFormatException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(signatureVersionId);
+ return ApkSigningBlockUtils.findSignature(
+ apk, zipSections, signatureVersionBlockId, result);
}
/**
- * Asserts that signing the specified golden input file using the provided signing
- * configuration produces output identical to the specified golden output file.
+ * Asserts that signing the specified golden input file using the provided signing configuration
+ * produces output identical to the specified golden output file.
*/
private void assertGolden(
- String inResourceName, String expectedOutResourceName,
- ApkSigner.Builder apkSignerBuilder) throws Exception {
+ String inResourceName,
+ String expectedOutResourceName,
+ ApkSigner.Builder apkSignerBuilder)
+ throws Exception {
// Sign the provided golden input
DataSource out = sign(inResourceName, apkSignerBuilder);
@@ -950,17 +1311,13 @@
}
}
- private DataSource sign(
- String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception {
+ private DataSource sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
+ throws Exception {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName)));
ReadableDataSink out = DataSinks.newInMemoryDataSink();
- apkSignerBuilder
- .setInputApk(in)
- .setOutputApk(out)
- .build()
- .sign();
+ apkSignerBuilder.setInputApk(in).setOutputApk(out).build().sign();
return out;
}
@@ -982,6 +1339,18 @@
ApkVerifierTest.assertVerified(result);
}
+ private static void assertSourceStampVerified(DataSource signedApk, ApkVerifier.Result result)
+ throws ApkSigningBlockUtils.SignatureNotFoundException, IOException,
+ ZipFormatException {
+ SignatureInfo signatureInfo =
+ getSignatureInfoFromApk(
+ signedApk,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP,
+ V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID);
+ assertNotNull(signatureInfo.signatureBlock);
+ assertTrue(result.isSourceStampVerified());
+ }
+
private static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) {
ApkVerifierTest.assertVerificationFailure(result, expectedIssue);
}
@@ -997,10 +1366,10 @@
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
String keyNameInResources, String certNameInResources) throws Exception {
- PrivateKey privateKey = Resources.toPrivateKey(ApkSignerTest.class,
- keyNameInResources + ".pk8");
- List<X509Certificate> certs = Resources.toCertificateChain(ApkSignerTest.class,
- certNameInResources);
+ PrivateKey privateKey =
+ Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8");
+ List<X509Certificate> certs =
+ Resources.toCertificateChain(ApkSignerTest.class, certNameInResources);
return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
}
}
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 351d0a8..ed154c5 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -17,6 +17,7 @@
package com.android.apksig;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
@@ -27,6 +28,12 @@
import com.android.apksig.internal.util.HexEncoding;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.util.DataSources;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
@@ -37,12 +44,12 @@
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.List;
import java.util.Locale;
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
@RunWith(JUnit4.class)
public class ApkVerifierTest {
@@ -52,8 +59,9 @@
private static final String[] DSA_KEY_NAMES_2048_AND_LARGER = {"2048", "3072"};
private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"};
private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"};
- private static final String[] RSA_KEY_NAMES_2048_AND_LARGER =
- {"2048", "3072", "4096", "8192", "16384"};
+ private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = {
+ "2048", "3072", "4096", "8192", "16384"
+ };
@Test
public void testOriginalAccepted() throws Exception {
@@ -121,46 +129,36 @@
@Test
public void testV1OneSignerSHA1withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA224withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA256withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA384withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA512withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
}
@Test
@@ -233,8 +231,7 @@
@Test
public void testV1OneSignerSHA256withDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
assertVerifiedForEach(
"v1-only-with-dsa-sha256-2.16.840.1.101.3.4.3.2-%s.apk", DSA_KEY_NAMES);
}
@@ -246,8 +243,7 @@
// This should fail because the v1 signature indicates that the APK was supposed to be
// signed with v2 scheme as well, making the platform's anti-stripping protections reject
// the APK.
- assertVerificationFailure(
- "v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
+ assertVerificationFailure("v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
// Similar to above, but the X-Android-APK-Signed anti-stripping header in v1 signature
// lists unknown signature schemes in addition to APK Signature Scheme v2. Unknown schemes
@@ -262,8 +258,7 @@
// APK signed with v2 and v3 schemes, but v3 signature was stripped from the file by
// modifying the v3 block ID to be the verity padding block ID. Without the stripping
// protection this modification ignores the v3 signing scheme block.
- assertVerificationFailure(
- "v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
+ assertVerificationFailure("v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
}
@Test
@@ -271,10 +266,12 @@
// The V2 signature scheme was introduced in N, and V3 was introduced in P. This test
// verifies a max SDK of pre-P ignores the V3 signature and a max SDK of pre-N ignores both
// the V2 and V3 signatures.
- assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
- AndroidSdkVersion.O));
- assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
- AndroidSdkVersion.M));
+ assertVerified(
+ verifyForMaxSdkVersion(
+ "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.O));
+ assertVerified(
+ verifyForMaxSdkVersion(
+ "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.M));
}
@Test
@@ -390,13 +387,11 @@
// Based on v2-only-with-rsa-pkcs1-sha512-4096.apk. Obtained by modifying APK signer to
// flip the leftmost bit in content digest before signing signed-data.
- assertVerificationFailure(
- "v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
+ assertVerificationFailure("v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
// Based on v2-only-with-ecdsa-sha256-p256.apk. Obtained by modifying APK signer to flip the
// leftmost bit in content digest before signing signed-data.
- assertVerificationFailure(
- "v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
+ assertVerificationFailure("v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
}
@Test
@@ -425,21 +420,18 @@
// Obtained from v2-only-with-rsa-pkcs1-sha512-4096.apk by flipping a bit in the magic
// field in the footer of APK Signing Block. This makes the APK Signing Block disappear.
assertVerificationFailure(
- "v2-only-wrong-apk-sig-block-magic.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-wrong-apk-sig-block-magic.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying APK signer to insert "GARBAGE" between ZIP Central Directory and
// End of Central Directory. The APK is otherwise fine and is signed with APK Signature
// Scheme v2. Based on v2-only-with-rsa-pkcs1-sha256.apk.
assertVerificationFailure(
- "v2-only-garbage-between-cd-and-eocd.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-garbage-between-cd-and-eocd.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying the size in APK Signature Block header. Based on
// v2-only-with-ecdsa-sha512-p521.apk.
assertVerificationFailure(
- "v2-only-apk-sig-block-size-mismatch.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-apk-sig-block-size-mismatch.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying the ID under which APK Signature Scheme v2 Block is stored in
// APK Signing Block and by modifying the APK signer to not insert anti-stripping
@@ -539,7 +531,8 @@
// APK's v2 and v3 signatures contain unknown additional attributes before and after the
// anti-stripping and lineage attributes.
assertVerified(
- verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P)); }
+ verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));
+ }
@Test
public void testV2MismatchBetweenSignaturesAndDigestsBlockRejected() throws Exception {
@@ -585,16 +578,14 @@
public void testV2SignerBlockWithNoCertificatesRejected() throws Exception {
// APK is signed with v2 only. There are no certificates listed in the signer block.
// Obtained by modifying APK signer to output no certificates.
- assertVerificationFailure(
- "v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
+ assertVerificationFailure("v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
}
@Test
public void testV3SignerBlockWithNoCertificatesRejected() throws Exception {
// APK is signed with v3 only. There are no certificates listed in the signer block.
// Obtained by modifying APK signer to output no certificates.
- assertVerificationFailure(
- "v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
+ assertVerificationFailure("v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
}
@Test
@@ -739,25 +730,29 @@
try {
verifyForMinSdkVersion("empty-unsigned.apk", 1);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// JAR-signed empty ZIP archive
try {
verifyForMinSdkVersion("v1-only-empty.apk", 18);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// APK Signature Scheme v2 signed empty ZIP archive
try {
verifyForMinSdkVersion("v2-only-empty.apk", AndroidSdkVersion.N);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// APK Signature Scheme v3 signed empty ZIP archive
try {
verifyForMinSdkVersion("v3-only-empty.apk", AndroidSdkVersion.P);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
}
@Test
@@ -786,6 +781,47 @@
}
@Test
+ public void testTargetSdkMinSchemeVersionNotMet() throws Exception {
+ // Android 11 / SDK version 30 requires apps targeting this SDK version or higher must be
+ // signed with at least the V2 signature scheme. This test verifies if an app is targeting
+ // this SDK version and is only signed with a V1 signature then the verifier reports the
+ // platform will not accept it.
+ assertVerificationFailure(verify("v1-ec-p256-targetSdk-30.apk"),
+ Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET);
+ }
+
+ @Test
+ public void testTargetSdkMinSchemeVersionMet() throws Exception {
+ // This test verifies if an app is signed with the minimum required signature scheme version
+ // for the target SDK version then the verifier reports the platform will accept it.
+ assertVerified(verify("v2-ec-p256-targetSdk-30.apk"));
+
+ // If an app is only signed with a signature scheme higher than the required version for the
+ // target SDK the verifier should also report that the platform will accept it.
+ assertVerified(verify("v3-ec-p256-targetSdk-30.apk"));
+ }
+
+ @Test
+ public void testTargetSdkMinSchemeVersionNotMetMaxLessThanTarget() throws Exception {
+ // If the minimum signature scheme for the target SDK version is not met but the maximum
+ // SDK version is less than the target then the verifier should report that the platform
+ // will accept it since the specified max SDK version does not know about the minimum
+ // signature scheme requirement.
+ verifyForMaxSdkVersion("v1-ec-p256-targetSdk-30.apk", 29);
+ }
+
+ @Test
+ public void testTargetSdkNoUsesSdkElement() throws Exception {
+ // The target SDK minimum signature scheme version check will attempt to obtain the
+ // targetSdkVersion attribute value from the uses-sdk element in the AndroidManifest. If
+ // the targetSdkVersion is not specified then the verifier should behave the same as the
+ // platform; the minSdkVersion should be used when available and when neither the minimum or
+ // target SDK are specified a default value of 1 should be used. This test verifies that the
+ // verifier does not fail when the uses-sdk element is not specified.
+ verify("v1-only-no-uses-sdk.apk");
+ }
+
+ @Test
public void testV1MultipleDigestAlgsInManifestAndSignatureFile() throws Exception {
// MANIFEST.MF contains SHA-1 and SHA-256 digests for each entry, .SF contains only SHA-1
// digests. This file was obtained by:
@@ -954,8 +990,7 @@
verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
Issue.JAR_SIG_VERIFY_EXCEPTION);
assertVerificationFailure(
- verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
- Issue.JAR_SIG_VERIFY_EXCEPTION);
+ 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.
@@ -964,8 +999,7 @@
verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
Issue.JAR_SIG_VERIFY_EXCEPTION);
assertVerificationFailure(
- verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
- Issue.JAR_SIG_VERIFY_EXCEPTION);
+ verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
}
@Test
@@ -1020,6 +1054,66 @@
assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
}
+ @Test
+ public void testSourceStampBlock_correctSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verify("valid-stamp.apk");
+ // Verifies the signature of the APK.
+ assertVerified(verificationResult);
+ // Verifies the signature of source stamp.
+ assertTrue(verificationResult.isSourceStampVerified());
+ }
+
+ @Test
+ public void testSourceStampBlock_signatureMissing() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-without-block.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+
+ @Test
+ public void testSourceStampBlock_certificateMismatch() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(
+ verificationResult,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v1SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v1.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v2SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v2.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch_v3SignatureScheme() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v3.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_malformedSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(
+ verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
@@ -1027,13 +1121,13 @@
private ApkVerifier.Result verifyForMinSdkVersion(
String apkFilenameInResources, int minSdkVersion)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, minSdkVersion, null);
}
private ApkVerifier.Result verifyForMaxSdkVersion(
String apkFilenameInResources, int maxSdkVersion)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, maxSdkVersion);
}
@@ -1041,7 +1135,7 @@
String apkFilenameInResources,
Integer minSdkVersionOverride,
Integer maxSdkVersionOverride)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
ApkVerifier.Builder builder =
@@ -1077,8 +1171,12 @@
if (msg.length() > 0) {
msg.append('\n');
}
- msg.append("JAR signer ").append(signerName).append(": ")
- .append(issue.getIssue()).append(": ").append(issue);
+ msg.append("JAR signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1088,8 +1186,25 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v2 signer ")
- .append(signerName).append(": ")
- .append(issue.getIssue()).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
+ }
+ }
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1);
+ for (IssueWithParams issue : signer.getErrors()) {
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK Signature Scheme v3 signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
}
}
@@ -1099,7 +1214,8 @@
private void assertVerified(
String apkFilenameInResources,
Integer minSdkVersionOverride,
- Integer maxSdkVersionOverride) throws Exception {
+ Integer maxSdkVersionOverride)
+ throws Exception {
assertVerified(
verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride),
apkFilenameInResources);
@@ -1130,8 +1246,12 @@
if (msg.length() > 0) {
msg.append('\n');
}
- msg.append("JAR signer ").append(signerName).append(": ")
- .append(issue.getIssue()).append(" ").append(issue);
+ msg.append("JAR signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(" ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1144,7 +1264,9 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v2 signer ")
- .append(signerName).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
@@ -1157,22 +1279,78 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v3 signer ")
- .append(signerName).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue);
}
}
- fail("APK failed verification for the wrong reason"
- + ". Expected: " + expectedIssue + ", actual: " + msg);
+ fail(
+ "APK failed verification for the wrong reason"
+ + ". Expected: "
+ + expectedIssue
+ + ", actual: "
+ + msg);
+ }
+
+ private static void assertSourceStampVerificationFailure(
+ ApkVerifier.Result result, Issue expectedIssue) {
+ if (result.isSourceStampVerified()) {
+ fail(
+ "APK source stamp verification succeeded instead of failing with "
+ + expectedIssue);
+ return;
+ }
+
+ StringBuilder msg = new StringBuilder();
+ List<IssueWithParams> resultIssueWithParams =
+ Stream.of(result.getErrors(), result.getWarnings())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ for (IssueWithParams issue : resultIssueWithParams) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(issue);
+ }
+
+ ApkVerifier.Result.SourceStampInfo signer = result.getSourceStampInfo();
+ if (signer != null) {
+ List<IssueWithParams> sourceStampIssueWithParams =
+ Stream.of(signer.getErrors(), signer.getWarnings())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ for (IssueWithParams issue : sourceStampIssueWithParams) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK SourceStamp signer").append(": ").append(issue);
+ }
+ }
+
+ fail(
+ "APK source stamp failed verification for the wrong reason"
+ + ". Expected: "
+ + expectedIssue
+ + ", actual: "
+ + msg);
}
private void assertVerificationFailure(
- String apkFilenameInResources, ApkVerifier.Issue expectedIssue)
- throws Exception {
+ String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
}
- private void assertVerifiedForEach(
- String apkFilenamePatternInResources, String[] args) throws Exception {
+ private void assertVerifiedForEach(String apkFilenamePatternInResources, String[] args)
+ throws Exception {
assertVerifiedForEach(apkFilenamePatternInResources, args, null, null);
}
@@ -1180,7 +1358,8 @@
String apkFilenamePatternInResources,
String[] args,
Integer minSdkVersionOverride,
- Integer maxSdkVersionOverride) throws Exception {
+ Integer maxSdkVersionOverride)
+ throws Exception {
for (String arg : args) {
String apkFilenameInResources =
String.format(Locale.US, apkFilenamePatternInResources, arg);
@@ -1189,12 +1368,11 @@
}
private void assertVerifiedForEachForMinSdkVersion(
- String apkFilenameInResources, String[] args, int minSdkVersion)
- throws Exception {
+ String apkFilenameInResources, String[] args, int minSdkVersion) throws Exception {
assertVerifiedForEach(apkFilenameInResources, args, minSdkVersion, null);
}
- private static byte[] sha256(byte[] msg) throws Exception {
+ private static byte[] sha256(byte[] msg) {
try {
return MessageDigest.getInstance("SHA-256").digest(msg);
} catch (NoSuchAlgorithmException e) {
@@ -1202,7 +1380,7 @@
}
}
- private static void assumeThatRsaPssAvailable() throws Exception {
+ private static void assumeThatRsaPssAvailable() {
Assume.assumeTrue(Security.getProviders("Signature.SHA256withRSA/PSS") != null);
}
}
diff --git a/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java b/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
index 9578926..12f08f1 100644
--- a/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
+++ b/src/test/java/com/android/apksig/internal/util/FileChannelDataSourceTest.java
@@ -90,14 +90,14 @@
assertArrayEquals(expectedBytes, resultBytes);
}
- private byte[] getDataSinkBytes(ByteArrayDataSink dataSink) {
+ private static byte[] getDataSinkBytes(ByteArrayDataSink dataSink) {
ByteBuffer result = dataSink.getByteBuffer(0, (int)dataSink.size());
byte[] resultBytes = new byte[result.limit()];
result.get(resultBytes);
return resultBytes;
}
- private byte[] createFileContent(int fileSize) {
+ private static byte[] createFileContent(int fileSize) {
byte[] fullFileContent = new byte[fileSize];
for (int i = 0; i < fileSize; ++i) {
fullFileContent[i] = (byte) (i % 255);
diff --git a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
index 1fa1e40..85e9e90 100644
--- a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
+++ b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
@@ -66,8 +66,7 @@
private static String generateRootHash(String inputResource, byte[] salt) throws IOException {
byte[] input = Resources.toByteArray(VerityTreeBuilderTest.class, inputResource);
assertNotNull(input);
- try {
- VerityTreeBuilder builder = new VerityTreeBuilder(salt);
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(salt)) {
return HexEncoding.encode(builder.generateVerityTreeRootHash(
DataSources.asDataSource(ByteBuffer.wrap(input))));
} catch (NoSuchAlgorithmException e) {
@@ -88,8 +87,7 @@
};
String expectedRootHash =
"7694e72c242107a5b4ce6091faf867e2f13c033b6b64faddcb13b3d698a8495a";
- {
- VerityTreeBuilder builder = new VerityTreeBuilder(null);
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
byte[] rootHash = builder.generateVerityTreeRootHash(
DataSources.asDataSource(ByteBuffer.allocate(4096)), // before Signing Block
makeStringDataSource("this is central directory (fake data)"),
@@ -100,13 +98,12 @@
// File ending with different length of zeros get the same hash. This is to match the
// behavior of fs-verity, where it expects the client to add salt for the same level of
// protection.
- {
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
ByteBuffer fileTailWithDifferentLengthOfZeros = ByteBuffer.allocate(
sampleEoCDFromDisk.length + 1);
fileTailWithDifferentLengthOfZeros.put(sampleEoCDFromDisk);
fileTailWithDifferentLengthOfZeros.rewind();
- VerityTreeBuilder builder = new VerityTreeBuilder(null);
byte[] rootHash = builder.generateVerityTreeRootHash(
DataSources.asDataSource(ByteBuffer.allocate(4096)), // before Signing Block
makeStringDataSource("this is central directory (fake data)"),
diff --git a/src/test/resources/com/android/apksig/golden-aligned-out.apk b/src/test/resources/com/android/apksig/golden-aligned-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
index 5133049..1c0edeb 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
index b9dc782..965f901 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
index 2396782..e82f67b 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
index d947e3c..69f5e64 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
index 88c571b..13a3bc9 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
index 25f35cc..19931ed 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
index 30e1f72..36f4825 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
index f97cbeb..48f9aa3 100644
--- a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
index cc03744..cf04aa2 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
index e359da7..f6b60d6 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
index d177361..aef3280 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
index 68f07ed..f34a8e5 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
index 4b51e4f..b0debf6 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
index 7177862..9940c4f 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
index bd3e668..f8b4171 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
index 67a7d3f..80a70b9 100644
--- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
index 7289853..5ae642a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
new file mode 100644
index 0000000..5ce588a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-out.apk b/src/test/resources/com/android/apksig/golden-rsa-out.apk
index 232db96..5ce588a 100644
--- a/src/test/resources/com/android/apksig/golden-rsa-out.apk
+++ b/src/test/resources/com/android/apksig/golden-rsa-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
new file mode 100644
index 0000000..232db96
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
index c708211..5194496 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
index dd6324b..02ecee0 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
index 0bd34c4..17a872d 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
index 4fdc18c..92e5099 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
index 4e523ca..08538a0 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
index 74e7dbc..c18c445 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
index 831c756..9bcbc7a 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
index 3196267..4db4579 100644
--- a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
+++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/original-with-stamp-file.apk b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
new file mode 100644
index 0000000..604fe6f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/original-with-stamp-file.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
new file mode 100644
index 0000000..add4aa0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
new file mode 100644
index 0000000..e55eb90
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
new file mode 100644
index 0000000..de23558
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
new file mode 100644
index 0000000..1dc1e99
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
new file mode 100644
index 0000000..f1105f9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
new file mode 100644
index 0000000..d28774a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk
new file mode 100644
index 0000000..604fe6f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..6a561c0
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
new file mode 100644
index 0000000..714f9ff
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..a29dd6c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
new file mode 100644
index 0000000..c58ec8b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk
new file mode 100644
index 0000000..2f2a592
--- /dev/null
+++ b/src/test/resources/com/android/apksig/valid-stamp.apk
Binary files differ