Repository: jetty/jetty.project
Pull request #12924: Fixes #12923 - Introduce Bouncy Castle Jetty module.
Author: login: sbordet
Created: 2025-03-19T16:32:36Z | Merged: 2025-03-24T14:27:36Z

* Introduced Bouncy Castle module.
* Moved Conscrypt module from jetty-home to jetty-alpn-conscrypt-server.
* Added tests for both modules.
* Reduced noisy logging in SslContextFactory.
* Catch and ignore exceptions thrown by ExtendedSSLSession.getRequestedServerNames() in SecureRequestCustomizer.

Linked issue #12923: Introduce Bouncy Castle Jetty module
**Jetty version(s)**
12.0.x

**Description**
Introduce a `bouncycastle` Jetty module similar to the `conscrypt` Jetty module.
Linked issue comments: user: login: mperktold
created_at: 2025-04-11T14:09:15Z
body: Are there any guidelines on how to choose whether to use Bouncy Castle over Conscrypt? According to the docs, one might want to choose Conscrypt over the JDK's default provider for performance reasons. Does Bouncy Castle also offer a performance benefit? How do Conscrypt and Bouncy Castle compare in terms of performance?

I am asking because we are thinking about moving from Conscrypt to Bouncy Castle in order to reduce dependencies (we already use Bouncy Castle for other things), but we are unsure of the consequences. Maybe you could add some guidelines to the docs?
author_association: NONE
user: login: sbordet
created_at: 2025-04-12T10:54:58Z
body: We did not run performance benchmarks for BouncyCastle, so I do not have an answer.

@lorban do you have insights?
Would it be difficult to configure our benchmarks to use BouncyCastle?
author_association: CONTRIBUTOR
user: login: lorban
created_at: 2025-04-14T08:15:01Z
body: @sbordet it shouldn't be that hard to modify out benchmark to test default impl vs bouncy castle vs conscrypt. Let me have a look at this.
author_association: CONTRIBUTOR
user: login: mperktold
created_at: 2025-08-01T09:27:17Z
body: @lorban have you looked into this already? I would be interested in your results.

If not, we'll move to BouncyCastle anyway and see how it goes.
author_association: NONE
user: login: lorban
created_at: 2025-08-01T14:25:17Z
body: @mperktold I modified our benchmark to be able to configure any JSSE provider and ran a few preliminary tests. That was a few months a go and I left that work aside as there unfortunately were other priorities.

But IIRC conscrypt was the fastest, the default JDK impl was a close second and bouncy castle was noticeably slower. I didn't dig too much into those details and the benchmark I used was rather primitive so as always, YMMV.
author_association: CONTRIBUTOR

Diff:
diff --git a/documentation/jetty/modules/code/examples/pom.xml b/documentation/jetty/modules/code/examples/pom.xml
index ba1e164ef506..72353fcb4055 100644
--- a/documentation/jetty/modules/code/examples/pom.xml
+++ b/documentation/jetty/modules/code/examples/pom.xml
@@ -18,6 +18,14 @@
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bctls-jdk18on</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.conscrypt</groupId>
+      <artifactId>conscrypt-openjdk-uber</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-alpn-server</artifactId>
diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
index ba9470a34f23..ac840faedfc9 100644
--- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
+++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java
@@ -29,6 +29,8 @@
 import jakarta.servlet.http.HttpServlet;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
 import org.conscrypt.OpenSSLProvider;
 import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
 import org.eclipse.jetty.client.ContentResponse;
@@ -605,11 +607,26 @@ public void conscrypt()
         SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
         sslContextFactory.setKeyStorePath("/path/to/keystore");
         sslContextFactory.setKeyStorePassword("secret");
-        // Configure Jetty's SslContextFactory to use Conscrypt.
+        // Configure Jetty's SslContextFactory to use the Conscrypt provider.
         sslContextFactory.setProvider("Conscrypt");
         // end::conscrypt[]
     }
 
