aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2021-06-19 12:08:11 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2021-06-19 12:08:11 +0000
commitd370ab99d1dcb18dfefc86acffa4c5fd62d464ce (patch)
tree9377b651b9a765ffbc05ec4a394972c54446e1fa
parent705e0690285758aae423a157d17c6312d92d7343 (diff)
parentab50f0a22c4a770bb914d18483a6cc1dd930182b (diff)
downloadapksig-android12-mainline-media-release.tar.gz
Change-Id: Ib2e89d15d34f8c4b7efa57c6621ee4e1edefea9a
-rw-r--r--Android.bp19
-rw-r--r--build.gradle1
-rw-r--r--src/apksigner/java/com/android/apksigner/ApkSignerTool.java46
-rw-r--r--src/apksigner/java/com/android/apksigner/SignerParams.java17
-rw-r--r--src/apksigner/java/com/android/apksigner/help_sign.txt12
-rw-r--r--src/main/java/com/android/apksig/ApkSigner.java112
-rw-r--r--src/main/java/com/android/apksig/DefaultApkSignerEngine.java168
-rw-r--r--src/main/java/com/android/apksig/SigningCertificateLineage.java12
-rw-r--r--src/main/java/com/android/apksig/apk/ApkUtils.java21
-rw-r--r--src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java118
-rw-r--r--src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java12
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java1
-rw-r--r--src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java7
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java8
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java33
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java8
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java8
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java9
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java2
-rw-r--r--src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java2
-rw-r--r--src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java7
-rw-r--r--src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java2
-rw-r--r--src/test/java/com/android/apksig/ApkSignerTest.java390
-rw-r--r--src/test/java/com/android/apksig/SigningCertificateLineageTest.java28
-rw-r--r--src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java2
-rw-r--r--src/test/resources/com/android/apksig/golden-pinsapp-signed.apkbin0 -> 8732 bytes
-rwxr-xr-xsrc/test/resources/com/android/apksig/pinsapp-unsigned.apkbin0 -> 2205 bytes
-rw-r--r--src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apkbin0 -> 4623 bytes
-rw-r--r--src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apkbin0 -> 12703 bytes
29 files changed, 954 insertions, 91 deletions
diff --git a/Android.bp b/Android.bp
index ec57fb3..09cddbf 100644
--- a/Android.bp
+++ b/Android.bp
@@ -16,6 +16,23 @@
// apksig library, for signing APKs and verifying signatures of APKs
// ============================================================
+package {
+ default_applicable_licenses: ["tools_apksig_license"],
+}
+
+// Added automatically by a large-scale-change
+// http://go/android-license-faq
+license {
+ name: "tools_apksig_license",
+ visibility: [":__subpackages__"],
+ license_kinds: [
+ "SPDX-license-identifier-Apache-2.0",
+ ],
+ license_text: [
+ "LICENSE",
+ ],
+}
+
java_library_host {
name: "apksig",
srcs: [
@@ -36,6 +53,6 @@ java_binary_host {
"apksig",
"conscrypt-unbundled",
],
- required: ["libconscrypt_openjdk_jni"],
+ jni_libs: ["libconscrypt_openjdk_jni"],
java_version: "1.8",
}
diff --git a/build.gradle b/build.gradle
index 12c0d32..4c05a77 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,7 @@ buildscript {
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
testImplementation 'junit:junit:4.13'
+ testImplementation 'org.bouncycastle:bcprov-jdk15on:1.68'
}
protobuf {
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index c7cb660..9fd0c34 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -25,8 +25,6 @@ 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;
@@ -80,7 +78,9 @@ public class ApkSignerTool {
return;
}
+ // BEGIN-AOSP
addProviders();
+ // END-AOSP
String cmd = params[0];
try {
@@ -113,18 +113,20 @@ public class ApkSignerTool {
}
}
+ // BEGIN-AOSP
/**
* 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());
+ Security.addProvider(new org.conscrypt.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.
}
}
+ // END-AOSP
private static void sign(String[] params) throws Exception {
if (params.length == 0) {
@@ -157,6 +159,8 @@ public class ApkSignerTool {
String optionOriginalForm = null;
boolean v4SigningFlagFound = false;
boolean sourceStampFlagFound = false;
+ boolean deterministicDsaSigning = false;
+ boolean otherSignersSignaturesPreserved = false;
while ((optionName = optionsParser.nextOption()) != null) {
optionOriginalForm = optionsParser.getOptionOriginalForm();
if (("help".equals(optionName)) || ("h".equals(optionName))) {
@@ -257,6 +261,10 @@ public class ApkSignerTool {
File stampLineageFile = new File(
optionsParser.getRequiredValue("Stamp Lineage File"));
sourceStampLineage = getLineageFromInputFile(stampLineageFile);
+ } else if ("deterministic-dsa-signing".equals(optionName)) {
+ deterministicDsaSigning = optionsParser.getOptionalBooleanValue(false);
+ } else if ("append-signature".equals(optionName)) {
+ otherSignersSignaturesPreserved = optionsParser.getOptionalBooleanValue(true);
} else {
throw new ParameterException(
"Unsupported option: " + optionOriginalForm + ". See --help for supported"
@@ -313,7 +321,8 @@ public class ApkSignerTool {
for (SignerParams signer : signers) {
signerNumber++;
signer.setName("signer #" + signerNumber);
- ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever);
+ ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever,
+ deterministicDsaSigning);
if (signerConfig == null) {
return;
}
@@ -322,7 +331,8 @@ public class ApkSignerTool {
if (sourceStampFlagFound) {
sourceStampSignerParams.setName("stamp signer");
sourceStampSignerConfig =
- getSignerConfig(sourceStampSignerParams, passwordRetriever);
+ getSignerConfig(sourceStampSignerParams, passwordRetriever,
+ deterministicDsaSigning);
if (sourceStampSignerConfig == null) {
return;
}
@@ -343,7 +353,7 @@ public class ApkSignerTool {
new ApkSigner.Builder(signerConfigs)
.setInputApk(inputApk)
.setOutputApk(tmpOutputApk)
- .setOtherSignersSignaturesPreserved(false)
+ .setOtherSignersSignaturesPreserved(otherSignersSignaturesPreserved)
.setV1SigningEnabled(v1SigningEnabled)
.setV2SigningEnabled(v2SigningEnabled)
.setV3SigningEnabled(v3SigningEnabled)
@@ -389,8 +399,8 @@ public class ApkSignerTool {
}
}
- private static ApkSigner.SignerConfig getSignerConfig(
- SignerParams signer, PasswordRetriever passwordRetriever) {
+ private static ApkSigner.SignerConfig getSignerConfig(SignerParams signer,
+ PasswordRetriever passwordRetriever, boolean deterministicDsaSigning) {
try {
signer.loadPrivateKeyAndCerts(passwordRetriever);
} catch (ParameterException e) {
@@ -422,7 +432,8 @@ public class ApkSignerTool {
}
ApkSigner.SignerConfig signerConfig =
new ApkSigner.SignerConfig.Builder(
- v1SigBasename, signer.getPrivateKey(), signer.getCerts())
+ v1SigBasename, signer.getPrivateKey(), signer.getCerts(),
+ deterministicDsaSigning)
.build();
return signerConfig;
}
@@ -1091,10 +1102,19 @@ public class ApkSignerTool {
}
Provider provider;
if (constructorParam != null) {
- // Single-arg Provider constructor
- provider =
- (Provider) providerClass.getConstructor(String.class)
- .newInstance(constructorParam);
+ try {
+ // Single-arg Provider constructor
+ provider =
+ (Provider) providerClass.getConstructor(String.class)
+ .newInstance(constructorParam);
+ } catch (NoSuchMethodException e) {
+ // Starting from JDK 9 the single-arg constructor accepting the configuration
+ // has been replaced by a configure(String) method to be invoked after
+ // instantiating the Provider with the no-arg constructor.
+ provider = (Provider) providerClass.getConstructor().newInstance();
+ provider = (Provider) providerClass.getMethod("configure", String.class)
+ .invoke(provider, constructorParam);
+ }
} else {
// No-arg Provider constructor
provider = (Provider) providerClass.getConstructor().newInstance();
diff --git a/src/apksigner/java/com/android/apksigner/SignerParams.java b/src/apksigner/java/com/android/apksigner/SignerParams.java
index 8c8b550..515cd41 100644
--- a/src/apksigner/java/com/android/apksigner/SignerParams.java
+++ b/src/apksigner/java/com/android/apksigner/SignerParams.java
@@ -209,10 +209,19 @@ public class SignerParams {
}
Provider ksProvider;
if (keystoreProviderArg != null) {
- // Single-arg Provider constructor
- ksProvider =
- (Provider) ksProviderClass.getConstructor(String.class)
- .newInstance(keystoreProviderArg);
+ try {
+ // Single-arg Provider constructor
+ ksProvider =
+ (Provider) ksProviderClass.getConstructor(String.class)
+ .newInstance(keystoreProviderArg);
+ } catch (NoSuchMethodException e) {
+ // Starting from JDK 9 the single-arg constructor accepting the configuration
+ // has been replaced by a configure(String) method to be invoked after
+ // instantiating the Provider with the no-arg constructor.
+ ksProvider = (Provider) ksProviderClass.getConstructor().newInstance();
+ ksProvider = (Provider) ksProviderClass.getMethod("configure",
+ String.class).invoke(ksProvider, keystoreProviderArg);
+ }
} else {
// No-arg Provider constructor
ksProvider = (Provider) ksProviderClass.getConstructor().newInstance();
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index 1285810..d66b7a3 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -90,6 +90,18 @@ certificate.
can also be specified; the lineage will then be read from
the signed data in the APK.
+--deterministic-dsa-signing When signing with the DSA signature algorithm,
+ whether to use the deterministic version as specified in
+ RFC 6979.
+
+--append-signature Appends the current signature to any signatures that
+ already exist within the APK. This option can be used
+ when an APK is signed by multiple independent signers to
+ allow each to add their own signature without needing to
+ share their private key. This option can also be used to
+ preserve existing key / value blocks that exist within the
+ APK signing block.
+
-h, --help Show help about this command and exit
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index d4da569..ca792c4 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -284,7 +284,8 @@ public class ApkSigner {
new DefaultApkSignerEngine.SignerConfig.Builder(
signerConfig.getName(),
signerConfig.getPrivateKey(),
- signerConfig.getCertificates())
+ signerConfig.getCertificates(),
+ signerConfig.getDeterministicDsaSigning())
.build());
}
DefaultApkSignerEngine.Builder signerEngineBuilder =
@@ -304,7 +305,8 @@ public class ApkSigner {
new DefaultApkSignerEngine.SignerConfig.Builder(
mSourceStampSignerConfig.getName(),
mSourceStampSignerConfig.getPrivateKey(),
- mSourceStampSignerConfig.getCertificates())
+ mSourceStampSignerConfig.getCertificates(),
+ mSourceStampSignerConfig.getDeterministicDsaSigning())
.build());
}
if (mSourceStampSigningCertificateLineage != null) {
@@ -510,6 +512,28 @@ public class ApkSigner {
}
}
+ // Step 7.5. Generate pinlist.meta file if necessary.
+ // This has to be before the step 8 so that the file is signed.
+ if (pinByteRanges != null) {
+ // Covers JAR signature and zip central dir entry.
+ // The signature files don't have to be pinned, but pinning them isn't that wasteful
+ // since the total size is small.
+ pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));
+ String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
+ byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
+
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
+ outputOffset +=
+ outputDataToOutputApk(
+ entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ }
+
// 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 =
@@ -520,15 +544,7 @@ public class ApkSigner {
String entryName = entry.getName();
byte[] uncompressedData = entry.getData();
- ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
- signerEngine.outputJarEntry(entryName);
- if (inspectEntryRequest != null) {
- inspectEntryRequest
- .getDataSink()
- .consume(uncompressedData, 0, uncompressedData.length);
- inspectEntryRequest.done();
- }
-
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
outputOffset +=
outputDataToOutputApk(
entryName,
@@ -542,21 +558,6 @@ public class ApkSigner {
outputJarSignatureRequest.done();
}
- if (pinByteRanges != null) {
- 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);
- outputOffset +=
- outputDataToOutputApk(
- entryName,
- uncompressedData,
- outputOffset,
- outputCdRecords,
- lastModifiedTimeForNewEntries,
- lastModifiedDateForNewEntries,
- outputApkOut);
- }
-
// Step 9. Construct output ZIP Central Directory in an in-memory buffer
long outputCentralDirSizeBytes = 0;
for (CentralDirectoryRecord record : outputCdRecords) {
@@ -616,6 +617,20 @@ public class ApkSigner {
}
}
+ private static void requestOutputEntryInspection(
+ ApkSignerEngine signerEngine,
+ String entryName,
+ byte[] uncompressedData)
+ throws IOException {
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(
+ uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest.done();
+ }
+ }
+
private static long outputDataToOutputApk(
String entryName,
byte[] uncompressedData,
@@ -954,14 +969,18 @@ public class ApkSigner {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
+ private boolean mDeterministicDsaSigning;
private SignerConfig(
- String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
-
/** Returns the name of this signer. */
public String getName() {
return mName;
@@ -980,11 +999,36 @@ public class ApkSigner {
return mCertificates;
}
+
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
/** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
+
+ /**
+ * 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.
+ * @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}.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
/**
* Constructs a new {@code Builder}.
@@ -994,14 +1038,21 @@ public class ApkSigner {
* @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}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic variant (RFC6979) should be used.
*/
- public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/**
@@ -1009,7 +1060,8 @@ public class ApkSigner {
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(mName, mPrivateKey, mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates,
+ mDeterministicDsaSigning);
}
}
}
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 90f2a6d..e2256da 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -18,6 +18,7 @@ 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.VERITY_PADDING_BLOCK_ID;
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;
@@ -64,6 +65,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -103,6 +105,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private final int mMinSdkVersion;
private final SigningCertificateLineage mSigningCertificateLineage;
+ private List<byte[]> mPreservedV2Signers = Collections.emptyList();
+ private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList();
+
private List<V1SchemeSigner.SignerConfig> mV1SignerConfigs = Collections.emptyList();
private DigestAlgorithm mV1ContentDigestAlgorithm;
@@ -159,6 +164,21 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+ /**
+ * A Set of block IDs to be discarded when requesting to preserve the original signatures.
+ */
+ private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS;
+ static {
+ DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3);
+ // The verity padding block is recomputed on an
+ // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID);
+ // The source stamp block is not currently preserved; appending a new signature scheme
+ // block will invalidate the previous source stamp.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID);
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
private DefaultApkSignerEngine(
List<SignerConfig> signerConfigs,
SignerConfig sourceStampSignerConfig,
@@ -176,10 +196,6 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
- if (otherSignersSignaturesPreserved) {
- throw new UnsupportedOperationException(
- "Preserving other signer's signatures is not yet implemented");
- }
mV1SigningEnabled = v1SigningEnabled;
mV2SigningEnabled = v2SigningEnabled;
@@ -255,6 +271,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
v1SignerConfig.privateKey = signerConfig.getPrivateKey();
v1SignerConfig.certificates = certificates;
v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
+ v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning();
// For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm
// of comparable strength to the digest algorithm used for computing the signature.
// When there are multiple signers, pick the strongest digest algorithm out of their
@@ -441,7 +458,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
V2SchemeSigner.getSuggestedSignatureAlgorithms(
publicKey,
mMinSdkVersion,
- apkSigningBlockPaddingSupported && mVerityEnabled);
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
break;
case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
try {
@@ -449,7 +467,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
V3SchemeSigner.getSuggestedSignatureAlgorithms(
publicKey,
mMinSdkVersion,
- apkSigningBlockPaddingSupported && mVerityEnabled);
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
} catch (InvalidKeyException e) {
// It is possible for a signer used for v1/v2 signing to not be allowed for use
@@ -463,7 +482,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
try {
newSignerConfig.signatureAlgorithms =
V4SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported);
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported,
+ signerConfig.getDeterministicDsaSigning());
} catch (InvalidKeyException e) {
// V4 is an optional signing schema, ok to proceed without.
newSignerConfig.signatureAlgorithms = null;
@@ -502,9 +522,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
@Override
@SuppressWarnings("AndroidJdkLibsChecker")
public Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
- V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result();
+ V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
- V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult);
+ V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
String entryName = entry.getKey();
@@ -543,11 +563,92 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
}
if (mOtherSignersSignaturesPreserved) {
- // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
- // in this engine.
+ boolean schemeSignatureBlockPreserved = false;
+ mPreservedSignatureBlocks = new ArrayList<>();
+ try {
+ List<Pair<byte[], Integer>> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
+ for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+ // If a V2 signature block is found and the engine is configured to use V2
+ // then save any of the previous signers that are not part of the current
+ // signing request.
+ if (mV2SigningEnabled) {
+ List<Pair<List<X509Certificate>, byte[]>> v2Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ mPreservedV2Signers = new ArrayList<>(v2Signers.size());
+ for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) {
+ if (!isConfiguredWithSigner(v2Signer.getFirst())) {
+ mPreservedV2Signers.add(v2Signer.getSecond());
+ schemeSignatureBlockPreserved = true;
+ }
+ }
+ } else {
+ // else V2 signing is not enabled; save the entire signature block to be
+ // added to the final APK signing block.
+ mPreservedSignatureBlocks.add(signatureBlock);
+ schemeSignatureBlockPreserved = true;
+ }
+ } else if (signatureBlock.getSecond()
+ == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+ // Preserving other signers in the presence of a V3 signature block is only
+ // supported if the engine is configured to resign the APK with the V3
+ // signature scheme, and the V3 signer in the signature block is the same
+ // as the engine is configured to use.
+ if (!mV3SigningEnabled) {
+ throw new IllegalStateException(
+ "Preserving an existing V3 signature is not supported");
+ }
+ List<Pair<List<X509Certificate>, byte[]>> v3Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ if (v3Signers.size() > 1) {
+ throw new IllegalArgumentException(
+ "The provided APK signing block contains " + v3Signers.size()
+ + " V3 signers; the V3 signature scheme only supports"
+ + " one signer");
+ }
+ // If there is only a single V3 signer then ensure it is the signer
+ // configured to sign the APK.
+ if (v3Signers.size() == 1
+ && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) {
+ throw new IllegalStateException(
+ "The V3 signature scheme only supports one signer; a request "
+ + "was made to preserve the existing V3 signature, "
+ + "but the engine is configured to sign with a "
+ + "different signer");
+ }
+ } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains(
+ signatureBlock.getSecond())) {
+ mPreservedSignatureBlocks.add(signatureBlock);
+ }
+ }
+ } catch (ApkFormatException | CertificateException | IOException e) {
+ throw new IllegalArgumentException("Unable to parse the provided signing block", e);
+ }
+ // Signature scheme V3+ only support a single signer; if the engine is configured to
+ // sign with V3+ then ensure no scheme signature blocks have been preserved.
+ if (mV3SigningEnabled && schemeSignatureBlockPreserved) {
+ throw new IllegalStateException(
+ "Signature scheme V3+ only supports a single signer and cannot be "
+ + "appended to the existing signature scheme blocks");
+ }
return;
}
- // TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
+ }
+
+ /**
+ * Returns whether the engine is configured to sign the APK with a signer using the specified
+ * {@code signerCerts}.
+ */
+ private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ if (signerCerts.containsAll(signerConfig.getCertificates())) {
+ return true;
+ }
+ }
+ return false;
}
@Override
@@ -864,6 +965,13 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
+ // If the engine is configured to preserve previous signature blocks and any were found in
+ // the existing APK signing block then add them to the list to be used to generate the
+ // new APK signing block.
+ if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null
+ && !mPreservedSignatureBlocks.isEmpty()) {
+ signingSchemeBlocks.addAll(mPreservedSignatureBlocks);
+ }
// create APK Signature Scheme V2 Signature if requested
if (mV2SigningEnabled) {
@@ -877,7 +985,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
zipCentralDirectory,
eocd,
v2SignerConfigs,
- mV3SigningEnabled);
+ mV3SigningEnabled,
+ mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
}
if (mV3SigningEnabled) {
@@ -1422,12 +1531,15 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
private SignerConfig(
- String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ String name, PrivateKey privateKey, List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
mName = name;
mPrivateKey = privateKey;
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/** Returns the name of this signer. */
@@ -1448,11 +1560,19 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
return mCertificates;
}
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
/** Builder of {@link SignerConfig} instances. */
public static class Builder {
private final String mName;
private final PrivateKey mPrivateKey;
private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
/**
* Constructs a new {@code Builder}.
@@ -1464,12 +1584,29 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
* the first certificate must correspond to the {@code privateKey}.
*/
public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
+
+ /**
+ * 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.
+ * @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}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic signing algorithm variant (RFC6979) should be used.
+ */
+ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
mName = name;
mPrivateKey = privateKey;
mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
}
/**
@@ -1477,7 +1614,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine {
* this builder.
*/
public SignerConfig build() {
- return new SignerConfig(mName, mPrivateKey, mCertificates);
+ return new SignerConfig(mName, mPrivateKey, mCertificates,
+ mDeterministicDsaSigning);
}
}
}
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index b8f1f8b..6c505be 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -124,6 +124,11 @@ public class SigningCertificateLineage {
return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities);
}
+ public static SigningCertificateLineage readFromBytes(byte[] lineageBytes)
+ throws IOException {
+ return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes)));
+ }
+
public static SigningCertificateLineage readFromFile(File file)
throws IOException {
if (file == null) {
@@ -283,6 +288,10 @@ public class SigningCertificateLineage {
return result;
}
+ public byte[] getBytes() {
+ return write().array();
+ }
+
public void writeToFile(File file) throws IOException {
if (file == null) {
throw new NullPointerException("file == null");
@@ -402,7 +411,8 @@ public class SigningCertificateLineage {
// TODO switch to one signature algorithm selection, or add support for multiple algorithms
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
- publicKey, mMinSdkVersion, false /* padding support */);
+ publicKey, mMinSdkVersion, false /* verityEnabled */,
+ false /* deterministicDsaSigning */);
return algorithms.get(0);
}
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 69399a7..426f0be 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -97,6 +97,27 @@ public abstract class ApkUtils {
}
/**
+ * Returns the APK Signing Block of the provided {@code apk}.
+ *
+ * @throws ApkFormatException if the APK is not a valid ZIP archive
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk)
+ throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
+ ApkUtils.ZipSections inputZipSections;
+ try {
+ inputZipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+ return findApkSigningBlock(apk, inputZipSections);
+ }
+
+ /**
* Returns the APK Signing Block of the provided APK.
*
* @throws IOException if an I/O error occurs
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 e8f6fc0..61b7b00 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -39,8 +39,10 @@ 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.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.util.VerityTreeBuilder;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
import com.android.apksig.internal.zip.ZipUtils;
@@ -65,6 +67,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.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
@@ -91,7 +94,7 @@ public class ApkSigningBlockUtils {
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;
+ public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
{CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
@@ -843,7 +846,7 @@ public class ApkSigningBlockUtils {
// uint64: size (excluding this field)
// uint32: ID
// (size - 4) bytes: value
- // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes)
+ // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
// uint64: size (same as the one above)
// uint128: magic
@@ -877,7 +880,6 @@ public class ApkSigningBlockUtils {
long blockSizeFieldValue = resultSize - 8L;
result.putLong(blockSizeFieldValue);
-
for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
int apkSignatureSchemeId = schemeBlockPair.getSecond();
@@ -898,6 +900,116 @@ public class ApkSigningBlockUtils {
}
/**
+ * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a
+ * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the
+ * contents / value of the signature block and the second element is the ID of the block.
+ *
+ * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock}
+ */
+ public static List<Pair<byte[], Integer>> getApkSignatureBlocks(
+ DataSource apkSigningBlock) throws IOException {
+ // FORMAT:
+ // uint64: size (excluding this field)
+ // repeated ID-value pairs:
+ // uint64: size (excluding this field)
+ // uint32: ID
+ // (size - 4) bytes: value
+ // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
+ // uint64: size (same as the one above)
+ // uint128: magic
+ long apkSigningBlockSize = apkSigningBlock.size();
+ if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) {
+ throw new IllegalArgumentException(
+ "APK signing block size out of range: " + apkSigningBlockSize);
+ }
+ // Remove the header and footer from the signing block to iterate over only the repeated
+ // ID-value pairs.
+ ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8,
+ (int) apkSigningBlock.size() - 32);
+ apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>();
+ while (apkSigningBlockBuffer.hasRemaining()) {
+ long blockLength = apkSigningBlockBuffer.getLong();
+ if (blockLength > Integer.MAX_VALUE || blockLength < 4) {
+ throw new IllegalArgumentException(
+ "Block index " + (signatureBlocks.size() + 1) + " size out of range: "
+ + blockLength);
+ }
+ int blockId = apkSigningBlockBuffer.getInt();
+ // Since the block ID has already been read from the signature block read the next
+ // blockLength - 4 bytes as the value.
+ byte[] blockValue = new byte[(int) blockLength - 4];
+ apkSigningBlockBuffer.get(blockValue);
+ signatureBlocks.add(Pair.of(blockValue, blockId));
+ }
+ return signatureBlocks;
+ }
+
+ /**
+ * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code
+ * List} of {@code Pair} instances where the first element is a {@code List} of {@link
+ * X509Certificate}s and the second element is a byte array of the individual signer's block.
+ *
+ * <p>This method supports any signature block that adheres to the following format up to the
+ * signing certificate(s):
+ * <pre>
+ * * length-prefixed sequence of length-prefixed signers
+ * * length-prefixed signed data
+ * * length-prefixed sequence of length-prefixed digests:
+ * * uint32: signature algorithm ID
+ * * length-prefixed bytes: digest of contents
+ * * length-prefixed sequence of certificates:
+ * * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ * </pre>
+ *
+ * <p>Note, this is a convenience method to obtain any signers from an existing signature block;
+ * the signature of each signer will not be verified.
+ *
+ * @throws ApkFormatException if an error is encountered while parsing the provided {@code
+ * signatureBlock}
+ * @throws CertificateException if the signing certificate(s) within an individual signer block
+ * cannot be parsed
+ */
+ public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners(
+ byte[] signatureBlock) throws ApkFormatException, CertificateException {
+ ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock);
+ signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer);
+ List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>();
+ while (signersBuffer.hasRemaining()) {
+ // Parse the next signer block, save all of its bytes for the resulting List, and
+ // rewind the buffer to allow the signing certificate(s) to be parsed.
+ ByteBuffer signer = getLengthPrefixedSlice(signersBuffer);
+ byte[] signerBytes = new byte[signer.remaining()];
+ signer.get(signerBytes);
+ signer.rewind();
+
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ // The first length prefixed slice is the sequence of digests which are not required
+ // when obtaining the signing certificate(s).
+ getLengthPrefixedSlice(signedData);
+ ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certificates = new ArrayList<>();
+ while (certificatesBuffer.hasRemaining()) {
+ int certLength = certificatesBuffer.getInt();
+ byte[] certBytes = new byte[certLength];
+ if (certLength > certificatesBuffer.remaining()) {
+ throw new IllegalArgumentException(
+ "Cert index " + (certificates.size() + 1) + " under signer index "
+ + (signers.size() + 1) + " size out of range: " + certLength);
+ }
+ certificatesBuffer.get(certBytes);
+ GuaranteedEncodedFormX509Certificate signerCert =
+ new GuaranteedEncodedFormX509Certificate(
+ X509CertificateUtils.generateCertificate(certBytes), certBytes);
+ certificates.add(signerCert);
+ }
+ signers.add(Pair.of(certificates, signerBytes));
+ }
+ return signers;
+ }
+
+ /**
* Computes the digests of the given APK components according to the algorithms specified in the
* given SignerConfigs.
*
diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
index d54f1e0..804eb37 100644
--- a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
@@ -102,6 +102,18 @@ public enum SignatureAlgorithm {
AndroidSdkVersion.INITIAL_RELEASE),
/**
+ * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
+ * deterministically according to RFC 6979.
+ */
+ DETDSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDetDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /**
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
* in little endian) construct the final digest.
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
index b4ae71a..9cd7b1f 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -26,7 +26,6 @@ import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSupportedSignature;
import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
import com.android.apksig.internal.apk.SignatureAlgorithm;
-import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
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
index 5ba3618..a215b98 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -148,7 +148,12 @@ public abstract class V2SourceStampVerifier {
apkContentDigests.entrySet()) {
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
}
- Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+ Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
+ @Override
+ public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
+ return pair1.getFirst() - pair2.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 6e9e0c3..85301ca 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
@@ -89,6 +89,11 @@ public abstract class V1SchemeSigner {
* Digest algorithm used for the signature.
*/
public DigestAlgorithm signatureDigestAlgorithm;
+
+ /**
+ * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
+ */
+ public boolean deterministicDsaSigning;
}
/** Hidden constructor to prevent instantiation. */
@@ -495,7 +500,8 @@ public abstract class V1SchemeSigner {
PublicKey publicKey = signingCert.getPublicKey();
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
Pair<String, AlgorithmIdentifier> signatureAlgs =
- getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm);
+ getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
+ signerConfig.deterministicDsaSigning);
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
// Generate the cryptographic signature of the signature file
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 c870a9e..b69b7d3 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
@@ -86,7 +86,8 @@ public abstract class V2SchemeSigner {
* Signature Scheme v2
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
- int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -111,7 +112,10 @@ public abstract class V2SchemeSigner {
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
- algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
@@ -138,13 +142,27 @@ public abstract class V2SchemeSigner {
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ boolean v3SigningEnabled)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
+ signerConfigs, v3SigningEnabled, null);
+ }
+
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
- boolean v3SigningEnabled)
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
@@ -152,19 +170,24 @@ public abstract class V2SchemeSigner {
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV2Block(
- digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled),
+ digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
+ preservedV2SignerBlocks),
digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
- boolean v3SigningEnabled)
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+ if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
+ signerBlocks.addAll(preservedV2SignerBlocks);
+ }
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
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 cab2a47..04260d5 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
@@ -74,7 +74,8 @@ public abstract class V3SchemeSigner {
* Signature Scheme v3
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
- int minSdkVersion, boolean verityEnabled) throws InvalidKeyException {
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
@@ -99,7 +100,10 @@ public abstract class V3SchemeSigner {
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
- algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
index e1e01a9..4ae7a53 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
@@ -45,6 +45,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
/**
* APK Signer Lineage.
@@ -274,6 +275,13 @@ public class V3SigningCertificateLineage {
return true;
}
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
+ result = 31 * result + Arrays.hashCode(signature);
+ return result;
+ }
+
/**
* the signing cert for this node. This is part of the data signed by the parent node.
*/
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 1a1ad93..74aa629 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -74,11 +74,12 @@ public abstract class V4SchemeSigner {
* Based on a public key, return a signing algorithm that supports verity.
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
- int minSdkVersion, boolean apkSigningBlockPaddingSupported)
+ int minSdkVersion, boolean apkSigningBlockPaddingSupported,
+ boolean deterministicDsaSigning)
throws InvalidKeyException {
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
signingKey, minSdkVersion,
- apkSigningBlockPaddingSupported);
+ apkSigningBlockPaddingSupported, deterministicDsaSigning);
// Keeping only supported algorithms.
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
final SignatureAlgorithm algorithm = iter.next();
@@ -170,7 +171,7 @@ public abstract class V4SchemeSigner {
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
- final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
+ final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
signingInfoNoSignature);
// Signing.
@@ -313,8 +314,6 @@ public abstract class V4SchemeSigner {
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:
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
index 0a8484b..a6cd9db 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -93,7 +93,7 @@ public abstract class V4SchemeVerifier {
V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
signature.signingInfo);
- final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, signingInfo);
// First, verify the signature over signedData.
ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
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
index e36ed60..deabe12 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -134,7 +134,7 @@ public class V4Signature {
writeBytes(stream, this.signingInfo);
}
- static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+ static byte[] getSignedData(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(
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 c27c487..4185dbc 100644
--- a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
+++ b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
@@ -77,7 +77,8 @@ public class AlgorithmIdentifier {
* when signing with the specified key and digest algorithm.
*/
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
- PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
+ PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
String jcaDigestPrefixForSigAlg;
switch (digestAlgorithm) {
@@ -115,7 +116,9 @@ public class AlgorithmIdentifier {
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
- return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
+ String signingAlgorithmName =
+ jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
+ return Pair.of(signingAlgorithmName, sigAlgId);
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withECDSA",
diff --git a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
index 8f9e1fd..2a890f6 100644
--- a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
+++ b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
@@ -34,6 +34,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
+
import javax.security.auth.x500.X500Principal;
/**
@@ -210,6 +211,7 @@ public class DelegatingX509Certificate extends X509Certificate {
}
@Override
+ @SuppressWarnings("AndroidJdkLibsChecker")
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
mDelegate.verify(key, sigProvider);
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 40255a4..d799201 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -37,6 +37,7 @@ import com.android.apksig.internal.apk.v2.V2SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.asn1.Asn1BerParser;
import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.internal.x509.RSAPublicKey;
import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
@@ -46,6 +47,9 @@ import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.zip.ZipFormatException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -59,14 +63,20 @@ import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.file.Files;
+import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
+import java.security.Security;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
@RunWith(JUnit4.class)
public class ApkSignerTest {
@@ -83,6 +93,8 @@ public class ApkSignerTest {
private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2";
private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3";
+ private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
+
// This is the same cert as above with the modulus reencoded to remove the leading 0 sign bit.
private static final String FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS =
"rsa-2048_negmod.x509.der";
@@ -90,6 +102,11 @@ public class ApkSignerTest {
private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
"rsa-2048-lineage-2-signers";
+ // These are the ID and value of an extra signature block within the APK signing block that
+ // can be preserved through the setOtherSignersSignaturesPreserved API.
+ private final int EXTRA_BLOCK_ID = 0x7e57c0de;
+ private final byte[] EXTRA_BLOCK_VALUE = {0, 1, 2, 3, 4, 5, 6, 7};
+
@Rule
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
@@ -365,6 +382,15 @@ public class ApkSignerTest {
.setV2SigningEnabled(true)
.setV3SigningEnabled(true)
.setVerityEnabled(true));
+
+ signGolden(
+ "pinsapp-unsigned.apk",
+ new File(outDir, "golden-pinsapp-signed.apk"),
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
}
private static void signGolden(
@@ -705,10 +731,53 @@ public class ApkSignerTest {
verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
}
+
+ @Test
+ public void testDeterministicDsaSignedVerifies() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ // Sign so that the APK is guaranteed to verify on API Level 1+
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+ assertVerified(verifyForMinSdkVersion(out, 1));
+
+ // Sign so that the APK is guaranteed to verify on API Level 21+
+ out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(21));
+ assertVerified(verifyForMinSdkVersion(out, 21));
+ // Does not verify on API Level 20 because DSA with SHA-256 not supported
+ assertVerificationFailure(
+ verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
+ @Test
+ public void testDeterministicDsaSigningIsDeterministic() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signers).setMinSdkVersion(1);
+ File first = sign(in, apkSignerBuilder);
+ File second = sign(in, apkSignerBuilder);
+
+ assertFileContentsEqual(first, second);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
@Test
public void testEcSignedVerifies() throws Exception {
List<ApkSigner.SignerConfig> signers =
- Collections.singletonList(getDefaultSignerConfigFromResources("ec-p256"));
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
String in = "original.apk";
// NOTE: EC APK signatures are not supported prior to API Level 18
@@ -1249,6 +1318,293 @@ public class ApkSignerTest {
assertSourceStampVerified(signedApk, sourceStampVerificationResult);
}
+ @Test
+ public void testSignApk_Pinlist() throws Exception {
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig =
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ assertGolden(
+ "pinsapp-unsigned.apk",
+ "golden-pinsapp-signed.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setVerityEnabled(true));
+ assertTrue("pinlist.meta file must be in the signed APK.",
+ resourceZipFileContains("golden-pinsapp-signed.apk", "pinlist.meta"));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_extraSigBlock_signatureAppended()
+ throws Exception {
+ // The DefaultApkSignerEngine contains support to append a signature to an existing
+ // signing block; any existing signature blocks within the APK signing block should be
+ // left intact except for the original verity padding block (since this is regenerated) and
+ // the source stamp. This test verifies that an extra signature block is still in
+ // the APK signing block after appending a V2 signature.
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v1Only_signatureAppended() throws Exception {
+ // This test verifies appending an additional V1 signature to an existing V1 signer behaves
+ // similar to jarsigner where the APK is then verified as signed by both signers.
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v1-only-with-rsa-2048.apk",
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v3OnlyDifferentSigner_throwsException()
+ throws Exception {
+ // The V3 Signature Scheme only supports a single signer; if an attempt is made to append
+ // a different signer to a V3 signature then an exception should be thrown.
+ // The APK used for this test is signed with the ec-p256 signer so use the rsa-2048 to
+ // attempt to append a different signature.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertThrows(IllegalStateException.class, () ->
+ sign("v3-only-with-stamp.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true))
+ );
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v2OnlyAppendV2V3SameSigner_signatureAppended()
+ throws Exception {
+ // A V2 and V3 signature can be appended to an existing V2 signature if the same signer is
+ // used to resign the APK; this could be used in a case where an APK was previously signed
+ // with just the V2 signature scheme along with additional non-APK signing scheme signature
+ // blocks and the signer wanted to preserve those existing blocks.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME);
+ assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v2OnlyAppendV3SameSigner_throwsException()
+ throws Exception {
+ // A V3 only signature cannot be appended to an existing V2 signature, even when using the
+ // same signer, since the V2 signature would then not contain the stripping protection for
+ // the V3 signature. If the same signer is being used then the signer should be configured
+ // to resign using the V2 signature scheme as well as the V3 signature scheme.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+
+ assertThrows(IllegalStateException.class, () ->
+ sign("v2-rsa-2048-with-extra-sig-block.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(false)
+ .setV3SigningEnabled(true)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true)));
+ }
+
+ @Test
+ public void testOtherSignersSignaturesPreserved_v1v2IndividuallySign_signaturesAppended()
+ throws Exception {
+ // One of the primary requirements for appending signatures is when an APK has already
+ // released with two signers; with the minimum signature scheme v2 requirement for target
+ // SDK version 30+ each signer must be able to append their signature to the existing
+ // signature block. This test verifies an APK with appended signatures verifies as expected
+ // after a series of appending V1 and V2 signatures.
+ List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
+
+ // When two parties are signing an APK the first must sign with both V1 and V2; this will
+ // write the stripping-protection attribute to the V1 signature.
+ File signedApk = sign("original.apk",
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false));
+
+ // The second party can then append their signature with both the V1 and V2 signature; this
+ // will invalidate the V2 signature of the initial signer since the APK itself will be
+ // modified with this signers V1 / jar signature.
+ signedApk = sign(signedApk,
+ new ApkSigner.Builder(ecP256SignerConfig)
+ .setV1SigningEnabled(true)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ // The first party will then need to resign with just the V2 signature after its previous
+ // signature was invalidated by the V1 signature of the second signer; however since this
+ // signature is appended its previous V2 signature should be removed from the signature
+ // block and replaced with this new signature while preserving the V2 signature of the
+ // other signer.
+ signedApk = sign(signedApk,
+ new ApkSigner.Builder(rsa2048SignerConfig)
+ .setV1SigningEnabled(false)
+ .setV2SigningEnabled(true)
+ .setV3SigningEnabled(false)
+ .setV4SigningEnabled(false)
+ .setOtherSignersSignaturesPreserved(true));
+
+ ApkVerifier.Result result = verify(signedApk, null);
+ assertVerified(result);
+ assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME,
+ EC_P256_SIGNER_RESOURCE_NAME);
+ }
+
+ /**
+ * Asserts the provided {@code signedApk} contains a signature block with the expected
+ * {@code byte[]} value and block ID as specified in the {@code expectedBlock}.
+ */
+ private static void assertSigningBlockContains(File signedApk,
+ Pair<byte[], Integer> expectedBlock) throws Exception {
+ try (RandomAccessFile apkFile = new RandomAccessFile(signedApk, "r")) {
+ ApkUtils.ApkSigningBlock apkSigningBlock = ApkUtils.findApkSigningBlock(
+ DataSources.asDataSource(apkFile));
+ List<Pair<byte[], Integer>> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock.getContents());
+ for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond().equals(expectedBlock.getSecond())) {
+ if (Arrays.equals(signatureBlock.getFirst(), expectedBlock.getFirst())) {
+ return;
+ }
+ }
+ }
+ fail(String.format(
+ "The APK signing block did not contain the expected block with ID %08x",
+ expectedBlock.getSecond()));
+ }
+ }
+
+ /**
+ * Asserts the provided verification {@code result} contains the expected {@code signers} for
+ * each scheme that was used to verify the APK's signature.
+ */
+ private static void assertResultContainsSigners(ApkVerifier.Result result, String... signers)
+ throws Exception {
+ // A result must be successfully verified before verifying any of the result's signers.
+ assertTrue(result.isVerified());
+
+ List<X509Certificate> expectedSigners = new ArrayList<>();
+ for (String signer : signers) {
+ ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer);
+ expectedSigners.addAll(signerConfig.getCertificates());
+ }
+
+ if (result.isVerifiedUsingV1Scheme()) {
+ Set<X509Certificate> v1Signers = new HashSet<>();
+ for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
+ v1Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v1Signers.size());
+ assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers),
+ v1Signers.containsAll(expectedSigners));
+ }
+
+ if (result.isVerifiedUsingV2Scheme()) {
+ Set<X509Certificate> v2Signers = new HashSet<>();
+ for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
+ v2Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v2Signers.size());
+ assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers),
+ v2Signers.containsAll(expectedSigners));
+ }
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ Set<X509Certificate> v3Signers = new HashSet<>();
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ v3Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v3Signers.size());
+ assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers),
+ v3Signers.containsAll(expectedSigners));
+ }
+ }
+
+ /**
+ * Returns a comma delimited {@code String} containing all of the Subject Names from the
+ * provided {@code certificates}.
+ */
+ private static String getAllSubjectNamesFrom(Collection<X509Certificate> certificates) {
+ StringBuilder result = new StringBuilder();
+ for (X509Certificate certificate : certificates) {
+ if (result.length() > 0) {
+ result.append(", ");
+ }
+ result.append(certificate.getSubjectDN().getName());
+ }
+ return result.toString();
+ }
+
+ private static boolean resourceZipFileContains(String resourceName, String zipEntryName)
+ throws IOException {
+ ZipInputStream zip = new ZipInputStream(
+ Resources.toInputStream(ApkSignerTest.class, resourceName));
+ while (true) {
+ ZipEntry entry = zip.getNextEntry();
+ if (entry == null) {
+ break;
+ }
+
+ if (entry.getName().equals(zipEntryName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private RSAPublicKey getRSAPublicKeyFromSigningBlock(File apk, int signatureVersionId)
throws Exception {
int signatureVersionBlockId;
@@ -1358,11 +1714,21 @@ public class ApkSignerTest {
}
}
- private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
- throws Exception {
+ private File sign(File inApkFile, ApkSigner.Builder apkSignerBuilder) throws Exception {
+ try (RandomAccessFile apkFile = new RandomAccessFile(inApkFile, "r")) {
+ DataSource in = DataSources.asDataSource(apkFile);
+ return sign(in, apkSignerBuilder);
+ }
+ }
+
+ private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName)));
+ return sign(in, apkSignerBuilder);
+ }
+
+ private File sign(DataSource in, ApkSigner.Builder apkSignerBuilder) throws Exception {
File outFile = mTemporaryFolder.newFile();
apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
@@ -1412,13 +1778,24 @@ public class ApkSignerTest {
ApkVerifierTest.assertVerificationFailure(result, expectedIssue);
}
+ private void assertFileContentsEqual(File first, File second) throws IOException {
+ assertArrayEquals(Files.readAllBytes(Paths.get(first.getPath())),
+ Files.readAllBytes(Paths.get(second.getPath())));
+ }
+
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, false);
+ }
+
+ private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
+ String keyNameInResources, boolean deterministicDsaSigning) throws Exception {
PrivateKey privateKey =
Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8");
List<X509Certificate> certs =
Resources.toCertificateChain(ApkSignerTest.class, keyNameInResources + ".x509.pem");
- return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
+ return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs,
+ deterministicDsaSigning).build();
}
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
@@ -1429,4 +1806,9 @@ public class ApkSignerTest {
Resources.toCertificateChain(ApkSignerTest.class, certNameInResources);
return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
}
+
+ private static ApkSigner.SignerConfig getDeterministicDsaSignerConfigFromResources(
+ String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, true);
+ }
}
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index 14cab83..d5dc71d 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -90,6 +90,23 @@ public class SigningCertificateLineageTest {
}
@Test
+ public void testLineageFromBytesContainsExpectedSigners() throws Exception {
+ // This file contains the lineage with the three rsa-2048 signers
+ DataSource lineageDataSource = Resources.toDataSource(getClass(),
+ "rsa-2048-lineage-3-signers");
+ SigningCertificateLineage lineage = SigningCertificateLineage.readFromBytes(
+ lineageDataSource.getByteBuffer(0, (int) lineageDataSource.size()).array());
+ List<SignerConfig> signers = new ArrayList<>(3);
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME));
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+ signers.add(
+ Resources.toLineageSignerConfig(getClass(), THIRD_RSA_2048_SIGNER_RESOURCE_NAME));
+ assertLineageContainsExpectedSigners(lineage, signers);
+ }
+
+ @Test
public void testLineageFromFileContainsExpectedSigners() throws Exception {
// This file contains the lineage with the three rsa-2048 signers
DataSource lineageDataSource = Resources.toDataSource(getClass(),
@@ -131,6 +148,17 @@ public class SigningCertificateLineageTest {
}
@Test
+ public void testLineageWrittenToBytesContainsExpectedSigners() throws Exception {
+ SigningCertificateLineage lineage = createLineageWithSignersFromResources(
+ FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+ lineage = updateLineageWithSignerFromResources(lineage,
+ THIRD_RSA_2048_SIGNER_RESOURCE_NAME);
+ byte[] lineageBytes = lineage.getBytes();
+ lineage = SigningCertificateLineage.readFromBytes(lineageBytes);
+ assertLineageContainsExpectedSigners(lineage, mSigners);
+ }
+
+ @Test
public void testLineageWrittenToFileContainsExpectedSigners() throws Exception {
SigningCertificateLineage lineage = createLineageWithSignersFromResources(
FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
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 85e9e90..8396d76 100644
--- a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
+++ b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java
@@ -79,7 +79,7 @@ public final class VerityTreeBuilderTest {
return DataSources.asDataSource(ByteBuffer.wrap(data.getBytes(UTF_8)));
}
- @Test public void generateVerityTreeRootHashFromDummyDataSource() throws Exception {
+ @Test public void generateVerityTreeRootHashFromPlaceholderDataSource() throws Exception {
// This sample was taken from src/test/resources/com/android/apksig/original.apk.
byte[] sampleEoCDFromDisk = new byte[] {
0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x79, 0x01,
diff --git a/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk
new file mode 100644
index 0000000..43c39f1
--- /dev/null
+++ b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/pinsapp-unsigned.apk b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk
new file mode 100755
index 0000000..b6a6e8f
--- /dev/null
+++ b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk
new file mode 100644
index 0000000..61f4122
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
new file mode 100644
index 0000000..94b54c9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
Binary files differ