Add initial support for appending signatures to an APK

The V1 and V2 signature schemes support multiple signers, but
currently apksig does not support appending a V1 or V2 signature
to an already signed APK. This commit adds the initial support to
append a V1 / V2 signature to an already signed APK through one of
the following options:
- The --append-signature option when signing via apksigner
- ApkSigner.Builder#setOtherSignersSignaturePreserved(true) when
  using the apksig Java API
These options can also be used when an APK has additional key / value
attribute blocks within the APK signing block; using the same signing
config will overwrite the existing signatures while preserving these
additional blocks.

Note, when an APK has multiple V1 and V2 signatures one signer must
sign the APK multiple times since another signer appending their
V1 signature will invalidate an existing V2 signature. The following
process will allow two signers, A and B, to sign an APK with V1 and
V2 signatures:
1. A signs the APK with a V1 and V2 signature; this will add the
   stripping protection attribute to A's V1 signature.
2. B uses one of the append options listed above to sign the APK
   with a V1 and V2 signature. This will invalidate A's V2
   signature since V2 is a whole APK signature scheme and B's V1
   signature will modify the APK, but A's V1 signature will still
   be valid.
3. A uses one of the append options listed above to sign the APK
   with a V2 signature. This will overwrite A's previous V2
   signature using the updated contents of the APK and append
   this signature to B's existing signature in the V2 signature
   block.

Bug: 186395570
Test: gradlew build
Change-Id: I8ad2f21e111dcdc5e2c41695439794afba9cc1de
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