+    public void bouncyCastle()
+    {
+        // tag::bouncyCastle[]
+        // Configure the JDK with the Bouncy Castle providers, you need both.
+        Security.addProvider(new BouncyCastleProvider());
+        Security.addProvider(new BouncyCastleJsseProvider());
+
+        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
+        sslContextFactory.setKeyStorePath("/path/to/keystore");
+        sslContextFactory.setKeyStorePassword("secret");
+        // Configure Jetty's SslContextFactory to use the Bouncy Castle provider.
+        sslContextFactory.setProvider("BCJSSE");
+        // end::bouncyCastle[]
+    }
+
     public void keyStoreScanner() throws Exception
     {
         // tag::keyStoreScanner[]
diff --git a/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc b/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc
index 37dc04538d65..0f4face18715 100644
--- a/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc
+++ b/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc
@@ -861,6 +861,21 @@ To use Conscrypt as the TLS provider just enable the `conscrypt` Jetty module:
 $ java -jar $JETTY_HOME/start.jar --add-modules=conscrypt
 ----
 
+[[ssl-bouncycastle]]
+=== Using Bouncy Castle as SSL/TLS Provider
+
+If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime.
+
+OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider.
+
+link:https://www.bouncycastle.org/[Bouncy Castle] is a pure Java alternative provider for the Java security APIs, and includes a TLS provider.
+
+To use Bouncy Castle as the TLS provider just enable the `bouncycastle` Jetty module:
+
+----
+$ java -jar $JETTY_HOME/start.jar --add-modules=bouncycastle
+----
+
 [[ssl-sni]]
 === Configuring SNI
 
diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc
index 6eea0fc30ced..32d42f11f330 100644
--- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc
+++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc
@@ -590,6 +590,24 @@ Then, you must configure the JDK with the Conscrypt provider, and configure Jett
 include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=conscrypt]
 ----
 
+[[connector-protocol-tls-bouncycastle]]
+==== Using Bouncy Castle as SSL/TLS Provider
+
+If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime.
+
+OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider.
+
+link:https://www.bouncycastle.org/[Bouncy Castle] is a pure Java alternative provider for the Java security APIs, and includes a TLS provider.
+
+To use Bouncy Castle as TLS provider, you must have the Bouncy Castle jars and the Jetty dependency `jetty-alpn-bouncycastle-server-{jetty-version}.jar` in the class-path or module-path.
+
+Then, you must configure the JDK with the Conscrypt provider, and configure Jetty to use the Conscrypt provider, in this way:
+
+[,java,indent=0,options=nowrap]
+----
+include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=bouncyCastle]
+----
+
 [[connector-protocol-tls-keystore-auto-reload]]
 ==== SslContextFactory KeyStore Auto-Reload
 
diff --git a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-client/pom.xml b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-client/pom.xml
index c3ba576460b2..4c2782a77817 100644
--- a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-client/pom.xml
+++ b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-client/pom.xml
@@ -28,7 +28,6 @@
     <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bctls-jdk18on</artifactId>
