Add initial support for appending signatures to an APK am: b22ac09e78

Original change: https://googleplex-android-review.googlesource.com/c/platform/tools/apksig/+/14708776

Change-Id: I76312c6b04ae64bbcd4dc9571295111c3d77552d
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 @@
         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 @@
                 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 @@
                 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 @@
                       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 @@
 
 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.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 @@
     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 @@
 
     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 @@
         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 @@
         }
 
         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 @@
         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 @@
                             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 @@
     }
 
     /**
+     * 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.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.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 @@
               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 @@
         //     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 @@
         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 @@
     }
 
     /**
+     * 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 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 @@
                         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.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.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 @@
     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 @@
     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 @@
     @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 @@
             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 @@
         }
     }
 
-    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
new file mode 100644
index 0000000..61f4122
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
new file mode 100644
index 0000000..94b54c9
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk
Binary files differ