HTTPCORE-682: Custom provider for key manager/trust manager initialization (#296)
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java
index 6f8c478..886003b 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java
@@ -37,6 +37,7 @@
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
 import java.security.Principal;
 import java.security.PrivateKey;
 import java.security.Provider;
@@ -88,6 +89,8 @@
     private String trustManagerFactoryAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
     private SecureRandom secureRandom;
     private Provider provider;
+    private Provider tsProvider;
+    private Provider ksProvider;
 
     /**
      * An empty immutable {@code KeyManager} array.
@@ -141,6 +144,56 @@
     }
 
     /**
+     * Sets the JCA provider to use for creating trust stores.
+     * @param provider provider to use for creating trust stores.
+     * @return this builder
+     * @since 5.2
+     */
+    public SSLContextBuilder setTrustStoreProvider(final Provider provider) {
+        this.tsProvider = provider;
+        return this;
+    }
+
+    /**
+     * Sets the JCA provider name to use for creating trust stores.
+     * @param name Name of the provider to use for creating trust stores, the provider must be registered with the JCA.
+     * @return this builder
+     * @since 5.2
+     */
+    public SSLContextBuilder setTrustStoreProvider(final String name) throws NoSuchProviderException {
+        this.tsProvider = Security.getProvider(name);
+        if (this.tsProvider == null) {
+            throw new NoSuchProviderException(name);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the JCA provider to use for creating key stores.
+     * @param provider provider to use for creating key stores.
+     * @return this builder
+     * @since 5.2
+     */
+    public SSLContextBuilder setKeyStoreProvider(final Provider provider) {
+        this.ksProvider = provider;
+        return this;
+    }
+
+    /**
+     * Sets the JCA provider name to use for creating key stores.
+     * @param name Name of the provider to use for creating key stores, the provider must be registered with the JCA.
+     * @return this builder
+     * @since 5.2
+     */
+    public SSLContextBuilder setKeyStoreProvider(final String name) throws NoSuchProviderException {
+        this.ksProvider = Security.getProvider(name);
+        if (this.ksProvider == null) {
+            throw new NoSuchProviderException(name);
+        }
+        return this;
+    }
+
+    /**
      * Sets the key store type.
      *
      * @param keyStoreType
@@ -208,11 +261,15 @@
     public SSLContextBuilder loadTrustMaterial(
             final KeyStore truststore,
             final TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyStoreException {
-        final TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
-                trustManagerFactoryAlgorithm == null ? TrustManagerFactory.getDefaultAlgorithm()
-                        : trustManagerFactoryAlgorithm);
-        tmfactory.init(truststore);
-        final TrustManager[] tms = tmfactory.getTrustManagers();
+
+        final String alg = trustManagerFactoryAlgorithm == null ?
+                TrustManagerFactory.getDefaultAlgorithm() : trustManagerFactoryAlgorithm;
+
+        final TrustManagerFactory tmFactory = tsProvider == null ?
+                TrustManagerFactory.getInstance(alg) : TrustManagerFactory.getInstance(alg, tsProvider);
+
+        tmFactory.init(truststore);
+        final TrustManager[] tms = tmFactory.getTrustManagers();
         if (tms != null) {
             if (trustStrategy != null) {
                 for (int i = 0; i < tms.length; i++) {
@@ -279,11 +336,15 @@
             final char[] keyPassword,
             final PrivateKeyStrategy aliasStrategy)
             throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
-        final KeyManagerFactory kmfactory = KeyManagerFactory
-                .getInstance(keyManagerFactoryAlgorithm == null ? KeyManagerFactory.getDefaultAlgorithm()
-                        : keyManagerFactoryAlgorithm);
-        kmfactory.init(keystore, keyPassword);
-        final KeyManager[] kms = kmfactory.getKeyManagers();
+
+        final String alg = keyManagerFactoryAlgorithm == null ?
+                KeyManagerFactory.getDefaultAlgorithm() : keyManagerFactoryAlgorithm;
+
+        final KeyManagerFactory kmFactory = ksProvider == null ?
+                KeyManagerFactory.getInstance(alg) : KeyManagerFactory.getInstance(alg, ksProvider);
+
+        kmFactory.init(keystore, keyPassword);
+        final KeyManager[] kms = kmFactory.getKeyManagers();
         if (kms != null) {
             if (aliasStrategy != null) {
                 for (int i = 0; i < kms.length; i++) {
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/ssl/DummyProvider.java b/httpcore5/src/test/java/org/apache/hc/core5/ssl/DummyProvider.java
new file mode 100644
index 0000000..6d82a48
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/ssl/DummyProvider.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.ssl;
+
+import java.security.Provider;
+import java.security.Security;
+import java.util.HashSet;
+import java.util.Set;
+
+public class DummyProvider extends Provider {
+
+    private final Provider realJSSEProvider = Security.getProvider(TestSSLContextBuilder.PROVIDER_SUN_JSSE);
+    private final Provider realJCEEProvider = Security.getProvider(TestSSLContextBuilder.PROVIDER_SUN_JCE);
+    final static String NAME = "FAKE";
+
+    private final Set<String> requestedTypes = new HashSet<>();
+
+    public DummyProvider() {
+        super(NAME, 1.1, "http core fake provider 1.1");
+    }
+
+    public boolean hasBeenRequested(final String what) {
+        return requestedTypes.contains(what);
+    }
+
+    @Override
+    public Service getService(final String type, final String algorithm) {
+        requestedTypes.add(type);
+        if ("KeyStore".equals(type)) {
+            return realJCEEProvider.getService(type, algorithm);
+        }
+        return realJSSEProvider.getService(type, algorithm);
+    }
+
+    @Override
+    public synchronized Set<Service> getServices() {
+        return realJSSEProvider.getServices();
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java
index 51cad2d..ca9671d 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java
@@ -37,6 +37,7 @@
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
 import java.security.Principal;
 import java.security.Security;
 import java.security.UnrecoverableKeyException;
@@ -69,7 +70,8 @@
  */
 public class TestSSLContextBuilder {
 
-    private static final String PROVIDER_SUN_JSSE = "SunJSSE";
+    static final String PROVIDER_SUN_JSSE = "SunJSSE";
+    static final String PROVIDER_SUN_JCE = "SunJCE";
 
     private static boolean isWindows() {
         return System.getProperty("os.name").contains("Windows");
@@ -181,26 +183,115 @@
         final URL resource1 = getResource("/test-server.p12");
         final String storePassword = "nopassword";
         final String keyPassword = "nopassword";
-        final SSLContext sslContext = SSLContextBuilder.create()
-                .setProvider(Security.getProvider(PROVIDER_SUN_JSSE))
+        final DummyProvider provider = new DummyProvider();
+        SSLContextBuilder.create()
+                .setProvider(provider)
                 .loadKeyMaterial(resource1, storePassword.toCharArray(), keyPassword.toCharArray())
                 .build();
-        Assert.assertEquals(PROVIDER_SUN_JSSE, sslContext.getProvider().getName());
+        Assert.assertTrue(provider.hasBeenRequested("SSLContext"));
     }
 
     @Test
     public void testBuildWithProviderName() throws Exception {
+
+        final DummyProvider provider = new DummyProvider();
+        Security.insertProviderAt(provider, 1);
+        try {
+
+            final URL resource1 = getResource("/test-server.p12");
+            final String storePassword = "nopassword";
+            final String keyPassword = "nopassword";
+            SSLContextBuilder.create()
+                    .setProvider(DummyProvider.NAME)
+                    .loadKeyMaterial(resource1, storePassword.toCharArray(), keyPassword.toCharArray())
+                    .build();
+            Assert.assertTrue(provider.hasBeenRequested("SSLContext"));
+
+        } finally {
+            Security.removeProvider(DummyProvider.NAME);
+        }
+    }
+
+    @Test
+    public void testBuildKSWithNoSuchProvider() {
+        Assert.assertThrows(NoSuchProviderException.class,
+                () -> SSLContextBuilder.create()
+                .setKeyStoreProvider("no-such-provider")
+                .build());
+    }
+
+    @Test
+    public void testBuildKSWithProvider() throws Exception {
         final URL resource1 = getResource("/test-server.p12");
         final String storePassword = "nopassword";
         final String keyPassword = "nopassword";
-        final SSLContext sslContext = SSLContextBuilder.create()
-                .setProvider(PROVIDER_SUN_JSSE)
+        final DummyProvider provider = new DummyProvider();
+        SSLContextBuilder.create()
+                .setKeyStoreProvider(provider)
                 .loadKeyMaterial(resource1, storePassword.toCharArray(), keyPassword.toCharArray())
                 .build();
-        Assert.assertEquals(PROVIDER_SUN_JSSE, sslContext.getProvider().getName());
+        Assert.assertTrue(provider.hasBeenRequested("KeyManagerFactory"));
     }
 
     @Test
+    public void testBuildKSWithProviderName() throws Exception {
+
+        final DummyProvider provider = new DummyProvider();
+        Security.insertProviderAt(provider, 1);
+        try {
+
+            final URL resource1 = getResource("/test-server.p12");
+            final String storePassword = "nopassword";
+            final String keyPassword = "nopassword";
+            SSLContextBuilder.create()
+                    .setKeyStoreProvider(DummyProvider.NAME)
+                    .loadKeyMaterial(resource1, storePassword.toCharArray(), keyPassword.toCharArray())
+                    .build();
+            Assert.assertTrue(provider.hasBeenRequested("KeyManagerFactory"));
+
+        } finally {
+            Security.removeProvider(DummyProvider.NAME);
+        }
+    }
+
+    @Test
+    public void testBuildTSWithNoSuchProvider() {
+        Assert.assertThrows(NoSuchProviderException.class, ()->
+            SSLContextBuilder.create()
+                    .setTrustStoreProvider("no-such-provider")
+                    .build());
+    }
+
+    @Test
+    public void testBuildTSWithProvider() throws Exception {
+        final DummyProvider provider = new DummyProvider();
+        SSLContextBuilder.create()
+                .setTrustStoreProvider(provider)
+                .loadTrustMaterial((KeyStore) null, null)
+                .build();
+        Assert.assertTrue(provider.hasBeenRequested("TrustManagerFactory"));
+    }
+
+    @Test
+    public void testBuildTSWithProviderName() throws Exception {
+
+        final DummyProvider provider = new DummyProvider();
+        Security.insertProviderAt(provider, 1);
+        try {
+
+            SSLContextBuilder.create()
+                    .setTrustStoreProvider(DummyProvider.NAME)
+                    .loadTrustMaterial((KeyStore) null, null)
+                    .build();
+            Assert.assertTrue(provider.hasBeenRequested("TrustManagerFactory"));
+
+        } finally {
+            Security.removeProvider(DummyProvider.NAME);
+        }
+    }
+
+
+    @Test
     public void testKeyWithAlternatePasswordInvalid() throws Exception {
         final URL resource1 = getResource("/test-keypasswd.p12");
         final String storePassword = "nopassword";