diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2021-06-19 12:08:11 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2021-06-19 12:08:11 +0000 |
commit | d370ab99d1dcb18dfefc86acffa4c5fd62d464ce (patch) | |
tree | 9377b651b9a765ffbc05ec4a394972c54446e1fa | |
parent | 705e0690285758aae423a157d17c6312d92d7343 (diff) | |
parent | ab50f0a22c4a770bb914d18483a6cc1dd930182b (diff) | |
download | apksig-android12-mainline-media-release.tar.gz |
Snap for 7474514 from ab50f0a22c4a770bb914d18483a6cc1dd930182b to mainline-media-releaseandroid-mainline-12.0.0_r89android-mainline-12.0.0_r74android-mainline-12.0.0_r62android-mainline-12.0.0_r46android-mainline-12.0.0_r29android-mainline-12.0.0_r12android-mainline-12.0.0_r119android-mainline-12.0.0_r104android12-mainline-media-release
Change-Id: Ib2e89d15d34f8c4b7efa57c6621ee4e1edefea9a
29 files changed, 954 insertions, 91 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..43c39f1 --- /dev/null +++ b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk diff --git a/src/test/resources/com/android/apksig/pinsapp-unsigned.apk b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk Binary files differnew file mode 100755 index 0000000..b6a6e8f --- /dev/null +++ b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk 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 Binary files differnew file mode 100644 index 0000000..61f4122 --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk 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 Binary files differnew file mode 100644 index 0000000..94b54c9 --- /dev/null +++ b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk |