Merge "Handle OOM error when allocating space for signature block" into main
diff --git a/Android.bp b/Android.bp
index 8f3c7a6..86b1643 100644
--- a/Android.bp
+++ b/Android.bp
@@ -35,14 +35,10 @@
 
 java_library_host {
     name: "apksig",
-    srcs: [
-        "src/main/java/**/*.java",
-        "src/stub/java/**/*.java",
-    ],
+    srcs: ["src/main/java/**/*.java"],
     exclude_srcs: [
         "src/main/java/com/android/apksig/kms/aws/**/*.java",
         "src/main/java/com/android/apksig/kms/gcp/**/*.java",
-        "src/main/java/com/android/apksig/kms/KmsSignerEngine.java",
     ],
     java_version: "1.8",
     target: {
@@ -53,17 +49,34 @@
 }
 
 java_library_host {
-    name: "apksig-kms",
+    name: "apksig-kms-provider-aws",
     srcs: [
-        "src/main/java/**/*.java",
+        "src/main/java/com/android/apksig/kms/aws/**/*.java",
     ],
     libs: [
+        "apksig",
         "awssdk-kms",
         "awssdk-url-connection-client",
-        "google-cloud-kms",
-        "libprotobuf-java-util-full",
-        "slf4j-api",
     ],
+    services: ["src/providers/aws/*"],
+    java_version: "1.8",
+    target: {
+        windows: {
+            enabled: true,
+        },
+    },
+}
+
+java_library_host {
+    name: "apksig-kms-provider-gcp",
+    srcs: [
+        "src/main/java/com/android/apksig/kms/gcp/**/*.java",
+    ],
+    libs: [
+        "apksig",
+        "google-cloud-kms",
+    ],
+    services: ["src/providers/gcp/*"],
     java_version: "1.8",
     target: {
         windows: {
@@ -106,7 +119,9 @@
     defaults: ["apksigner-defaults"],
     wrapper: "etc/apksigner-kms",
     static_libs: [
-        "apksig-kms",
+        "apksig",
+        "apksig-kms-provider-aws",
+        "apksig-kms-provider-gcp",
         "awssdk-kms",
         "awssdk-url-connection-client",
         "conscrypt-unbundled",
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 9aa811d..8a6c84d 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,8 +1,5 @@
 [Builtin Hooks]
 google_java_format = true
 
-[Builtin Hooks Options]
-google_java_format = --sort-imports
-
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 4d3f195..519fe66 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -265,6 +265,12 @@
                 signerParams.setKeystoreProviderArg(
                         optionsParser.getRequiredValue(
                                 "JCA KeyStore Provider constructor argument"));
+            } else if ("kms-type".equals(optionName)) {
+                signerParams.setKmsType(
+                        optionsParser.getRequiredValue("Key Management Service (KMS) type"));
+            } else if ("kms-key-alias".equals(optionName)) {
+                signerParams.setKmsKeyAlias(
+                        optionsParser.getRequiredValue("Key Management Service (KMS) key alias"));
             } else if ("key".equals(optionName)) {
                 signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file"));
             } else if ("cert".equals(optionName)) {
@@ -471,6 +477,8 @@
             } else {
                 v1SigBasename = keyFileName.substring(0, delimiterIndex);
             }
+        } else if (signer.getKmsKeyAlias() != null) {
+            v1SigBasename = signer.getKmsKeyAlias();
         } else {
             throw new RuntimeException("Neither KeyStore key alias nor private key file available");
         }
@@ -672,6 +680,10 @@
                             verbose, printCertsPem);
                 }
             }
+            if (sourceStampInfo != null && verbose) {
+                System.out.println(
+                        "Source Stamp Timestamp: " + sourceStampInfo.getTimestampEpochSeconds());
+            }
         } else {
             System.err.println("DOES NOT VERIFY");
         }
@@ -1068,6 +1080,10 @@
                 signerParams.setKeystoreProviderArg(
                         optionsParser.getRequiredValue(
                                 "JCA KeyStore Provider constructor argument"));
+            } else if ("kms-type".equals(optionName)) {
+                signerParams.setKmsType(optionsParser.getRequiredValue("KMS Type"));
+            } else if ("kms-key-alias".equals(optionName)) {
+                signerParams.setKmsKeyAlias(optionsParser.getRequiredValue("KMS Key Alias"));
             } else if ("key".equals(optionName)) {
                 signerParams.setKeyFile(optionsParser.getRequiredValue("Private key file"));
             } else if ("cert".equals(optionName)) {
diff --git a/src/apksigner/java/com/android/apksigner/SignerParams.java b/src/apksigner/java/com/android/apksigner/SignerParams.java
index 0736738..8f4f323 100644
--- a/src/apksigner/java/com/android/apksigner/SignerParams.java
+++ b/src/apksigner/java/com/android/apksigner/SignerParams.java
@@ -16,13 +16,11 @@
 
 package com.android.apksigner;
 
-import static java.util.Arrays.stream;
 
 import com.android.apksig.KeyConfig;
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
 import com.android.apksig.internal.util.X509CertificateUtils;
-import com.android.apksig.kms.KmsType;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -72,6 +70,8 @@
 
     private String keyFile;
     private String certFile;
+    private String mKmsType;
+    private String mKmsKeyAlias;
 
     private String v1SigFileBasename;
 
@@ -139,6 +139,18 @@
         this.keyFile = keyFile;
     }
 