-      <version>${bouncycastle.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/pom.xml b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/pom.xml
index 6a0cb9c8b211..09a93ad60830 100644
--- a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/pom.xml
+++ b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/pom.xml
@@ -31,7 +31,6 @@
     <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bctls-jdk18on</artifactId>
-      <version>${bouncycastle.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle.mod b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle.mod
new file mode 100644
index 000000000000..99eb3e61f5e6
--- /dev/null
+++ b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle.mod
@@ -0,0 +1,34 @@
+[description]
+Installs the Bouncy Castle JSSE provider.
+
+[tags]
+3rdparty
+ssl
+bouncycastle
+
+[depend]
+ssl
+
+[provides]
+alpn-impl
+
+[files]
+maven://org.bouncycastle/bcprov-jdk18on/${bouncycastle.version}|lib/bouncycastle/bcprov-jdk18on-${bouncycastle.version}.jar
+maven://org.bouncycastle/bctls-jdk18on/${bouncycastle.version}|lib/bouncycastle/bctls-jdk18on-${bouncycastle.version}.jar
+basehome:modules/bouncycastle/bouncycastle.xml|etc/bouncycastle.xml
+
+[xml]
+etc/bouncycastle.xml
+
+[lib]
+lib/bouncycastle/bcprov-jdk18on-${bouncycastle.version}.jar
+lib/bouncycastle/bctls-jdk18on-${bouncycastle.version}.jar
+lib/jetty-alpn-bouncycastle-server-${jetty.version}.jar
+
+[license]
+The Bouncy Castle libraries are distributed under the MIT license.
+https://github.com/bcgit/bc-java/blob/main/LICENSE.md
+
+[ini]
+bouncycastle.version?=@bouncycastle.version@
+jetty.sslContext.provider?=BCJSSE
diff --git a/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle/bouncycastle.xml b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle/bouncycastle.xml
new file mode 100644
index 000000000000..faca13cc1f4d
--- /dev/null
+++ b/jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle/bouncycastle.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
+<Configure>
+  <Call class="java.security.Security" name="addProvider">
+    <Arg>
+      <New class="org.bouncycastle.jce.provider.BouncyCastleProvider" />
+    </Arg>
+  </Call>
+  <Call class="java.security.Security" name="addProvider">
+    <Arg>
+      <New class="org.bouncycastle.jsse.provider.BouncyCastleJsseProvider" />
+    </Arg>
+  </Call>
+</Configure>
diff --git a/jetty-home/src/main/resources/modules/conscrypt.mod b/jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt.mod
similarity index 69%
rename from jetty-home/src/main/resources/modules/conscrypt.mod
rename to jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt.mod
index 174fe11bbb78..ff853c12e456 100644
--- a/jetty-home/src/main/resources/modules/conscrypt.mod
+++ b/jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt.mod
@@ -3,6 +3,8 @@ Installs the Conscrypt JSSE provider.
 
 [tags]
 3rdparty
+ssl
+conscrypt
 
 [depend]
 ssl
@@ -12,7 +14,6 @@ alpn-impl
 
 [files]
 maven://org.conscrypt/conscrypt-openjdk-uber/${conscrypt.version}|lib/conscrypt/conscrypt-uber-${conscrypt.version}.jar
-#maven://org.conscrypt/conscrypt-openjdk/${conscrypt.version}/jar/linux-x86_64|lib/conscrypt/conscrypt-${conscrypt.version}-linux-x86_64.jar
 basehome:modules/conscrypt/conscrypt.xml|etc/conscrypt.xml
 
 [xml]
@@ -20,11 +21,10 @@ etc/conscrypt.xml
 
 [lib]
 lib/conscrypt/conscrypt-uber-${conscrypt.version}.jar
-lib/conscrypt/conscrypt-${conscrypt.version}-linux-x86_64.jar
 lib/jetty-alpn-conscrypt-server-${jetty.version}.jar
 
 [license]
-Conscrypt is distributed under the Apache Licence 2.0
+The Conscrypt libraries are distributed under the Apache Licence 2.0.
 https://github.com/google/conscrypt/blob/master/LICENSE
 
 [ini]
diff --git a/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml b/jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt/conscrypt.xml
similarity index 90%
rename from jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml
rename to jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt/conscrypt.xml
index 4f3d7a5f2bdc..397c64a8ed9c 100644
--- a/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml
+++ b/jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt/conscrypt.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_9_3.dtd">
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
 <Configure id="Conscrypt" class="org.conscrypt.OpenSSLProvider">
   <Call class="java.security.Security" name="addProvider">
     <Arg><Ref refid="Conscrypt"/></Arg>
diff --git a/jetty-core/jetty-keystore/src/main/config/modules/test-keystore.mod b/jetty-core/jetty-keystore/src/main/config/modules/test-keystore.mod
index c2763ab49968..3cdbf0b4a3f7 100644
--- a/jetty-core/jetty-keystore/src/main/config/modules/test-keystore.mod
+++ b/jetty-core/jetty-keystore/src/main/config/modules/test-keystore.mod
@@ -5,6 +5,8 @@ DO NOT USE IN PRODUCTION!!!
 [tags]
 demo
 ssl
+bouncycastle
+keystore
 
 [before]
 ssl
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java
index 76b05f467fa0..f38ea07efbb7 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java
@@ -15,6 +15,7 @@
 
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.ExtendedSSLSession;
@@ -244,7 +245,7 @@ protected String retrieveSni(Request request, SSLSession session)
         // SSLSession attributes, so perform a more expensive SNI retrieval.
         if (session instanceof ExtendedSSLSession extended)
         {
-            for (SNIServerName serverName : extended.getRequestedServerNames())
+            for (SNIServerName serverName : getRequestedServerNames(extended))
             {
                 if (serverName instanceof SNIHostName hostName)
                     return hostName.getAsciiName();
@@ -255,6 +256,20 @@ protected String retrieveSni(Request request, SSLSession session)
         return null;
     }
 
+    private List<SNIServerName> getRequestedServerNames(ExtendedSSLSession session)
+    {
+        try
+        {
+            // Ignore exceptions thrown from this method.
+            // For example: https://github.com/bcgit/bc-java/issues/1773.
+            return session.getRequestedServerNames();
+        }
+        catch (Throwable x)
+        {
+            return List.of();
+        }
+    }
+
     private X509 getX509(SSLSession session)
     {
         X509 x509 = (X509)session.getValue(X509_ATTRIBUTE);
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java
index 20725ff049a1..25dd48dfa37b 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java
@@ -1698,17 +1698,13 @@ protected CertificateFactory getCertificateFactoryInstance(String type) throws C
         try
         {
             if (provider != null)
-            {
                 return CertificateFactory.getInstance(type, provider);
-            }
         }
         catch (Throwable cause)
         {
             String msg = String.format("Unable to get CertificateFactory instance for type [%s] on provider [%s], using default", type, provider);
             if (LOG.isDebugEnabled())
                 LOG.debug(msg, cause);
-            else
-                LOG.info(msg);
         }
 
         return CertificateFactory.getInstance(type);
@@ -1722,17 +1718,13 @@ protected CertStore getCertStoreInstance(Collection<? extends CRL> crls) throws
         try
         {
             if (provider != null)
-            {
                 return CertStore.getInstance(type, new CollectionCertStoreParameters(crls), provider);
-            }
         }
         catch (Throwable cause)
         {
             String msg = String.format("Unable to get CertStore instance for type [%s] on provider [%s], using default", type, provider);
             if (LOG.isDebugEnabled())
                 LOG.debug(msg, cause);
-            else
-                LOG.info(msg);
         }
 
         return CertStore.getInstance(type, new CollectionCertStoreParameters(crls));
@@ -1746,18 +1738,13 @@ protected KeyManagerFactory getKeyManagerFactoryInstance() throws NoSuchAlgorith
         try
         {
             if (provider != null)
-            {
                 return KeyManagerFactory.getInstance(algorithm, provider);
-            }
         }
         catch (Throwable cause)
         {
-            // fall back to non-provider option
             String msg = String.format("Unable to get KeyManagerFactory instance for algorithm [%s] on provider [%s], using default", algorithm, provider);
             if (LOG.isDebugEnabled())
                 LOG.debug(msg, cause);
-            else
-                LOG.info(msg);
         }
 
         return KeyManagerFactory.getInstance(algorithm);
@@ -1774,17 +1761,13 @@ protected SecureRandom getSecureRandomInstance() throws NoSuchAlgorithmException
             try
             {
                 if (provider != null)
-                {
                     return SecureRandom.getInstance(algorithm, provider);
-                }
             }
             catch (Throwable cause)
             {
                 String msg = String.format("Unable to get SecureRandom instance for algorithm [%s] on provider [%s], using default", algorithm, provider);
                 if (LOG.isDebugEnabled())
                     LOG.debug(msg, cause);
-                else
-                    LOG.info(msg);
             }
 
             return SecureRandom.getInstance(algorithm);
@@ -1801,17 +1784,13 @@ protected SSLContext getSSLContextInstance() throws NoSuchAlgorithmException
         try
         {
             if (provider != null)
-            {
                 return SSLContext.getInstance(protocol, provider);
-            }
         }
         catch (Throwable cause)
         {
             String msg = String.format("Unable to get SSLContext instance for protocol [%s] on provider [%s], using default", protocol, provider);
             if (LOG.isDebugEnabled())
                 LOG.debug(msg, cause);
-            else
-                LOG.info(msg);
         }
 
         return SSLContext.getInstance(protocol);
@@ -1824,17 +1803,13 @@ protected TrustManagerFactory getTrustManagerFactoryInstance() throws NoSuchAlgo
         try
         {
             if (provider != null)
-            {
                 return TrustManagerFactory.getInstance(algorithm, provider);
-            }
         }
         catch (Throwable cause)
         {
             String msg = String.format("Unable to get TrustManagerFactory instance for algorithm [%s] on provider [%s], using default", algorithm, provider);
             if (LOG.isDebugEnabled())
                 LOG.debug(msg, cause);
-            else
-                LOG.info(msg);
         }
 
         return TrustManagerFactory.getInstance(algorithm);
diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml
index 41b253b4eaff..bde7939b9097 100644
--- a/jetty-home/pom.xml
+++ b/jetty-home/pom.xml
@@ -19,6 +19,10 @@
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-alpn-bouncycastle-server</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-alpn-conscrypt-server</artifactId>
diff --git a/pom.xml b/pom.xml
index f514cc7896c1..29ad74708a78 100644
--- a/pom.xml
+++ b/pom.xml
@@ -277,7 +277,6 @@
     <maven.surefire.plugin.version>3.3.0</maven.surefire.plugin.version>
     <maven.version>3.9.9</maven.version>
     <maven.war.plugin.version>3.4.0</maven.war.plugin.version>
-    <mina.core.version>2.2.4</mina.core.version>
     <mongo.docker.version>5.0.26</mongo.docker.version>
     <mongodb.version>5.1.3</mongodb.version>
     <netty.version>4.1.115.Final</netty.version>
@@ -600,16 +599,16 @@
         <artifactId>maven-resolver-supplier</artifactId>
         <version>${maven.resolver.version}</version>
       </dependency>
-      <dependency>
-        <groupId>org.apache.mina</groupId>
-        <artifactId>mina-core</artifactId>
-        <version>${mina.core.version}</version>
-      </dependency>
       <dependency>
         <groupId>org.awaitility</groupId>
         <artifactId>awaitility</artifactId>
         <version>${awaitility.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.bouncycastle</groupId>
+        <artifactId>bctls-jdk18on</artifactId>
+        <version>${bouncycastle.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.codehaus.plexus</groupId>
         <artifactId>plexus-classworlds</artifactId>
diff --git a/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/JettyHomeTester.java b/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/JettyHomeTester.java
index e4587431f221..350facbd6337 100644
--- a/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/JettyHomeTester.java
+++ b/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/JettyHomeTester.java
@@ -89,16 +89,6 @@ private JettyHomeTester(Config config)
         this.config = config;
     }
 
-    /**
-     * Starts the distribution with the given arguments
-     *
-     * @param args arguments to use to start the distribution
-     */
-    public JettyHomeTester.Run start(String... args) throws Exception
-    {
-        return start(Arrays.asList(args));
-    }
-
     public Path getJettyBase()
     {
         return config.getJettyBase();
@@ -109,12 +99,39 @@ public Path getJettyHome()
         return config.getJettyHome();
     }
 
+    /**
+     * Starts the distribution with the given arguments
+     *
+     * @param args arguments to use to start the distribution
+     */
+    public JettyHomeTester.Run start(String... args) throws Exception
+    {
+        return start(Arrays.asList(args));
+    }
+
     /**
      * Start the distribution with the arguments
      *
      * @param args arguments to use to start the distribution
      */
     public JettyHomeTester.Run start(List<String> args) throws Exception
+    {
+        return start(List.of(), args);
+    }
+
+    /**
+     * <p>Starts the distribution with the given JVM arguments, and program arguments.</p>
+     * <p>JVM arguments allow for example to attach to the JVM with a debugger, and/or
+     * set Jetty logging properties; specify respectively
+     * {@code -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005}
+     * and {@code -Dorg.eclipse.jetty.LEVEL=DEBUG}.</p>
+     *
+     * @param jvmArgs the JVM arguments
+     * @param args the program arguments
+     * @return the JVM process
+     * @throws Exception if the JVM process cannot be executed
+     */
+    public JettyHomeTester.Run start(List<String> jvmArgs, List<String> args) throws Exception
     {
         File jettyBaseDir = getJettyBase().toFile();
         Path workDir = Files.createDirectories(jettyBaseDir.toPath().resolve("work"));
@@ -122,6 +139,7 @@ public JettyHomeTester.Run start(List<String> args) throws Exception
         List<String> commands = new ArrayList<>();
         commands.add(Tester.getJavaExecutable("java"));
         commands.addAll(config.getJVMArgs());
+        commands.addAll(jvmArgs);
         commands.add("-Djava.io.tmpdir=" + workDir.toAbsolutePath());
         int debugPort = Integer.getInteger("distribution.debug.port", 0);
         if (debugPort > 0)
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/BouncyCastleModuleTest.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/BouncyCastleModuleTest.java
new file mode 100644
index 000000000000..b92a251b50fa
--- /dev/null
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/BouncyCastleModuleTest.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.tests.distribution;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.tests.testers.JettyHomeTester;
+import org.eclipse.jetty.tests.testers.Tester;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Isolated("Modifies the Security TLS Provider")
+public class BouncyCastleModuleTest
+{
+    @Test
+    public void testBouncyCastleModule() throws Exception
+    {
+        String jettyVersion = System.getProperty("jettyVersion");
+        JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
+            .jettyVersion(jettyVersion)
+            .build();
+
+        try (JettyHomeTester.Run configRun = distribution.start("--approve-all-licenses", "--add-modules=https,bouncycastle,test-keystore"))
+        {
+            assertTrue(configRun.awaitFor(30, TimeUnit.SECONDS));
+            assertEquals(0, configRun.getExitValue());
+
+            int httpsPort = Tester.freePort();
+            try (JettyHomeTester.Run startRun = distribution.start(List.of("jetty.ssl.selectors=1", "jetty.ssl.port=" + httpsPort)))
+            {
+                assertTrue(startRun.awaitConsoleLogsFor("Started oejs.Server@", 10, TimeUnit.SECONDS));
+                assertTrue(startRun.getLogs().stream().anyMatch(line -> line.contains("provider=BCJSSE")));
+
+                try (HttpClient httpClient = new HttpClient())
+                {
+                    httpClient.setSslContextFactory(new SslContextFactory.Client(true));
+                    httpClient.start();
+                    ContentResponse response = httpClient.GET("https://localhost:" + httpsPort);
+                    assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
+                }
+            }
+        }
+    }
+}
diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/ConscryptModuleTest.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/ConscryptModuleTest.java
new file mode 100644
index 000000000000..2eb203bf34d2
--- /dev/null
+++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/ConscryptModuleTest.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.tests.distribution;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.tests.testers.JettyHomeTester;
+import org.eclipse.jetty.tests.testers.Tester;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Isolated("Modifies the Security TLS Provider")
+public class ConscryptModuleTest
+{
+    @Test
+    public void testConscryptModule() throws Exception
+    {
+        String jettyVersion = System.getProperty("jettyVersion");
+        JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
+            .jettyVersion(jettyVersion)
+            .build();
+
+        try (JettyHomeTester.Run configRun = distribution.start("--approve-all-licenses", "--add-modules=https,conscrypt,test-keystore"))
+        {
+            assertTrue(configRun.awaitFor(30, TimeUnit.SECONDS));
+            assertEquals(0, configRun.getExitValue());
+
+            int httpsPort = Tester.freePort();
+            try (JettyHomeTester.Run startRun = distribution.start(List.of("jetty.ssl.selectors=1", "jetty.ssl.port=" + httpsPort)))
+            {
+                assertTrue(startRun.awaitConsoleLogsFor("Started oejs.Server@", 10, TimeUnit.SECONDS));
+                assertTrue(startRun.getLogs().stream().anyMatch(line -> line.contains("provider=Conscrypt")));
+
+                try (HttpClient httpClient = new HttpClient())
+                {
+                    httpClient.setSslContextFactory(new SslContextFactory.Client(true));
+                    httpClient.start();
+                    ContentResponse response = httpClient.GET("https://localhost:" + httpsPort);
+                    assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
+                }
+            }
+        }
+    }
+}


Reviews:
user: login: joakime
body: The new tags with only 1 entry doesn't work.
How about a new tag for both bouncycastle and conscrypt instead?
Like `tls` or `security.tls` or something like that?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:41:33Z
user: login: lorban
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:15:40Z
user: login: elvinfucom
state: COMMENTED
author_association: NONE
submitted_at: 2025-09-28T06:45:24Z
user: login: sbordet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-09-29T13:38:08Z

Review comments:
diff_hunk: @@ -3,6 +3,7 @@ Installs the Conscrypt JSSE provider.
 
 [tags]
 3rdparty
+conscrypt
path: jetty-core/jetty-alpn/jetty-alpn-conscrypt-server/src/main/config/modules/conscrypt.mod
user: login: joakime
body: Are there more than 1 module in this tag?
created_at: 2025-03-19T18:39:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+[description]
+Installs the Bouncy Castle JSSE provider.
+
+[tags]
+3rdparty
+bouncycastle
path: jetty-core/jetty-alpn/jetty-alpn-bouncycastle-server/src/main/config/modules/bouncycastle.mod
user: login: joakime
body: If this is the only module in this tag, then drop this tag.
created_at: 2025-03-19T18:40:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -590,6 +590,24 @@ Then, you must configure the JDK with the Conscrypt provider, and configure Jett
 include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=conscrypt]
 ----
 
+[[connector-protocol-tls-bouncycastle]]
+==== Using Bouncy Castle as SSL/TLS Provider
+
+If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime.
+
+OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider.
+
+link:https://www.bouncycastle.org/[Bouncy Castle] is a pure Java alternative provider for the Java security APIs, and includes a TLS provider.
+
+To use Bouncy Castle as TLS provider, you must have the Bouncy Castle jars and the Jetty dependency `jetty-alpn-bouncycastle-server-{jetty-version}.jar` in the class-path or module-path.
+
+Then, you must configure the JDK with the Conscrypt provider, and configure Jetty to use the Conscrypt provider, in this way:
path: documentation/jetty/modules/programming-guide/pages/server/http.adoc
user: login: elvinfucom
body: The "Conscrypt" should be "Bouncy Castle".
created_at: 2025-09-28T06:45:24Z
author_association: NONE
diff_hunk: @@ -590,6 +590,24 @@ Then, you must configure the JDK with the Conscrypt provider, and configure Jett
 include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=conscrypt]
 ----
 
+[[connector-protocol-tls-bouncycastle]]
+==== Using Bouncy Castle as SSL/TLS Provider
+
+If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime.
+
+OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider.
+
+link:https://www.bouncycastle.org/[Bouncy Castle] is a pure Java alternative provider for the Java security APIs, and includes a TLS provider.
+
+To use Bouncy Castle as TLS provider, you must have the Bouncy Castle jars and the Jetty dependency `jetty-alpn-bouncycastle-server-{jetty-version}.jar` in the class-path or module-path.
+
+Then, you must configure the JDK with the Conscrypt provider, and configure Jetty to use the Conscrypt provider, in this way:
path: documentation/jetty/modules/programming-guide/pages/server/http.adoc
user: login: sbordet
body: @elvinfucom good catch, thanks! Fixed now.
created_at: 2025-09-29T13:38:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: sbordet
created_at: 2025-03-19T19:24:28Z
body: @joakime the idea is that tags are for searching modules.
So if my query is "italy", I'll find nothing.
But if I'm curious if Jetty supports Bouncy Castle, my query is an easy-to-guess "bouncycastle" and I'll find one module.

I'm not against adding other tags such as "tls", but "bouncycastle" and "conscrypt" should remain.
author_association: CONTRIBUTOR
user: login: joakime
created_at: 2025-03-19T19:55:32Z
body: > @joakime the idea is that tags are for searching modules.
> So if my query is "italy", I'll find nothing.
> But if I'm curious if Jetty supports Bouncy Castle, my query is an easy-to-guess "bouncycastle" and I'll find one module.

I think you are confusing --list-modules with --show-module
author_association: CONTRIBUTOR