diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2021-05-29 01:10:01 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2021-05-29 01:10:01 +0000 |
commit | f5abfd383f206313070a3e95ef35c3d9973c5bcb (patch) | |
tree | 9377b651b9a765ffbc05ec4a394972c54446e1fa | |
parent | d0be60412f5e9b535f8c169c19e01fbbdee970ea (diff) | |
parent | 5d01d1d2178e9841c730c589d2f8d5f716cef32b (diff) | |
download | apksig-android12L-d2-s1-release.tar.gz |
Snap for 7408667 from 5d01d1d2178e9841c730c589d2f8d5f716cef32b to sc-d2-releaseandroid-12.1.0_r26android-12.1.0_r25android-12.1.0_r24android-12.1.0_r23android-12.1.0_r18android-12.1.0_r17android-12.1.0_r16android-12.1.0_r15android-12.1.0_r14android-12.1.0_r13android-12.1.0_r12android12L-d2-s8-releaseandroid12L-d2-s7-releaseandroid12L-d2-s6-releaseandroid12L-d2-s5-releaseandroid12L-d2-s4-releaseandroid12L-d2-s3-releaseandroid12L-d2-s2-releaseandroid12L-d2-s1-releaseandroid12L-d2-release
Change-Id: Ia8ab5bb5a8adc80c0ac6c0a7d973eafb32ad495d
-rw-r--r-- | src/apksigner/java/com/android/apksigner/ApkSignerTool.java | 5 | ||||
-rw-r--r-- | src/apksigner/java/com/android/apksigner/help_sign.txt | 8 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/DefaultApkSignerEngine.java | 121 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/apk/ApkUtils.java | 21 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java | 118 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java | 25 | ||||
-rw-r--r-- | src/test/java/com/android/apksig/ApkSignerTest.java | 281 | ||||
-rw-r--r-- | src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk | bin | 0 -> 4623 bytes | |||
-rw-r--r-- | src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk | bin | 0 -> 12703 bytes |
9 files changed, 561 insertions, 18 deletions
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java index 6bc00d2..9fd0c34 100644 --- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java +++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java @@ -160,6 +160,7 @@ public class ApkSignerTool { 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))) { @@ -262,6 +263,8 @@ public class ApkSignerTool { 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" @@ -350,7 +353,7 @@ public class ApkSignerTool { new ApkSigner.Builder(signerConfigs) .setInputApk(inputApk) .setOutputApk(tmpOutputApk) - .setOtherSignersSignaturesPreserved(false) + .setOtherSignersSignaturesPreserved(otherSignersSignaturesPreserved) .setV1SigningEnabled(v1SigningEnabled) .setV2SigningEnabled(v2SigningEnabled) .setV3SigningEnabled(v3SigningEnabled) diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt index 88c27a1..d66b7a3 100644 --- a/src/apksigner/java/com/android/apksigner/help_sign.txt +++ b/src/apksigner/java/com/android/apksigner/help_sign.txt @@ -94,6 +94,14 @@ certificate. 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/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index c108191..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; @@ -547,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 @@ -868,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) { @@ -881,7 +985,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { zipCentralDirectory, eocd, v2SignerConfigs, - mV3SigningEnabled); + mV3SigningEnabled, + mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null); signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock); } if (mV3SigningEnabled) { 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 b91ccd8..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 placeholder 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/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java index 156a163..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 @@ -142,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 = @@ -156,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/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java index 830d571..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; @@ -70,8 +71,12 @@ 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 { @@ -88,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"; @@ -95,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(); @@ -764,7 +776,8 @@ public class ApkSignerTest { @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 @@ -1322,6 +1335,258 @@ public class ApkSignerTest { 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( @@ -1449,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); 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 |