+    public void setKmsType(String mKmsType) {
+        this.mKmsType = mKmsType;
+    }
+
+    public String getKmsKeyAlias() {
+        return mKmsKeyAlias;
+    }
+
+    public void setKmsKeyAlias(String mKmsKeyAlias) {
+        this.mKmsKeyAlias = mKmsKeyAlias;
+    }
+
     public void setCertFile(String certFile) {
         this.certFile = certFile;
     }
@@ -205,23 +217,20 @@
                 && (certFile == null)
                 && (v1SigFileBasename == null)
                 && (mKeyConfig == null)
-                && (certs == null);
+                && (certs == null)
+                && (mKmsType == null)
+                && (mKmsKeyAlias == null);
     }
 
     public void loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever) throws Exception {
-        KmsType kmsType =
-                stream(KmsType.values())
-                        .filter(v -> v.toString().equalsIgnoreCase(keystoreType))
-                        .findFirst()
-                        .orElse(null);
-
-        if (kmsType != null) {
-            if (keystoreKeyAlias == null) {
+        if (mKmsType != null) {
+            if (mKmsKeyAlias == null) {
                 throw new ParameterException(
-                        "key alias (--ks-key-alias) is required if ks-type is a cloud KMS");
+                        "kms key alias (--kms-key-alias) is required if kms type (--kms-type) is"
+                                + " provided");
             }
             certs = loadCertsFromFile(certFile);
-            mKeyConfig = new KeyConfig.Kms(kmsType, keystoreKeyAlias);
+            mKeyConfig = new KeyConfig.Kms(mKmsType, mKmsKeyAlias);
             return;
         }
 
@@ -244,8 +253,8 @@
         }
 
         throw new ParameterException(
-                "KeyStore (--ks), private key file (--key), or key alias and cloud provider"
-                        + " (--ks-key-alias and --ks-type) must be specified");
+                "KeyStore (--ks), private key file (--key), or KMS key alias and type"
+                        + " (--kms-key-alias and --kms-type) must be specified");
     }
 
     private void loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever)
diff --git a/src/main/java/com/android/apksig/KeyConfig.java b/src/main/java/com/android/apksig/KeyConfig.java
index 8f9aa15..71179b8 100644
--- a/src/main/java/com/android/apksig/KeyConfig.java
+++ b/src/main/java/com/android/apksig/KeyConfig.java
@@ -16,7 +16,6 @@
 
 package com.android.apksig;
 
-import com.android.apksig.kms.KmsType;
 
 import java.security.PrivateKey;
 import java.util.function.Function;
@@ -50,7 +49,7 @@
 
     /** For signing via a Key Management Service (KMS). */
     public static class Kms extends KeyConfig {
-        public final KmsType kmsType;
+        public final String kmsType;
         public final String keyAlias;
 
         @Override
@@ -58,7 +57,7 @@
             return kms.apply(this);
         }
 
-        public Kms(KmsType kmsType, String keyAlias) {
+        public Kms(String kmsType, String keyAlias) {
             this.kmsType = kmsType;
             this.keyAlias = keyAlias;
         }
diff --git a/src/main/java/com/android/apksig/SignerEngineFactory.java b/src/main/java/com/android/apksig/SignerEngineFactory.java
index ee20c58..4301ac7 100644
--- a/src/main/java/com/android/apksig/SignerEngineFactory.java
+++ b/src/main/java/com/android/apksig/SignerEngineFactory.java
@@ -16,9 +16,12 @@
 
 package com.android.apksig;
 
-import com.android.apksig.kms.KmsSignerEngine;
+import com.android.apksig.kms.KmsException;
+import com.android.apksig.kms.KmsSignerEngineProvider;
 
 import java.security.spec.AlgorithmParameterSpec;
+import java.util.Objects;
+import java.util.ServiceLoader;
 
 /** Simple util to fetch a signer engine based on provided config values. */
 public class SignerEngineFactory {
@@ -41,6 +44,23 @@
                 jca ->
                         new JcaSignerEngine(
                                 jca.privateKey, jcaSignatureAlgorithm, algorithmParameterSpec),
-                kms -> KmsSignerEngine.fromKmsConfig(kms, jcaSignatureAlgorithm));
+                kms -> getKmsImplementation(kms, jcaSignatureAlgorithm, algorithmParameterSpec));
+    }
+
+    private static SignerEngine getKmsImplementation(
+            KeyConfig.Kms keyConfig,
+            String jcaSignatureAlgorithm,
+            AlgorithmParameterSpec algorithmParameterSpec) {
+        ServiceLoader<KmsSignerEngineProvider> providers =
+                ServiceLoader.load(KmsSignerEngineProvider.class);
+        for (KmsSignerEngineProvider provider : providers) {
+            if (Objects.equals(provider.getKmsType(), keyConfig.kmsType)) {
+                return provider.getInstance(
+                        keyConfig, jcaSignatureAlgorithm, algorithmParameterSpec);
+            }
+        }
+
+        throw new KmsException(
+                keyConfig.kmsType, "No SignerEngine implementation found on the classpath");
     }
 }
diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java
index 1af64f8..1af8fd4 100644
--- a/src/main/java/com/android/apksig/SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -942,10 +942,6 @@
 
         private final int mCallerConfiguredFlags;
 
-        private SignerCapabilities(int flags) {
-            this(flags, 0);
-        }
-
         private SignerCapabilities(int flags, int callerConfiguredFlags) {
             mFlags = flags;
             mCallerConfiguredFlags = callerConfiguredFlags;
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 156ea17..1a0db19 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -353,6 +353,10 @@
      * @throws CodenameMinSdkVersionException if the {@code codename} is not supported
      */
     static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
+        if ("Baklava".equals(codename)) {
+            return 34; // VIC (35) was the version before Baklava, return VIC version minus one
+        }
+
         char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
         // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
         // We only look at the first letter of the codename as this is the most important letter.
@@ -373,7 +377,7 @@
             // element at insertionIndex (if present) is greater than firstChar.
             int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
             if (insertionIndex == 0) {
-                // 'A' or 'B' -- never released to public
+                // 'A' or 'B' (not Baklava) -- never released to public
                 return 1;
             } else {
                 // The element at insertionIndex - 1 is the newest older codename.
diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
index 7bf952d..416cf87 100644
--- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -180,9 +180,6 @@
         if (signerConfig.certificates.isEmpty()) {
             throw new SignatureException("No certificates configured for signer");
         }
-        if (signerConfig.certificates.size() != 1) {
-            throw new CertificateEncodingException("Should only have one certificate");
-        }
 
         // Collecting data for signing.
         final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
index 81026ba..5c1f407 100644
--- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
+++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -28,13 +28,11 @@
 import java.nio.ByteOrder;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-
 import java.util.ArrayList;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Phaser;
 import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 /**
  * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
@@ -60,10 +58,6 @@
      * Typical prefetch size.
      */
     private final static int MAX_PREFETCH_CHUNKS = 1024;
-    /**
-     * Minimum chunks to be processed by a single worker task.
-     */
-    private final static int MIN_CHUNKS_PER_WORKER = 8;
 
     /**
      * Digest algorithm (JCA Digest algorithm name) used in the tree.
diff --git a/src/main/java/com/android/apksig/kms/KmsException.java b/src/main/java/com/android/apksig/kms/KmsException.java
index 47a124d..9daceaa 100644
--- a/src/main/java/com/android/apksig/kms/KmsException.java
+++ b/src/main/java/com/android/apksig/kms/KmsException.java
@@ -18,25 +18,25 @@
 
 /** Represents an exception thrown by the external KMS. */
 public class KmsException extends RuntimeException {
-    private final KmsType mKmsType;
+    private final String mKmsType;
 
-    public KmsException(KmsType kmsType, String message) {
+    public KmsException(String kmsType, String message) {
         super(message);
         this.mKmsType = kmsType;
     }
 
-    public KmsException(KmsType kmsType, String message, Throwable cause) {
+    public KmsException(String kmsType, String message, Throwable cause) {
         super(message, cause);
         this.mKmsType = kmsType;
     }
 
-    public KmsException(KmsType kmsType, Throwable cause) {
+    public KmsException(String kmsType, Throwable cause) {
         super(cause);
         this.mKmsType = kmsType;
     }
 
     @Override
     public String getMessage() {
-        return "KMS " + mKmsType.toString() + " threw exception: " + super.getMessage();
+        return "KMS " + mKmsType + " threw exception: " + super.getMessage();
     }
 }
diff --git a/src/main/java/com/android/apksig/kms/KmsSignerEngine.java b/src/main/java/com/android/apksig/kms/KmsSignerEngine.java
deleted file mode 100644
index 5b1f2cf..0000000
--- a/src/main/java/com/android/apksig/kms/KmsSignerEngine.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apksig.kms;
-
-import com.android.apksig.KeyConfig;
-import com.android.apksig.SignerEngine;
-import com.android.apksig.kms.aws.AwsSignerEngine;
-import com.android.apksig.kms.gcp.GcpSignerEngine;
-
-/** Performs cryptographic signing with a Key Management Service (KMS). */
-public abstract class KmsSignerEngine implements SignerEngine {
-    public final KmsType kmsType;
-    public final String keyAlias;
-
-    /** Subclasses must specify the type of KMS and a signing key alias. */
-    public KmsSignerEngine(KmsType kmsType, String keyAlias) {
-        this.kmsType = kmsType;
-        this.keyAlias = keyAlias;
-    }
-
-    @Override
-    public abstract byte[] sign(byte[] data);
-
-    /** Fetch a concrete implementation based on the provided config. */
-    public static KmsSignerEngine fromKmsConfig(
-            KeyConfig.Kms kmsConfig, String jcaSignatureAlgorithm) {
-        switch (kmsConfig.kmsType) {
-            case AWS:
-                return new AwsSignerEngine(kmsConfig.keyAlias, jcaSignatureAlgorithm);
-            case GCP:
-                return new GcpSignerEngine(kmsConfig.keyAlias);
-            default:
-                throw new KmsException(kmsConfig.kmsType, "Unsupported KMS");
-        }
-    }
-}
diff --git a/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java b/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java
new file mode 100644
index 0000000..957917b
--- /dev/null
+++ b/src/main/java/com/android/apksig/kms/KmsSignerEngineProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.kms;
+
+import com.android.apksig.KeyConfig;
+import com.android.apksig.SignerEngine;
+
+import java.security.spec.AlgorithmParameterSpec;
+
+public interface KmsSignerEngineProvider {
+
+    /** Instantiates a concrete signer engine */
+    SignerEngine getInstance(
+            KeyConfig.Kms kmsConfig,
+            String jcaSignatureAlgorithm,
+            AlgorithmParameterSpec algorithmParameterSpec);
+
+    /** Which KMS provider this engine applies to */
+    String getKmsType();
+}
diff --git a/src/main/java/com/android/apksig/kms/KmsType.java b/src/main/java/com/android/apksig/kms/KmsType.java
index c135ddf..de81f4d 100644
--- a/src/main/java/com/android/apksig/kms/KmsType.java
+++ b/src/main/java/com/android/apksig/kms/KmsType.java
@@ -17,7 +17,7 @@
 package com.android.apksig.kms;
 
 /** Represents the supported Key Management Services. */
-public enum KmsType {
-    AWS,
-    GCP,
+public class KmsType {
+    public static String AWS = "aws";
+    public static String GCP = "gcp";
 }
diff --git a/src/main/java/com/android/apksig/kms/aws/AwsSignerEngine.java b/src/main/java/com/android/apksig/kms/aws/AwsSignerEngine.java
index 74a7cd5..8851a1d 100644
--- a/src/main/java/com/android/apksig/kms/aws/AwsSignerEngine.java
+++ b/src/main/java/com/android/apksig/kms/aws/AwsSignerEngine.java
@@ -18,8 +18,8 @@
 
 import static com.android.apksig.kms.KmsType.AWS;
 
+import com.android.apksig.SignerEngine;
 import com.android.apksig.kms.KmsException;
-import com.android.apksig.kms.KmsSignerEngine;
 
 import software.amazon.awssdk.core.SdkBytes;
 import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
@@ -27,12 +27,13 @@
 import software.amazon.awssdk.services.kms.model.SignRequest;
 import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;
 
-public class AwsSignerEngine extends KmsSignerEngine {
+public class AwsSignerEngine implements SignerEngine {
+    private final String mKeyAlias;
     private static final String ALIAS_PREFIX = "alias/";
     private final SigningAlgorithmSpec mSigningAlgorithmSpec;
 
     public AwsSignerEngine(String keyAlias, String jcaSignatureAlgorithm) {
-        super(AWS, keyAlias);
+        mKeyAlias = keyAlias;
         mSigningAlgorithmSpec = fromJcaSignatureAlgorithm(jcaSignatureAlgorithm);
     }
 
@@ -42,7 +43,7 @@
                 KmsClient.builder().httpClientBuilder(UrlConnectionHttpClient.builder()).build()) {
             return client.sign(
                             SignRequest.builder()
-                                    .keyId(ALIAS_PREFIX + keyAlias)
+                                    .keyId(ALIAS_PREFIX + mKeyAlias)
                                     .signingAlgorithm(mSigningAlgorithmSpec)
                                     .message(SdkBytes.fromByteArray(data))
                                     .build())
diff --git a/src/main/java/com/android/apksig/kms/aws/AwsSignerEngineProvider.java b/src/main/java/com/android/apksig/kms/aws/AwsSignerEngineProvider.java
new file mode 100644
index 0000000..bc55e6a
--- /dev/null
+++ b/src/main/java/com/android/apksig/kms/aws/AwsSignerEngineProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.kms.aws;
+
+import com.android.apksig.KeyConfig;
+import com.android.apksig.SignerEngine;
+import com.android.apksig.kms.KmsSignerEngineProvider;
+import com.android.apksig.kms.KmsType;
+
+import java.security.spec.AlgorithmParameterSpec;
+
+public class AwsSignerEngineProvider implements KmsSignerEngineProvider {
+
+    @Override
+    public SignerEngine getInstance(
+            KeyConfig.Kms kmsConfig,
+            String jcaSignatureAlgorithm,
+            AlgorithmParameterSpec algorithmParameterSpec) {
+        return new AwsSignerEngine(kmsConfig.keyAlias, jcaSignatureAlgorithm);
+    }
+
+    @Override
+    public String getKmsType() {
+        return KmsType.AWS;
+    }
+}
diff --git a/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngine.java b/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngine.java
index 61e0ad3..c72c2c6 100644
--- a/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngine.java
+++ b/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngine.java
@@ -16,9 +16,10 @@
 
 package com.android.apksig.kms.gcp;
 
+import static com.android.apksig.kms.KmsType.GCP;
+
+import com.android.apksig.SignerEngine;
 import com.android.apksig.kms.KmsException;
-import com.android.apksig.kms.KmsSignerEngine;
-import com.android.apksig.kms.KmsType;
 
 import com.google.cloud.kms.v1.AsymmetricSignRequest;
 import com.google.cloud.kms.v1.CryptoKeyVersionName;
@@ -28,7 +29,8 @@
 import java.io.IOException;
 
 /** Signs data using Google Cloud Platform. */
-public class GcpSignerEngine extends KmsSignerEngine {
+public class GcpSignerEngine implements SignerEngine {
+    private final String mKeyAlias;
 
     /**
      * Create an engine to sign data with GCP
@@ -37,13 +39,13 @@
      *     href="https://cloud.google.com/java/docs/reference/google-cloud-spanner/latest/com.google.spanner.admin.database.v1.CryptoKeyVersionName">CryptoKeyVersionName</a>
      */
     public GcpSignerEngine(String keyAlias) {
-        super(KmsType.GCP, keyAlias);
+        mKeyAlias = keyAlias;
     }
 
     @Override
     public byte[] sign(byte[] data) {
         try (KeyManagementServiceClient client = KeyManagementServiceClient.create()) {
-            CryptoKeyVersionName cryptoKeyVersionName = CryptoKeyVersionName.parse(this.keyAlias);
+            CryptoKeyVersionName cryptoKeyVersionName = CryptoKeyVersionName.parse(mKeyAlias);
             return client.asymmetricSign(
                             AsymmetricSignRequest.newBuilder()
                                     .setName(cryptoKeyVersionName.toString())
@@ -52,8 +54,7 @@
                     .getSignature()
                     .toByteArray();
         } catch (IOException e) {
-            throw new KmsException(
-                    this.kmsType, "Error initializing KeyManagementServiceClient", e);
+            throw new KmsException(GCP, "Error initializing KeyManagementServiceClient", e);
         }
     }
 }
diff --git a/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngineProvider.java b/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngineProvider.java
new file mode 100644
index 0000000..2f95bcc
--- /dev/null
+++ b/src/main/java/com/android/apksig/kms/gcp/GcpSignerEngineProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.kms.gcp;
+
+import com.android.apksig.KeyConfig;
+import com.android.apksig.SignerEngine;
+import com.android.apksig.kms.KmsSignerEngineProvider;
+import com.android.apksig.kms.KmsType;
+
+import java.security.spec.AlgorithmParameterSpec;
+
+public class GcpSignerEngineProvider implements KmsSignerEngineProvider {
+
+    @Override
+    public SignerEngine getInstance(
+            KeyConfig.Kms kmsConfig,
+            String jcaSignatureAlgorithm,
+            AlgorithmParameterSpec algorithmParameterSpec) {
+        return new GcpSignerEngine(kmsConfig.keyAlias);
+    }
+
+    @Override
+    public String getKmsType() {
+        return KmsType.GCP;
+    }
+}
diff --git a/src/main/resources/META-INF/services/com.android.apksig.kms.KmsSignerEngineProvider b/src/main/resources/META-INF/services/com.android.apksig.kms.KmsSignerEngineProvider
new file mode 100644
index 0000000..bb4e9ab
--- /dev/null
+++ b/src/main/resources/META-INF/services/com.android.apksig.kms.KmsSignerEngineProvider
@@ -0,0 +1,2 @@
+com.android.apksig.kms.aws.AwsSignerEngineProvider
+com.android.apksig.kms.gcp.GcpSignerEngineProvider
diff --git a/src/providers/aws/com.android.apksig.kms.KmsSignerEngineProvider b/src/providers/aws/com.android.apksig.kms.KmsSignerEngineProvider
new file mode 100644
index 0000000..61a33da
--- /dev/null
+++ b/src/providers/aws/com.android.apksig.kms.KmsSignerEngineProvider
@@ -0,0 +1 @@
+com.android.apksig.kms.aws.AwsSignerEngineProvider
diff --git a/src/providers/gcp/com.android.apksig.kms.KmsSignerEngineProvider b/src/providers/gcp/com.android.apksig.kms.KmsSignerEngineProvider
new file mode 100644
index 0000000..5cda539
--- /dev/null
+++ b/src/providers/gcp/com.android.apksig.kms.KmsSignerEngineProvider
@@ -0,0 +1 @@
+com.android.apksig.kms.gcp.GcpSignerEngineProvider
diff --git a/src/stub/java/com/android/apksig/kms/KmsSignerEngine.java b/src/stub/java/com/android/apksig/kms/KmsSignerEngine.java
deleted file mode 100644
index 5cb11a3..0000000
--- a/src/stub/java/com/android/apksig/kms/KmsSignerEngine.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.apksig.kms;
-
-import com.android.apksig.KeyConfig;
-import com.android.apksig.SignerEngine;
-
-/** Stub KMS engine for builds that don't care about a KMS. */
-public abstract class KmsSignerEngine implements SignerEngine {
-    public final KmsType kmsType;
-    public final String keyAlias;
-
-    /** Subclasses must specify the type of KMS and a signing key alias. */
-    public KmsSignerEngine(KmsType kmsType, String keyAlias) {
-        this.kmsType = kmsType;
-        this.keyAlias = keyAlias;
-    }
-
-    @Override
-    public abstract byte[] sign(byte[] data);
-
-    /**
-     * Always throws an exception. This class is only included in builds that don't use the KMS
-     * feature.
-     */
-    public static KmsSignerEngine fromKmsConfig(
-            KeyConfig.Kms kmsConfig, String jcaSignatureAlgorithm) {
-        throw new KmsException(
-                kmsConfig.kmsType,
-                "This code path should never be executed if you are using a KMS.  Are you using the"
-                        + " right dependency (apksig-kms)?");
-    }
-}
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 0a9d659..a2ecf8d 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -25,6 +25,7 @@
 import static com.android.apksig.apk.ApkUtils.findZipSections;
 import static com.android.apksig.internal.util.Resources.EC_P256_2_SIGNER_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.EC_P256_SIGNER_RESOURCE_NAME;
+import static com.android.apksig.internal.util.Resources.FIRST_AND_SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS;
 import static com.android.apksig.internal.util.Resources.FIRST_RSA_2048_SIGNER_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.FIRST_RSA_4096_SIGNER_RESOURCE_NAME;
@@ -35,7 +36,9 @@
 import static com.android.apksig.internal.util.Resources.LINEAGE_RSA_2048_3_SIGNERS_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.LINEAGE_RSA_2048_TO_RSA_4096_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
+// BEGIN-AOSP
 import static com.android.apksig.internal.util.Resources.TEST_GCP_KEY_RING;
+// END-AOSP
 import static com.android.apksig.internal.util.Resources.THIRD_RSA_2048_SIGNER_RESOURCE_NAME;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -66,10 +69,12 @@
 import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
 import com.android.apksig.internal.zip.CentralDirectoryRecord;
 import com.android.apksig.internal.zip.LocalFileRecord;
+// BEGIN-AOSP
 import com.android.apksig.kms.aws.AwsSignerConfigGenerator;
 import com.android.apksig.kms.aws.KeyAliasClient;
 import com.android.apksig.kms.gcp.GcpSignerConfigGenerator;
 import com.android.apksig.kms.gcp.KeyRingClient;
+// END-AOSP
 import com.android.apksig.util.DataSource;
 import com.android.apksig.util.DataSources;
 import com.android.apksig.zip.ZipFormatException;
@@ -732,6 +737,7 @@
                         .setAlignmentPreserved(true));
     }
 
+    // BEGIN-AOSP
     @Test
     public void testAws_Golden() throws Exception {
         try (KeyAliasClient client = new KeyAliasClient()) {
@@ -881,7 +887,9 @@
                         .setSigningCertificateLineage(lineage)
                         .setAlignmentPreserved(true));
     }
+    // END-AOSP
 
+    // BEGIN-AOSP
     @Test
     public void testGcp_Golden() throws Exception {
         try (KeyRingClient keyRingClient = new KeyRingClient(TEST_GCP_KEY_RING)) {
@@ -1030,6 +1038,7 @@
                         .setSigningCertificateLineage(lineage)
                         .setAlignmentPreserved(true));
     }
+    // END-AOSP
 
     @Test
     public void testMinSdkVersion_Golden() throws Exception {
@@ -3454,6 +3463,34 @@
     }
 
     @Test
+    public void testV4_certificateChainInSignerConfig_v4UsesCurrentSigner() throws Exception {
+        // The APK SignerConfig supports a certificate chain as input; this chain represents the
+        // current signing certificate, the previous issuer of this certificate, and any previous
+        // issuers back to the root. As long as the current signer for the SignerConfig is
+        // specified as the first certificate, all of the certificates in the chain should be
+        // stored in the length-prefixed sequence of X.509 certificates. To remain consistent
+        // with SigningConfigs for previous signature schemes, the V4 signature scheme should also
+        // accept SigningConfigs with a certificate chain; while the entire chain will not be
+        // stored in the V4 signature, this will allow SignerConfig instances with certificate
+        // chains to be used across all signature schemes. For more details about the certificate
+        // chain in the V3 signature block, see
+        // https://source.android.com/docs/security/features/apksigning/v3#format
+        List<ApkSigner.SignerConfig> rsa2048SignerConfig = Arrays.asList(
+                getDefaultSignerConfigFromResources(
+                        FIRST_AND_SECOND_RSA_2048_SIGNER_RESOURCE_NAME));
+
+        File signedApk = sign("original.apk",
+                new ApkSigner.Builder(rsa2048SignerConfig)
+                        .setV1SigningEnabled(true)
+                        .setV2SigningEnabled(true)
+                        .setV3SigningEnabled(true)
+                        .setV4SigningEnabled(true));
+        ApkVerifier.Result result = verify(signedApk, null);
+
+        assertResultContainsV4Signers(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME);
+    }
+
+    @Test
     public void
     testSourceStampTimestamp_signWithSourceStampAndTimestampDefault_validTimestampValue()
             throws Exception {
diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
index 4ca8959..a8b3d0b 100644
--- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
+++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java
@@ -20,7 +20,9 @@
 import static com.android.apksig.internal.util.Resources.FIRST_RSA_2048_SIGNER_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.SECOND_RSA_1024_SIGNER_RESOURCE_NAME;
 import static com.android.apksig.internal.util.Resources.SECOND_RSA_2048_SIGNER_RESOURCE_NAME;
+// BEGIN-AOSP
 import static com.android.apksig.internal.util.Resources.TEST_GCP_KEY_RING;
+// END-AOSP
 import static com.android.apksig.internal.util.Resources.THIRD_RSA_2048_SIGNER_RESOURCE_NAME;
 
 import static org.junit.Assert.assertEquals;
@@ -38,10 +40,12 @@
 import com.android.apksig.internal.apk.v3.V3SchemeSigner;
 import com.android.apksig.internal.util.ByteBufferUtils;
 import com.android.apksig.internal.util.Resources;
+// BEGIN-AOSP
 import com.android.apksig.kms.aws.AwsSignerConfigGenerator;
 import com.android.apksig.kms.aws.KeyAliasClient;
 import com.android.apksig.kms.gcp.GcpSignerConfigGenerator;
 import com.android.apksig.kms.gcp.KeyRingClient;
+// END-AOSP
 import com.android.apksig.util.DataSource;
 
 import org.junit.Before;
@@ -280,6 +284,7 @@
         assertExpectedCapabilityValues(newSignerCapabilities, newSignerCapabilityValues);
     }
 
+    // BEGIN-AOSP
     @Test
     public void
             testRotationWithExitingLineageAndNonDefaultCapabilitiesForNewSigner_previousSignerAws()
@@ -305,7 +310,9 @@
         SignerCapabilities newSignerCapabilities = lineage.getSignerCapabilities(newSigner);
         assertExpectedCapabilityValues(newSignerCapabilities, newSignerCapabilityValues);
     }
+    // END-AOSP
 
+    // BEGIN-AOSP
     @Test
     public void
             testRotationWithExitingLineageAndNonDefaultCapabilitiesForNewSigner_originalSignerAws()
@@ -331,7 +338,9 @@
         SignerCapabilities newSignerCapabilities = lineage.getSignerCapabilities(newSigner);
         assertExpectedCapabilityValues(newSignerCapabilities, newSignerCapabilityValues);
     }
+    // END-AOSP
 
+    // BEGIN-AOSP
     @Test
     public void
             testRotationWithExitingLineageAndNonDefaultCapabilitiesForNewSigner_previousSignerGcp()
@@ -357,7 +366,9 @@
         SignerCapabilities newSignerCapabilities = lineage.getSignerCapabilities(newSigner);
         assertExpectedCapabilityValues(newSignerCapabilities, newSignerCapabilityValues);
     }
+    // END-AOSP
 
+    // BEGIN-AOSP
     @Test
     public void
             testRotationWithExitingLineageAndNonDefaultCapabilitiesForNewSigner_originalSignerGcp()
@@ -383,6 +394,7 @@
         SignerCapabilities newSignerCapabilities = lineage.getSignerCapabilities(newSigner);
         assertExpectedCapabilityValues(newSignerCapabilities, newSignerCapabilityValues);
     }
+    // END-AOSP
 
     @Test
     public void testRotationWithExitingLineageAndNonDefaultCapabilitiesForNewSigner()
diff --git a/src/test/java/com/android/apksig/internal/util/Resources.java b/src/test/java/com/android/apksig/internal/util/Resources.java
index 75677cf..5120544 100644
--- a/src/test/java/com/android/apksig/internal/util/Resources.java
+++ b/src/test/java/com/android/apksig/internal/util/Resources.java
@@ -21,7 +21,9 @@
 import com.android.apksig.SigningCertificateLineage;
 import com.android.apksig.util.DataSource;
 
+// BEGIN-AOSP
 import com.google.cloud.kms.v1.KeyRingName;
+// END-AOSP
 
 import org.junit.rules.TemporaryFolder;
 
@@ -58,13 +60,20 @@
     public static final String FIRST_RSA_1024_SIGNER_RESOURCE_NAME = "rsa-1024";
     public static final String SECOND_RSA_1024_SIGNER_RESOURCE_NAME = "rsa-1024_2";
 
+    // This resource uses a PEM certificate file containing the certificate chain with both the
+    // first and second RSA-2048 signers. This resource should be used for any tests that require
+    // a certificate chain in the SignerConfig.
+    public static final String FIRST_AND_SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048-2-1";
+
     public static final String FIRST_RSA_4096_SIGNER_RESOURCE_NAME = "rsa-4096";
 
     public static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256";
     public static final String EC_P256_2_SIGNER_RESOURCE_NAME = "ec-p256_2";
 
+    // BEGIN-AOSP
     public static final KeyRingName TEST_GCP_KEY_RING =
             KeyRingName.of("apksigner-cloud-kms", "us-central1", "testV3");
+    // END-AOSP
 
     // This is the same cert as above with the modulus reencoded to remove the leading 0 sign bit.
     public static final String FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS =
diff --git a/src/test/resources/com/android/apksig/rsa-2048-2-1.pk8 b/src/test/resources/com/android/apksig/rsa-2048-2-1.pk8
new file mode 100644
index 0000000..5a572ff
--- /dev/null
+++ b/src/test/resources/com/android/apksig/rsa-2048-2-1.pk8
Binary files differ
diff --git a/src/test/resources/com/android/apksig/rsa-2048-2-1.x509.pem b/src/test/resources/com/android/apksig/rsa-2048-2-1.x509.pem
new file mode 100644
index 0000000..b14fae4
--- /dev/null
+++ b/src/test/resources/com/android/apksig/rsa-2048-2-1.x509.pem
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgIJANh7AUYJKp9PMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE4MDYxOTAwMDUwMFoXDTI4MDYxNjAwMDUwMFowFTET
+MBEGA1UEAwwKcnNhLTIwNDhfMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBALgwhcEJHldw5BD4tZP3aeeYyi8FCVx68WvuRcpJ1IwCbXd03giTyc3Y8Olr
+7D67Y5aLdCW+XE8Z9pNHd43dXe9aRN6kcbhVynzVfe5PKCeYt3dVYgxh8eQqO6A5
+f6DpJjF1jkqRmR6BipsDw5t8PwiiJ03jnoaepdGvnQpwxHEf7izWte+XHBPbJH6A
+vqXCUVlHw+CpI4J2NhZqfSa60F4y5heKF4mF4+97JPODopVeFXvS1VctYEY3ycsB
+uumZy3Q9Lp2E07KP7SP7oKAegg0uReqeqVcofBQESP4iMefw86QhqkTysfG3q3Sf
+RmmbTLnovAxSjjQZ5oZzGJuBQXECAwEAAaNQME4wHQYDVR0OBBYEFD/NVrkySRE2
+6bctYSVM+os/31yRMB8GA1UdIwQYMBaAFBcCLXMQfzibORLraiGzAYZZLB1vMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACIYKbWz0byV8hCwVhK3UGrI
+PV6+DWAbRrx74y0cqhlPIyroOdSSEsmGythQMMVdvkvHGLqdA9gwQqYkNkWFQQqp
+Cfj9+YJdEKTKJtMO/ZTATJyN0ACEXr5YQo9ivAASY2pfiTfQXbPGsEhycuG3gJ1Y
+rTC3imERhkHomf8ZvTJEwSOF0bQYNg/Ar2nNYNdf3oCrrylLIxUaq0pp/5CrvZT4
+SMKLBfimirfxGS31Z9TEuvDma1Rn2xwfDOhmiSvC30PK/xPsN1xvallHtJDmTdNV
+M4E7Z5hDyCOQ3y1o3VHr73dZnA0M0WdM7JIH9Vcs+R0otFdyXrfeMw4YvMZ0/Qc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDQ4JI1EJ239V4wss0jpVlZMudh2/kARCVdoBgsRQuvc2RNnO23Eyynlt9UN+Dc
+NRdQIhbCpVTjdEl/bePECHlqg9NE3frAj5GebiUdWL6A/idKsZA1nAKyIgxxjcnu
++38OcrlO6XOm36euxGfd/ULrghZGXzMVFq4uLiIv3DqFkUcIlE0BvUiUoNwpopV4
+MKj1GQgoaEObJG5xkMBKO6vg36VfJ3s3V3r48uJxYGhhBZEB0EpoXLd4i0piAB8S
+MLb0Ek6wA/HZ8A2rdnStk1wl/83OM1jO0uB3hyfJpqIijlvNGnrloYyyOIqS0LGH
+nxSJD7goASH2Ef0h4yxbsOvHAgMBAAGjUDBOMB0GA1UdDgQWBBQXAi1zEH84mzkS
+62ohswGGWSwdbzAfBgNVHSMEGDAWgBQXAi1zEH84mzkS62ohswGGWSwdbzAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB92T5toLkF6dLl65/boH5Qvub
+5wfIk0AD12T3t3kYWQFOH0YDCHNL3SfmrjYM/CwNJAd1KuCL5AZcn0km/n0SFXt5
+8Ps/MBcb0eK1fYezeEehKUyt5IBgDTKeQOel6So8rGuQRrDf/WV8rt6fugkIODFx
+sB3oj4ESaGXbvmvWD6q4a3koq/nV26kALchnAr7/FTNq3HEIQ1BDr9pldVh1gEV/
+ohHKcQP4M22Es7lredzpIcb5K6Ko/UtwsSRtHnoOjwmb+L/FsgAJsekmcJG5TK1X
+ciIsrrNFDCYzf/d9O1PD/V95kB7460qMzrGWZpc3mLe+OnmVMq6c4omOtIKl
+-----END CERTIFICATE-----
+