Skip to content

feat: Use bouncycastle to generate certificates#3354

Merged
tomakehurst merged 4 commits into
wiremock:masterfrom
Malandril:feature/switch_to_bouncycastle
Apr 23, 2026
Merged

feat: Use bouncycastle to generate certificates#3354
tomakehurst merged 4 commits into
wiremock:masterfrom
Malandril:feature/switch_to_bouncycastle

Conversation

@Malandril

@Malandril Malandril commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

References

This pr switches the certificate generation to bouncy castle #3117 to fix issues with java 17+ and fix #3333

Closes #3117

Submitter checklist

  • Recommended: Join WireMock Slack to get any help in #help-contributing or a project-specific channel like #wiremock-java
  • The PR request is well described and justified, including the body and the references
  • The PR title represents the desired changelog entry
  • The repository's code style is followed (see the contributing guide)
  • Test coverage that demonstrates that the change works as expected
  • For new features, there's necessary documentation in this pull request or in a subsequent PR to wiremock.org

@Malandril Malandril requested a review from a team as a code owner March 4, 2026 23:21
`maven-publish`
id("com.diffplug.spotless")
id("com.github.johnrengelman.shadow")
id("com.gradleup.shadow")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Need to switch to a more recent shadow jar release to support bouncycastle without issues.

@tomakehurst

Copy link
Copy Markdown
Member

Thanks for contributing this, we've been wanting to make this change for a long time.

The biggest concern we've had about this is the ~10Mb it'll add to the standalone file size. Can you think of way we can mitigate this?

The idea we were kicking around was to have a separate library just to do the fake CA stuff using Bouncycastle, and shrink it aggressively with Proguard (or similar). This is obviously a bit more hassle to maintain than keeping it in the core codebase, so we'd welcome any more convenient approaches.

@Malandril

Copy link
Copy Markdown
Contributor Author

Using the minimize from shadow jar seems to work well the total size of the jar is around 25 MB including bouncycastle.

Unminimized with bouncycastle its ~ 34 MB
Unminimized without bouncycastle its ~ 24 MB
Minimized without bouncycastle its ~ 18 MB
Minimized with bouncycastle its ~ 25 MB

However it might delete some classes that are dynamically loaded, i tried a few requests and didn't find any case where it caused issues.

@tomakehurst

Copy link
Copy Markdown
Member

I really want to merge this, but I'm nervous about applying minimize() to the entire project. It risks breaking things quietly in future, even if we're OK now.

One reasonable compromise might be to move all of the code relating to certificate wrangling to a dedicated sub-project, which would own the bouncycastle dependency. Then minimize() could have an inclusion filter over just that project. It'll produce a bigger file than minimising everything, but be far less likely to cause regressions.

@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch from 4911927 to 68fa15a Compare March 12, 2026 10:24
Comment thread gradle/libs.versions.toml
[plugins]
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" }
spotless = { id = "com.diffplug.spotless", version = "8.3.0" }
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Actually unused, as the version is set in the buildSrc

not(
assignableTo(
com.github.tomakehurst.wiremock.standalone.WireMockServerRunner.class)))
.and(not(ANONYMOUS_CLASSES))

@Malandril Malandril Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Locally the tests where flaky, and anonymous classes where detected as unused, and sometimes the test passed, I excluded anonymous classes

@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch from 68fa15a to f7177ee Compare March 12, 2026 10:36
@@ -0,0 +1,95 @@
plugins {

@Malandril Malandril Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: With the new version of shadow jar, there is an issue with maven publication when the main project has multiple publications

Execution failed for task ':generatePomFileForMavenJavaPublication'.
> Failed to query the value of property 'dependencies'.
   > Publishing is not able to resolve a dependency on a project with multiple publications that have different coordinates.
     Found the following publications in root project 'wiremock':
       - Maven publication 'mavenJava' with coordinates org.wiremock:wiremock:4.0.0-beta.29
       - Maven publication 'standaloneJar' with coordinates org.wiremock:wiremock-standalone:4.0.0-beta.29

So i had to extract the to a new submodule the standalone jar publication

@Malandril

Malandril commented Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

@tomakehurst I extracted the certificate generation to a submodule, and running the publication i encountered some issues, so I had to extract the standalone publicaition to its own submodule.

Maybe you have an idea to make it work in a simpler way ?
Also might not be the cleanest way i created the certificate-generator module, so feel free if you have any suggestion
In the end it creates a standalone jar with a size of ~ 30MB against ~34MB with bouncycastle unminimized.

Comment thread .tool-versions
@@ -0,0 +1 @@
java temurin-17.0.18+8 No newline at end of file

@Malandril Malandril Mar 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Asdf config file to set the correct java version, I added it as it helps to switch automatically to the right java version, feel free to tell me to remove it if you think its unecessary

@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch 3 times, most recently from e3a11cc to a4bfcb1 Compare March 16, 2026 16:38
@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch from a4bfcb1 to 513399d Compare March 20, 2026 11:12
@tomakehurst

Copy link
Copy Markdown
Member

Sorry for the delay reviewing this. I think this looks good, and having a sub-module for the standalone JAR is probably no bad thing. I'm doing some final eyeballing of the JAR, but will merge assuming that doesn't show up anything.

@Mahoney can you think of any reason having a submodule for the standalone JAR would be a problem? I seem to remember us deciding not to do this in the distant past, but I don't know if any of the reasons are still valid.

@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch from 513399d to bb8a369 Compare April 6, 2026 16:08
@Malandril

Copy link
Copy Markdown
Contributor Author

I updated the release and localRelease tasks to also publish the standalone to try to keep the same behaviour

# Conflicts:
#	gradle/libs.versions.toml

diff --git c/build.gradle.kts i/build.gradle.kts
index 34a79bc..f79dcb1 100644
--- c/build.gradle.kts
+++ i/build.gradle.kts
@@ -106,6 +106,7 @@ dependencies {
   testImplementation(libs.mockito.core)
   testImplementation(libs.mockito.junit.jupiter)
   testImplementation(libs.scala.library)
+  testImplementation(libs.bouncycastle.bcpkix)

   testRuntimeOnly(files("src/test/resources/classpath file source/classpathfiles.zip", "src/test/resources/classpath-filesource.jar"))
   testRuntimeOnly(files("test-extension/test-extension.jar"))
@@ -385,7 +386,6 @@ eclipse.classpath.file {
       .filter { it.path.contains("JRE_CONTAINER") }
       .forEach {
         it.entryAttributes["module"] = true
-        it.entryAttributes["add-exports"] = "java.base/sun.security.x509=ALL-UNNAMED"
       }
   }
 }
diff --git c/buildSrc/build.gradle.kts i/buildSrc/build.gradle.kts
index 0477e81..c210374 100644
--- c/buildSrc/build.gradle.kts
+++ i/buildSrc/build.gradle.kts
@@ -11,7 +11,7 @@ repositories {

 dependencies {
   implementation("com.diffplug.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:6.25.0")
-  implementation("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1")
+  implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:8.3.10")
   implementation("org.sonarqube:org.sonarqube.gradle.plugin:6.2.0.5505")
   implementation("com.vanniktech.maven.publish.base:com.vanniktech.maven.publish.base.gradle.plugin:0.35.0")
 }
diff --git c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
index 4d3a736..26f52ba 100644
--- c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
+++ i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
@@ -9,7 +9,7 @@ plugins {
   signing
   `maven-publish`
   id("com.diffplug.spotless")
-  id("com.github.johnrengelman.shadow")
+  id("com.gradleup.shadow")
   id("org.sonarqube")
   id("com.vanniktech.maven.publish.base")
 }
@@ -30,7 +30,6 @@ java {

 tasks.jar {
   manifest {
-    attributes("Add-Exports" to "java.base/sun.security.x509")
     attributes("Implementation-Version" to project.version)
     attributes("Implementation-Title" to "WireMock")
   }
@@ -44,7 +43,6 @@ tasks {
     options.encoding = "UTF-8"
     options.compilerArgs.addAll(listOf(
       "-XDenableSunApiLintControl",
-      "--add-exports=java.base/sun.security.x509=ALL-UNNAMED",
     ))
   }

diff --git c/gradle/libs.versions.toml i/gradle/libs.versions.toml
index e958b72..a49b8fd 100644
--- c/gradle/libs.versions.toml
+++ i/gradle/libs.versions.toml
@@ -11,6 +11,7 @@ mockito = "5.23.0"
 jmh = "1.37"
 apache-http5 = "5.4.3"
 archunit = "1.4.2"
+bouncycastle = "1.84"

 [libraries]
 # Jetty dependencies
@@ -130,6 +131,9 @@ jakarta-websockets = { module = "jakarta.websocket:jakarta.websocket-client-api"

 jspecify = { module = "org.jspecify:jspecify", version = "1.0.0" }

+bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on",  version.ref = "bouncycastle" }
+bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on",  version.ref = "bouncycastle" }
+
 [plugins]
 nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" }
 spotless = { id = "com.diffplug.spotless", version = "8.4.0" }
diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java
index ab4da08..5cd6cac 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2021 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,13 +15,19 @@
  */
 package com.github.tomakehurst.wiremock.crypto;

+import java.io.IOException;
 import java.security.InvalidKeyException;
 import java.security.KeyPair;
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import org.bouncycastle.operator.OperatorCreationException;

 public interface CertificateSpecification {
   X509Certificate certificateFor(KeyPair keyPair)
-      throws CertificateException, InvalidKeyException, SignatureException;
+      throws CertificateException,
+          InvalidKeyException,
+          SignatureException,
+          IOException,
+          OperatorCreationException;
 }
diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java
index 2297d48..8ca86b3 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2021 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,99 +15,46 @@
  */
 package com.github.tomakehurst.wiremock.crypto;

-import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
 import static java.util.Objects.requireNonNull;

+import com.github.tomakehurst.wiremock.http.ssl.CertificateAuthority;
 import java.io.IOException;
-import java.math.BigInteger;
-import java.security.InvalidKeyException;
 import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.SecureRandom;
-import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.Date;
-import sun.security.x509.AlgorithmId;
-import sun.security.x509.CertificateAlgorithmId;
-import sun.security.x509.CertificateIssuerName;
-import sun.security.x509.CertificateSerialNumber;
-import sun.security.x509.CertificateSubjectName;
-import sun.security.x509.CertificateValidity;
-import sun.security.x509.CertificateX509Key;
-import sun.security.x509.X500Name;
-import sun.security.x509.X509CertImpl;
-import sun.security.x509.X509CertInfo;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.operator.OperatorCreationException;

-@SuppressWarnings("sunapi")
 public class X509CertificateSpecification implements CertificateSpecification {

-  private final X509CertificateVersion version;
-  private final X500Name subject;
-  private final X500Name issuer;
+  private final String subject;
+  private final String issuer;
   // java.time is JDK8 only
   private final Date notBefore;
   private final Date notAfter;

-  public X509CertificateSpecification(
-      X509CertificateVersion version, String subject, String issuer, Date notBefore, Date notAfter)
+  public X509CertificateSpecification(String subject, String issuer, Date notBefore, Date notAfter)
       throws IOException {
-    this.version = requireNonNull(version);
-    this.subject = new X500Name(requireNonNull(subject));
-    this.issuer = new X500Name(requireNonNull(issuer));
+    this.subject = requireNonNull(subject);
+    this.issuer = requireNonNull(issuer);
     this.notBefore = requireNonNull(notBefore);
     this.notAfter = requireNonNull(notAfter);
   }

   @OverRide
   public X509Certificate certificateFor(KeyPair keyPair)
-      throws CertificateException, InvalidKeyException, SignatureException {
-    try {
-      SecureRandom random = new SecureRandom();
-
-      X509CertInfo info = new X509CertInfo();
-      info.set(X509CertInfo.VERSION, version.getVersion());
-
-      // On Java <= 1.7 it has to be a `CertificateSubjectName`
-      // On Java >= 1.8 it has to be an `X500Name`
-      try {
-        info.set(X509CertInfo.SUBJECT, subject);
-      } catch (CertificateException ignore) {
-        info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subject));
-      }
-
-      // On Java <= 1.7 it has to be a `CertificateIssuerName`
-      // On Java >= 1.8 it has to be an `X500Name`
-      try {
-        info.set(X509CertInfo.ISSUER, issuer);
-      } catch (CertificateException ignore) {
-        info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuer));
-      }
-
-      info.set(X509CertInfo.VALIDITY, new CertificateValidity(notBefore, notAfter));
-
-      info.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic()));
-      info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(new BigInteger(64, random)));
-      info.set(
-          X509CertInfo.ALGORITHM_ID,
-          new CertificateAlgorithmId(new AlgorithmId(AlgorithmId.SHA256_oid)));
-
-      // Sign the cert to identify the algorithm that's used.
-      X509CertImpl cert = new X509CertImpl(info);
-      cert.sign(keyPair.getPrivate(), "SHA256withRSA");
-
-      // Update the algorithm and sign again.
-      info.set(
-          CertificateAlgorithmId.NAME + '.' + CertificateAlgorithmId.ALGORITHM,
-          cert.get(X509CertImpl.SIG_ALG));
-      cert = new X509CertImpl(info);
-      cert.sign(keyPair.getPrivate(), "SHA256withRSA");
-      cert.verify(keyPair.getPublic());
-
-      return cert;
-    } catch (IOException | NoSuchAlgorithmException | NoSuchProviderException e) {
-      return throwUnchecked(e, null);
-    }
+      throws CertificateException, IOException, OperatorCreationException {
+    return CertificateAuthority.buildCertificate(
+        CertificateAuthority.SIG_ALG_PREFIX + "RSA",
+        keyPair.getPublic(),
+        keyPair.getPrivate(),
+        keyPair.getPublic(),
+        notBefore,
+        notAfter,
+        new X500Principal(subject),
+        new X500Principal(issuer),
+        null,
+        false);
   }
 }
diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java
deleted file mode 100644
index e5907d2..000000000
--- c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Thomas Akehurst
- *
- * 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.github.tomakehurst.wiremock.crypto;
-
-import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
-
-import java.io.IOException;
-import sun.security.x509.CertificateVersion;
-
-@SuppressWarnings("sunapi")
-public enum X509CertificateVersion {
-  V1(CertificateVersion.V1),
-  V2(CertificateVersion.V2),
-  V3(CertificateVersion.V3);
-
-  private final CertificateVersion version;
-
-  X509CertificateVersion(int version) {
-    this.version = getVersion(version);
-  }
-
-  private static CertificateVersion getVersion(int version) {
-    try {
-      return new CertificateVersion(version);
-    } catch (IOException e) {
-      return throwUnchecked(e, null);
-    }
-  }
-
-  CertificateVersion getVersion() {
-    return version;
-  }
-}
diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java
index 93ef47f..cf60944 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ package com.github.tomakehurst.wiremock.http;
 import static com.github.tomakehurst.wiremock.common.ProxySettings.NO_PROXY;
 import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
 import static com.github.tomakehurst.wiremock.crypto.InMemoryKeyStore.KeyStoreType.JKS;
-import static com.github.tomakehurst.wiremock.crypto.X509CertificateVersion.V3;
 import static java.util.Collections.emptyList;

 import com.github.tomakehurst.wiremock.WireMockServer;
@@ -55,7 +54,6 @@ public abstract class HttpClientFactoryCertificateVerificationTest {

     CertificateSpecification certificateSpecification =
         new X509CertificateSpecification(
-            /* version= */ V3,
             /* subject= */ "CN=" + certificateCN,
             /* issuer= */ "CN=wiremock.org",
             /* notBefore= */ new Date(),
diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java
index 5472cb6..7d9c4a2 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@ package com.github.tomakehurst.wiremock.http;

 import static com.github.tomakehurst.wiremock.client.WireMock.*;
 import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
-import static com.github.tomakehurst.wiremock.crypto.X509CertificateVersion.V3;
 import static com.github.tomakehurst.wiremock.http.RequestMethod.GET;
 import static com.github.tomakehurst.wiremock.matching.MockRequest.mockRequest;
 import static com.github.tomakehurst.wiremock.stubbing.ServeEventFactory.newPostMatchServeEvent;
@@ -564,7 +563,6 @@ public class ProxyResponseRendererTest {

     CertificateSpecification certificateSpecification =
         new X509CertificateSpecification(
-            /* version= */ V3,
             /* subject= */ "CN=localhost",
             /* issuer= */ "CN=wiremock.org",
             /* notBefore= */ new Date(),
diff --git c/wiremock-core/build.gradle.kts i/wiremock-core/build.gradle.kts
index 6cada0d..333b253 100644
--- c/wiremock-core/build.gradle.kts
+++ i/wiremock-core/build.gradle.kts
@@ -26,6 +26,8 @@ dependencies {

     api(libs.jspecify)

+    api(libs.bouncycastle.bcpkix)
+
     implementation(libs.apache.http5.client)
     implementation(libs.handlebars.helpers) {
         exclude(group = "org.mozilla", module = "rhino")
@@ -42,6 +44,8 @@ dependencies {
     }
     implementation(libs.xmlunit.placeholders)

+    implementation(libs.bouncycastle.bcprov)
+
     modules {
         module("org.apache.logging.log4j:log4j-core") {
             replacedBy("org.apache.logging.log4j:log4j-to-slf4j")
diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
index a43b7f8..1829fcf 100644
--- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
+++ i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,22 +16,46 @@
 package com.github.tomakehurst.wiremock.http.ssl;

 import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend;
-import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
 import static java.util.Objects.requireNonNull;

 import java.io.IOException;
-import java.security.*;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.time.Period;
 import java.time.ZonedDateTime;
 import java.util.Date;
 import javax.net.ssl.SNIHostName;
-import sun.security.x509.*;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.bc.BcX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.jspecify.annotations.Nullable;

-@SuppressWarnings("sunapi")
 public class CertificateAuthority {

+  public static final String SIG_ALG_PREFIX = "SHA256With";
+  private static final Period CA_VALIDITY = Period.ofYears(10);
+  private static final Period CERT_VALIDITY = Period.ofYears(1);
+  public static final String CA_SUBJECT = "CN=WireMock Local Self Signed Root Certificate";
+
   private final X509Certificate[] certificateChain;
   private final PrivateKey key;

@@ -46,72 +70,34 @@ public class CertificateAuthority {
   public static CertificateAuthority generateCertificateAuthority()
       throws CertificateGenerationUnsupportedException {
     try {
-      KeyPair pair = generateKeyPair("RSA");
-      String sigAlg = "SHA256WithRSA";
-      X509CertInfo info =
-          makeX509CertInfo(
-              sigAlg,
-              "WireMock Local Self Signed Root Certificate",
-              ZonedDateTime.now().minus(Period.ofDays(1)),
-              Period.ofYears(10),
+      String keyType = "RSA";
+      KeyPair pair = generateKeyPair(keyType);
+      var certificate =
+          buildCertificate(
+              SIG_ALG_PREFIX + keyType,
               pair.getPublic(),
-              certificateAuthorityExtensions(pair.getPublic()));
-
-      X509CertImpl certificate = selfSign(info, pair.getPrivate(), sigAlg);
+              pair.getPrivate(),
+              pair.getPublic(),
+              CA_VALIDITY,
+              new X500Principal(CA_SUBJECT),
+              new X500Principal(CA_SUBJECT),
+              null,
+              true);

       return new CertificateAuthority(new X509Certificate[] {certificate}, pair.getPrivate());
     } catch (NoSuchAlgorithmException
-        | NoSuchProviderException
-        | InvalidKeyException
         | CertificateException
-        | SignatureException
+        | IOException
+        | OperatorCreationException
         | NoSuchMethodError
         | VerifyError
         | NoClassDefFoundError
-        | IOException
         | IllegalAccessError e) {
       throw new CertificateGenerationUnsupportedException(
           "Your runtime does not support generating certificates at runtime", e);
     }
   }

-  private static X509CertImpl selfSign(X509CertInfo info, PrivateKey privateKey, String sigAlg)
-      throws CertificateException,
-          NoSuchAlgorithmException,
-          InvalidKeyException,
-          NoSuchProviderException,
-          SignatureException {
-    X509CertImpl certificate = new X509CertImpl(info);
-    certificate.sign(privateKey, sigAlg);
-    return certificate;
-  }
-
-  private static CertificateExtensions certificateAuthorityExtensions(PublicKey publicKey) {
-    try {
-      KeyIdentifier keyId = new KeyIdentifier(publicKey);
-      byte[] keyIdBytes = keyId.getIdentifier();
-      CertificateExtensions extensions = new CertificateExtensions();
-      extensions.set(
-          AuthorityKeyIdentifierExtension.NAME,
-          new AuthorityKeyIdentifierExtension(keyId, null, null));
-
-      extensions.set(
-          BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, Integer.MAX_VALUE));
-
-      KeyUsageExtension keyUsage = new KeyUsageExtension(new boolean[7]);
-      keyUsage.set(KeyUsageExtension.KEY_CERTSIGN, true);
-      keyUsage.set(KeyUsageExtension.CRL_SIGN, true);
-      extensions.set(KeyUsageExtension.NAME, keyUsage);
-
-      extensions.set(
-          SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension(keyIdBytes));
-
-      return extensions;
-    } catch (IOException e) {
-      return throwUnchecked(e, null);
-    }
-  }
-
   public X509Certificate[] certificateChain() {
     return certificateChain;
   }
@@ -124,106 +110,112 @@ public class CertificateAuthority {
       throws CertificateGenerationUnsupportedException {
     try {
       KeyPair pair = generateKeyPair(keyType);
-      String sigAlg = "SHA256With" + keyType;
-      X509CertInfo info =
-          makeX509CertInfo(
-              sigAlg,
-              hostName.getAsciiName(),
-              ZonedDateTime.now().minus(Period.ofDays(1)),
-              Period.ofYears(1),
+      X509Certificate issuer = certificateChain[0];
+
+      var certificate =
+          buildCertificate(
+              SIG_ALG_PREFIX + keyType,
               pair.getPublic(),
-              subjectAlternativeName(hostName));
+              key,
+              issuer.getPublicKey(),
+              CERT_VALIDITY,
+              new X500Principal("CN=" + hostName.getAsciiName()),
+              issuer.getIssuerX500Principal(),
+              hostName.getAsciiName(),
+              false);

-      X509CertImpl certificate = sign(info);
-
-      X509Certificate[] fullChain = prepend(certificate, certificateChain);
-      return new CertChainAndKey(fullChain, pair.getPrivate());
+      return new CertChainAndKey(prepend(certificate, certificateChain), pair.getPrivate());
     } catch (NoSuchAlgorithmException
-        | NoSuchProviderException
-        | InvalidKeyException
         | CertificateException
-        | SignatureException
+        | IOException
+        | OperatorCreationException
         | NoSuchMethodError
         | VerifyError
         | NoClassDefFoundError
-        | IOException
         | IllegalAccessError e) {
       throw new CertificateGenerationUnsupportedException(
           "Your runtime does not support generating certificates at runtime", e);
     }
   }

-  private X509CertImpl sign(X509CertInfo info)
-      throws CertificateException,
-          IOException,
-          NoSuchAlgorithmException,
-          InvalidKeyException,
-          NoSuchProviderException,
-          SignatureException {
-    X509Certificate issuerCertificate = certificateChain[0];
-    info.set(X509CertInfo.ISSUER, issuerCertificate.getSubjectDN());
-
-    X509CertImpl certificate = new X509CertImpl(info);
-    certificate.sign(key, issuerCertificate.getSigAlgName());
-    return certificate;
-  }
-
   private static KeyPair generateKeyPair(String keyType) throws NoSuchAlgorithmException {
     KeyPairGenerator keyGen = KeyPairGenerator.getInstance(keyType);
     keyGen.initialize(2048, new SecureRandom());
     return keyGen.generateKeyPair();
   }

-  private static X509CertInfo makeX509CertInfo(
+  public static X509Certificate buildCertificate(
       String sigAlg,
-      String subjectName,
-      ZonedDateTime start,
-      Period validity,
       PublicKey publicKey,
-      CertificateExtensions certificateExtensions)
-      throws IOException, CertificateException, NoSuchAlgorithmException {
-    ZonedDateTime end = start.plus(validity);
-
-    X500Name myname = new X500Name("CN=" + subjectName);
-    X509CertInfo info = new X509CertInfo();
-    // Add all mandatory attributes
-    info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
-    info.set(
-        X509CertInfo.SERIAL_NUMBER,
-        new CertificateSerialNumber(new java.util.Random().nextInt() & 0x7fffffff));
-    info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(AlgorithmId.get(sigAlg)));
-    info.set(X509CertInfo.SUBJECT, myname);
-    info.set(X509CertInfo.KEY, new CertificateX509Key(publicKey));
-    info.set(
-        X509CertInfo.VALIDITY,
-        new CertificateValidity(Date.from(start.toInstant()), Date.from(end.toInstant())));
-    info.set(X509CertInfo.ISSUER, myname);
-    info.set(X509CertInfo.EXTENSIONS, certificateExtensions);
-    return info;
+      PrivateKey signerPrivateKey,
+      PublicKey signerPublicKey,
+      Period validity,
+      X500Principal subject,
+      X500Principal issuer,
+      @nullable String sanDnsName,
+      boolean isCA)
+      throws IOException, OperatorCreationException, CertificateException {
+    ZonedDateTime start = ZonedDateTime.now().minus(Period.ofDays(1));
+    return buildCertificate(
+        sigAlg,
+        publicKey,
+        signerPrivateKey,
+        signerPublicKey,
+        Date.from(start.toInstant()),
+        Date.from(start.plus(validity).toInstant()),
+        subject,
+        issuer,
+        sanDnsName,
+        isCA);
   }

-  private static CertificateExtensions subjectAlternativeName(SNIHostName hostName) {
-    GeneralName name = new GeneralName(dnsName(hostName));
-    GeneralNames names = new GeneralNames();
-    names.add(name);
-    try {
-      CertificateExtensions extensions = new CertificateExtensions();
-      extensions.set(
-          SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(names));
-      return extensions;
-    } catch (IOException e) {
-      // it's an in memory op, should be impossible...
-      return throwUnchecked(e, null);
-    }
-  }
+  public static X509Certificate buildCertificate(
+      String sigAlg,
+      PublicKey publicKey,
+      PrivateKey signerPrivateKey,
+      PublicKey signerPublicKey,
+      Date notBefore,
+      Date notAfter,
+      X500Principal subject,
+      X500Principal issuer,
+      @nullable String sanDnsName,
+      boolean isCA)
+      throws IOException, CertificateException, OperatorCreationException {
+    SubjectPublicKeyInfo subjectKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
+    SubjectPublicKeyInfo signerKeyInfo =
+        SubjectPublicKeyInfo.getInstance(signerPublicKey.getEncoded());

-  private static DNSName dnsName(SNIHostName name) {
-    try {
-      return new DNSName(name.getAsciiName());
-    } catch (IOException e) {
-      // DNSName throws IOException for a parse error (which isn't an IO problem...)
-      // An SNIHostName should be guaranteed not to have a parse issue
-      return throwUnchecked(e, null);
+    X509v3CertificateBuilder builder =
+        new JcaX509v3CertificateBuilder(
+            issuer,
+            BigInteger.valueOf(new SecureRandom().nextLong()).abs(),
+            notBefore,
+            notAfter,
+            subject,
+            publicKey);
+
+    BcX509ExtensionUtils extUtils = new BcX509ExtensionUtils();
+    builder.addExtension(
+        Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(subjectKeyInfo));
+    builder.addExtension(
+        Extension.authorityKeyIdentifier,
+        false,
+        extUtils.createAuthorityKeyIdentifier(signerKeyInfo));
+
+    if (isCA) {
+      builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
+      builder.addExtension(
+          Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
     }
+
+    if (sanDnsName != null) {
+      builder.addExtension(
+          Extension.subjectAlternativeName,
+          false,
+          new GeneralNames(new GeneralName(GeneralName.dNSName, sanDnsName)));
+    }
+    ContentSigner signer = new JcaContentSignerBuilder(sigAlg).build(signerPrivateKey);
+    X509CertificateHolder holder = builder.build(signer);
+    return new JcaX509CertificateConverter().getCertificate(holder);
   }
 }
diff --git c/.tool-versions i/.tool-versions
new file mode 100644
index 000000000..01ad62a
--- /dev/null
+++ i/.tool-versions
@@ -0,0 +1 @@
+java temurin-17.0.18+8
\ No newline at end of file
diff --git c/build.gradle.kts i/build.gradle.kts
index f79dcb1..2cb691b 100644
--- c/build.gradle.kts
+++ i/build.gradle.kts
@@ -188,56 +188,6 @@ tasks.jar {
   }
 }

-tasks.shadowJar {
-  archiveBaseName = "wiremock-standalone"
-  archiveClassifier = ""
-  configurations = listOf(
-    project.configurations.runtimeClasspath.get(),
-  )
-
-  relocate("org.mortbay", "wiremock.org.mortbay")
-  relocate("org.eclipse", "wiremock.org.eclipse")
-  relocate("org.codehaus", "wiremock.org.codehaus")
-  relocate("com.google", "wiremock.com.google")
-  relocate("com.google.thirdparty", "wiremock.com.google.thirdparty")
-  relocate("com.fasterxml.jackson", "wiremock.com.fasterxml.jackson")
-  relocate("org.apache", "wiremock.org.apache")
-  relocate("org.xmlunit", "wiremock.org.xmlunit")
-  relocate("org.hamcrest", "wiremock.org.hamcrest")
-  relocate("org.skyscreamer", "wiremock.org.skyscreamer")
-  relocate("org.json", "wiremock.org.json")
-  relocate("net.minidev", "wiremock.net.minidev")
-  relocate("com.jayway", "wiremock.com.jayway")
-  relocate("org.objectweb", "wiremock.org.objectweb")
-  relocate("org.custommonkey", "wiremock.org.custommonkey")
-  relocate("net.javacrumbs", "wiremock.net.javacrumbs")
-  relocate("net.sf", "wiremock.net.sf")
-  relocate("com.github.jknack", "wiremock.com.github.jknack")
-  relocate("org.antlr", "wiremock.org.antlr")
-  relocate("jakarta.servlet", "wiremock.jakarta.servlet")
-  relocate("org.checkerframework", "wiremock.org.checkerframework")
-  relocate("org.hamcrest", "wiremock.org.hamcrest")
-  relocate("org.slf4j", "wiremock.org.slf4j")
-  relocate("joptsimple", "wiremock.joptsimple")
-  exclude("joptsimple/HelpFormatterMessages.properties")
-  relocate("org.yaml", "wiremock.org.yaml")
-  relocate("com.ethlo", "wiremock.com.ethlo")
-  relocate("com.networknt", "wiremock.com.networknt")
-  relocate("org.jspecify", "wiremock.org.jspecify")
-
-  dependencies {
-    exclude(dependency("junit:junit"))
-  }
-
-  mergeServiceFiles()
-
-  exclude("META-INF/maven/**")
-  exclude("META-INF/versions/17/**")
-  exclude("META-INF/versions/21/**")
-  exclude("META-INF/versions/22/**")
-  exclude("module-info.class")
-  exclude("handlebars-*.js")
-}

 publishing {
   publications {
@@ -251,21 +201,6 @@ publishing {
         description = "A web service test double for all occasions"
       }
     }
-
-    create<MavenPublication>("standaloneJar") {
-      artifactId = "${tasks.jar.get().archiveBaseName.get()}-standalone"
-      project.shadow.component(this)
-
-      artifact(tasks.named("sourcesJar"))
-      artifact(tasks.named("javadocJar"))
-      artifact(testJar)
-
-      pom.packaging = "jar"
-      pom {
-        name = "WireMock"
-        description = "A web service test double for all occasions - standalone edition"
-      }
-    }
   }
 }

@@ -291,7 +226,6 @@ val addGitTag by tasks.registering {
 tasks.publish {
   dependsOn(
     checkReleasePreconditions,
-    "signStandaloneJarPublication",
     "signMavenJavaPublication",
   )
 }
@@ -301,7 +235,7 @@ tasks.withType<AbstractPublishToMaven>().configureEach {
 }

 tasks.assemble {
-  dependsOn(tasks.jar, tasks.shadowJar)
+  dependsOn(tasks.jar)
 }

 tasks.register("release") {
diff --git c/buildSrc/build.gradle.kts i/buildSrc/build.gradle.kts
index c210374..ae30478 100644
--- c/buildSrc/build.gradle.kts
+++ i/buildSrc/build.gradle.kts
@@ -11,7 +11,7 @@ repositories {

 dependencies {
   implementation("com.diffplug.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:6.25.0")
-  implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:8.3.10")
+  implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.2.2")
   implementation("org.sonarqube:org.sonarqube.gradle.plugin:6.2.0.5505")
   implementation("com.vanniktech.maven.publish.base:com.vanniktech.maven.publish.base.gradle.plugin:0.35.0")
 }
diff --git c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
index 9b5e600..88b3a5e 100644
--- c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
+++ i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts
@@ -206,12 +206,6 @@ publishing {
     }
   }

-  getComponents().withType<AdhocComponentWithVariants>().forEach { c ->
-    c.withVariantsFromConfiguration(configurations.shadowRuntimeElements.get()) {
-      skip()
-    }
-  }
-
   publications {
     withType<MavenPublication> {
       pom {
@@ -223,6 +217,10 @@ publishing {
   }
 }

+shadow {
+  addShadowVariantIntoJavaComponent = false
+}
+
 mavenPublishing {
   publishToMavenCentral(automaticRelease = true)
 }
diff --git c/gradle/libs.versions.toml i/gradle/libs.versions.toml
index 521f0b5..8cf8531 100644
--- c/gradle/libs.versions.toml
+++ i/gradle/libs.versions.toml
@@ -137,7 +137,6 @@ bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on",  version.ref
 [plugins]
 nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" }
 spotless = { id = "com.diffplug.spotless", version = "8.3.0" }
-shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
 sonarqube = { id = "org.sonarqube", version = "7.2.3.7755" }
 jmh = { id = "me.champeau.jmh", version = "0.7.3" }
-task-tree = { id = "com.dorongold.task-tree", version = "4.0.1" }
+task-tree = { id = "com.dorongold.task-tree", version = "4.0.1" }
\ No newline at end of file
diff --git c/settings.gradle.kts i/settings.gradle.kts
index 4d79e1c..4e7d94f 100644
--- c/settings.gradle.kts
+++ i/settings.gradle.kts
@@ -5,6 +5,8 @@ plugins {
 rootProject.name = "wiremock"

 include("wiremock-core")
+include("wiremock-core:certificate-generator")
+include("wiremock-standalone")
 include("wiremock-junit4")
 include("wiremock-junit5")
 include("wiremock-jetty")
diff --git c/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java i/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java
index 950f6c3..37149bb 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
 package com.github.tomakehurst.wiremock.common;

 import static com.github.tomakehurst.wiremock.common.ArrayFunctions.concat;
-import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;

 import org.junit.jupiter.api.Test;
@@ -64,37 +63,4 @@ class ArrayFunctionsTest {
     second[0] = 30;
     assertArrayEquals(new Integer[] {1, 2, 3, 4}, result);
   }
-
-  @test
-  void prependNullAndEmpty() {
-    assertArrayEquals(new Integer[] {null}, prepend(null, empty));
-  }
-
-  @test
-  void prependSomeAndEmpty() {
-    Integer[] result = prepend(1, empty);
-    assertArrayEquals(new Integer[] {1}, result);
-  }
-
-  @test
-  void prependNullAndNonEmpty() {
-    Integer[] second = {1, 2};
-
-    Integer[] result = prepend(null, second);
-    assertArrayEquals(new Integer[] {null, 1, 2}, result);
-
-    second[0] = 10;
-    assertArrayEquals(new Integer[] {null, 1, 2}, result);
-  }
-
-  @test
-  void prependSomeAndNonEmpty() {
-    Integer[] second = {2, 3};
-
-    Integer[] result = prepend(1, second);
-    assertArrayEquals(new Integer[] {1, 2, 3}, result);
-
-    second[0] = 30;
-    assertArrayEquals(new Integer[] {1, 2, 3}, result);
-  }
 }
diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java
new file mode 100644
index 000000000..eb45608
--- /dev/null
+++ i/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2026 Thomas Akehurst
+ *
+ * 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.github.tomakehurst.wiremock.http.ssl;
+
+import static com.github.tomakehurst.wiremock.http.ssl.CertificateAuthority.prepend;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class CertificateAuthorityTest {
+
+  private final Integer[] empty = new Integer[0];
+
+  @test
+  void prependNullAndEmpty() {
+    assertArrayEquals(new Integer[] {null}, prepend(null, empty));
+  }
+
+  @test
+  void prependSomeAndEmpty() {
+    Integer[] result = prepend(1, empty);
+    assertArrayEquals(new Integer[] {1}, result);
+  }
+
+  @test
+  void prependNullAndNonEmpty() {
+    Integer[] second = {1, 2};
+
+    Integer[] result = prepend(null, second);
+    assertArrayEquals(new Integer[] {null, 1, 2}, result);
+
+    second[0] = 10;
+    assertArrayEquals(new Integer[] {null, 1, 2}, result);
+  }
+
+  @test
+  void prependSomeAndNonEmpty() {
+    Integer[] second = {2, 3};
+
+    Integer[] result = prepend(1, second);
+    assertArrayEquals(new Integer[] {1, 2, 3}, result);
+
+    second[0] = 30;
+    assertArrayEquals(new Integer[] {1, 2, 3}, result);
+  }
+}
diff --git c/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java i/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java
index e950cbd..3b8e33d 100644
--- c/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java
+++ i/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021-2025 Thomas Akehurst
+ * Copyright (C) 2021-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package com.github.tomakehurst.wiremock.jetty.archunit;

 import static com.tngtech.archunit.base.DescribedPredicate.describe;
 import static com.tngtech.archunit.base.DescribedPredicate.not;
+import static com.tngtech.archunit.core.domain.JavaClass.Predicates.ANONYMOUS_CLASSES;
 import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
 import static com.tngtech.archunit.core.domain.JavaMember.Predicates.declaredIn;
 import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf;
@@ -81,6 +82,7 @@ class UnusedCodeTest {
                   not(
                       assignableTo(
                           com.github.tomakehurst.wiremock.standalone.WireMockServerRunner.class)))
+              .and(not(ANONYMOUS_CLASSES))
               .should(beReferencedClass)
               .as("should use all classes")
               .because("unused classes should be removed"));
diff --git c/src/test/resources/frozen/unused-classes i/src/test/resources/frozen/unused-classes
index a1f3bbc..e122073 100644
--- c/src/test/resources/frozen/unused-classes
+++ i/src/test/resources/frozen/unused-classes
@@ -1,7 +1 @@
-Class <com.github.tomakehurst.wiremock.client.WireMock$2> is unreferenced in (WireMock.java:0)
-Class <com.github.tomakehurst.wiremock.common.ContentTypes$1> is unreferenced in (ContentTypes.java:0)
-Class <com.github.tomakehurst.wiremock.common.xml.XmlNode$1> is unreferenced in (XmlNode.java:0)
-Class <com.github.tomakehurst.wiremock.extension.ServeEventListener$1> is unreferenced in (ServeEventListener.java:0)
-Class <com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.FormatJsonHelper$1> is unreferenced in (FormatJsonHelper.java:0)
-Class <com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.FormatXmlHelper$1> is unreferenced in (FormatXmlHelper.java:0)
-Class <com.github.tomakehurst.wiremock.http.JvmProxyConfigurer> is unreferenced in (JvmProxyConfigurer.java:0)
\ No newline at end of file
+Class <com.github.tomakehurst.wiremock.http.JvmProxyConfigurer> is unreferenced in (JvmProxyConfigurer.java:0)
diff --git c/wiremock-core/build.gradle.kts i/wiremock-core/build.gradle.kts
index 333b253..6f4e5e0 100644
--- c/wiremock-core/build.gradle.kts
+++ i/wiremock-core/build.gradle.kts
@@ -26,7 +26,7 @@ dependencies {

     api(libs.jspecify)

-    api(libs.bouncycastle.bcpkix)
+    api(project(":wiremock-core:certificate-generator"))

     implementation(libs.apache.http5.client)
     implementation(libs.handlebars.helpers) {
@@ -44,7 +44,6 @@ dependencies {
     }
     implementation(libs.xmlunit.placeholders)

-    implementation(libs.bouncycastle.bcprov)

     modules {
         module("org.apache.logging.log4j:log4j-core") {
diff --git c/wiremock-core/certificate-generator/build.gradle.kts i/wiremock-core/certificate-generator/build.gradle.kts
new file mode 100644
index 000000000..65afdcc
--- /dev/null
+++ i/wiremock-core/certificate-generator/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+  id("wiremock.common-conventions")
+  id("com.gradleup.shadow")
+}
+
+tasks.shadowJar {
+  archiveClassifier = ""
+  description = "Create a shadow JAR of all dependencies"
+  minimize()
+  configurations = listOf(
+    project.configurations.compileClasspath.get(),
+  )
+}
+
+tasks.jar {
+  enabled = false
+}
+
+shadow {
+  addShadowVariantIntoJavaComponent = true
+}
+
+dependencies {
+  // As we include them in the shadowed jar to reduce the size, we must not expose them to
+  // our consumers as transitive dependencies
+  compileOnly(libs.bouncycastle.bcpkix)
+  compileOnly(libs.bouncycastle.bcprov)
+}
+
+publishing {
+  publications {
+    create<MavenPublication>("shadow") {
+      artifactId = tasks.shadowJar.get().archiveBaseName.get()
+      from(components["shadow"])
+      artifact(tasks.sourcesJar)
+      pom {
+        name = "WireMock Bouncy Castle Wrapper"
+        description = "A bouncy castle wrapper with reduced size"
+      }
+    }
+  }
+}
+
+// Disable the plain jar from being published/consumed
+configurations.apiElements {
+  outgoing.artifacts.clear()
+  outgoing.artifact(tasks.shadowJar)
+}
+
+configurations.runtimeElements {
+  outgoing.artifacts.clear()
+  outgoing.artifact(tasks.shadowJar)
+}
diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java
similarity index 95%
rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java
rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java
index c839674..f089568 100644
--- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java
+++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
similarity index 81%
rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
index 1829fcf..b13a690 100644
--- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
+++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2026 Thomas Akehurst
+ * Copyright (C) 2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,12 +13,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.github.tomakehurst.wiremock.http.ssl;
+package com.github.tomakehurst.wiremock.http.ssl; /*
+                                                   * Copyright (C) 2020-2026 Thomas Akehurst
+                                                   *
+                                                   * 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.
+                                                   */

-import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend;
 import static java.util.Objects.requireNonNull;

 import java.io.IOException;
+import java.lang.reflect.Array;
 import java.math.BigInteger;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -47,7 +61,6 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
 import org.bouncycastle.operator.ContentSigner;
 import org.bouncycastle.operator.OperatorCreationException;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import org.jspecify.annotations.Nullable;

 public class CertificateAuthority {

@@ -152,7 +165,7 @@ public class CertificateAuthority {
       Period validity,
       X500Principal subject,
       X500Principal issuer,
-      @nullable String sanDnsName,
+      String sanDnsName,
       boolean isCA)
       throws IOException, OperatorCreationException, CertificateException {
     ZonedDateTime start = ZonedDateTime.now().minus(Period.ofDays(1));
@@ -178,7 +191,7 @@ public class CertificateAuthority {
       Date notAfter,
       X500Principal subject,
       X500Principal issuer,
-      @nullable String sanDnsName,
+      String sanDnsName,
       boolean isCA)
       throws IOException, CertificateException, OperatorCreationException {
     SubjectPublicKeyInfo subjectKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
@@ -218,4 +231,13 @@ public class CertificateAuthority {
     X509CertificateHolder holder = builder.build(signer);
     return new JcaX509CertificateConverter().getCertificate(holder);
   }
+
+  public static <T> T[] prepend(T t, T[] original) {
+    @SuppressWarnings("unchecked")
+    T[] newArray =
+        (T[]) Array.newInstance(original.getClass().getComponentType(), original.length + 1);
+    newArray[0] = t;
+    System.arraycopy(original, 0, newArray, 1, original.length);
+    return newArray;
+  }
 }
diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java
similarity index 94%
rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java
rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java
index e7afd16..be52e96 100644
--- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java
+++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java
index ecba478..b63da3a 100644
--- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java
+++ i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020-2025 Thomas Akehurst
+ * Copyright (C) 2020-2026 Thomas Akehurst
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@ package com.github.tomakehurst.wiremock.common;

 import static java.util.Arrays.copyOf;

-import java.lang.reflect.Array;
-
 public final class ArrayFunctions {

   public static <T> T[] concat(T[] first, T[] second) {
@@ -30,15 +28,6 @@ public final class ArrayFunctions {
     return both;
   }

-  public static <T> T[] prepend(T t, T[] original) {
-    @SuppressWarnings("unchecked")
-    T[] newArray =
-        (T[]) Array.newInstance(original.getClass().getComponentType(), original.length + 1);
-    newArray[0] = t;
-    System.arraycopy(original, 0, newArray, 1, original.length);
-    return newArray;
-  }
-
   private ArrayFunctions() {
     throw new UnsupportedOperationException("not instantiable");
   }
diff --git c/wiremock-standalone/build.gradle.kts i/wiremock-standalone/build.gradle.kts
new file mode 100644
index 000000000..19422a1
--- /dev/null
+++ i/wiremock-standalone/build.gradle.kts
@@ -0,0 +1,95 @@
+plugins {
+  id("wiremock.common-conventions")
+}
+
+shadow{
+  addShadowVariantIntoJavaComponent = true
+}
+
+dependencies {
+  implementation(project(":"))
+}
+
+tasks.shadowJar {
+  archiveBaseName = "wiremock-standalone"
+  archiveClassifier = ""
+  configurations = listOf(
+    project.configurations.runtimeClasspath.get(),
+  )
+  manifest {
+    attributes("Main-Class" to "wiremock.Run")
+  }
+  relocate("org.mortbay", "wiremock.org.mortbay")
+  relocate("org.eclipse", "wiremock.org.eclipse")
+  relocate("org.codehaus", "wiremock.org.codehaus")
+  relocate("com.google", "wiremock.com.google")
+  relocate("com.google.thirdparty", "wiremock.com.google.thirdparty")
+  relocate("com.fasterxml.jackson", "wiremock.com.fasterxml.jackson")
+  relocate("org.apache", "wiremock.org.apache")
+  relocate("org.xmlunit", "wiremock.org.xmlunit")
+  relocate("org.hamcrest", "wiremock.org.hamcrest")
+  relocate("org.skyscreamer", "wiremock.org.skyscreamer")
+  relocate("org.json", "wiremock.org.json")
+  relocate("net.minidev", "wiremock.net.minidev")
+  relocate("com.jayway", "wiremock.com.jayway")
+  relocate("org.objectweb", "wiremock.org.objectweb")
+  relocate("org.custommonkey", "wiremock.org.custommonkey")
+  relocate("net.javacrumbs", "wiremock.net.javacrumbs")
+  relocate("net.sf", "wiremock.net.sf")
+  relocate("com.github.jknack", "wiremock.com.github.jknack")
+  relocate("org.antlr", "wiremock.org.antlr")
+  relocate("jakarta.servlet", "wiremock.jakarta.servlet")
+  relocate("org.checkerframework", "wiremock.org.checkerframework")
+  relocate("org.hamcrest", "wiremock.org.hamcrest")
+  relocate("org.slf4j", "wiremock.org.slf4j")
+  relocate("joptsimple", "wiremock.joptsimple")
+  exclude("joptsimple/HelpFormatterMessages.properties")
+  relocate("org.yaml", "wiremock.org.yaml")
+  relocate("com.ethlo", "wiremock.com.ethlo")
+  relocate("com.networknt", "wiremock.com.networknt")
+  relocate("org.jspecify", "wiremock.org.jspecify")
+  relocate("org.bouncycastle", "wiremock.org.bouncycastle")
+
+  dependencies {
+    exclude(dependency("junit:junit"))
+  }
+
+  mergeServiceFiles()
+
+  exclude("META-INF/maven/**")
+  exclude("META-INF/versions/17/**")
+  exclude("META-INF/versions/21/**")
+  exclude("META-INF/versions/22/**")
+  exclude("module-info.class")
+  exclude("handlebars-*.js")
+}
+
+tasks.jar {
+  enabled = false
+}
+
+publishing {
+  publications {
+    create<MavenPublication>("standaloneJar") {
+      artifactId = tasks.shadowJar.get().archiveBaseName.get()
+      from(components.findByName("shadow"))
+      artifact(tasks.sourcesJar)
+
+      pom.packaging = "jar"
+      pom {
+        name = "WireMock"
+        description = "A web service test double for all occasions - standalone edition"
+      }
+    }
+  }
+}
+
+tasks.assemble {
+  dependsOn(tasks.shadowJar)
+}
+
+tasks.publish {
+  dependsOn(
+    "signStandaloneJarPublication",
+  )
+}
diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json
index 51a6165..27aea4d 100644
--- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json
+++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json
@@ -238,5 +238,9 @@
 { "input": "non-special:\\/opaque", "href": "non-special:\\/opaque", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "\\/opaque", "search": "", "hash": "" },
 { "input": "non-special:/\\path", "href": "non-special:/\\path", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "/\\path", "search": "", "hash": "" },
 { "input": "non-special://host\\a", "failure": true },
-{ "input": "non-special://host/a\\b", "href": "non-special://host/a\\b", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/a\\b", "search": "", "hash": "" }
+{ "input": "non-special://host/a\\b", "href": "non-special://host/a\\b", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/a\\b", "search": "", "hash": "" },
+{ "input": "data:text/plain,test#<foo> <bar>", "href": "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%3Cfoo%3E%20%3Cbar%3E" },
+{ "input": "about:blank#<foo> <bar>", "href": "about:blank#%3Cfoo%3E%20%3Cbar%3E", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%3Cfoo%3E%20%3Cbar%3E" },
+{ "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" },
+{ "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" }
 ]
\ No newline at end of file
diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json
index fd2201c..ec13a88 100644
--- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json
+++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json
@@ -10296,5 +10296,65 @@
     "pathname": "/a\\b",
     "search": "",
     "hash": ""
+  },
+  {
+    "comment": "Fragment with <> on data: URI",
+    "input": "data:text/plain,test#<foo> <bar>",
+    "base": null,
+    "href": "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "text/plain,test",
+    "search": "",
+    "hash": "#%3Cfoo%3E%20%3Cbar%3E"
+  },
+  {
+    "comment": "Fragment with <> on about:blank",
+    "input": "about:blank#<foo> <bar>",
+    "base": null,
+    "href": "about:blank#%3Cfoo%3E%20%3Cbar%3E",
+    "protocol": "about:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "blank",
+    "search": "",
+    "hash": "#%3Cfoo%3E%20%3Cbar%3E"
+  },
+  {
+    "comment": "Fragment percent-encode set on data: URI; tabs and newlines are removed",
+    "input":"data:text/plain,test#\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+    "base": null,
+    "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9",
+    "protocol": "data:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "text/plain,test",
+    "search": "",
+    "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9"
+  },
+  {
+    "comment": "Fragment percent-encode set on about:blank; tabs and newlines are removed",
+    "input": "about:blank#\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~\u007f\u0080\u0081Éé",
+    "base": null,
+    "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9",
+    "protocol": "about:",
+    "username": "",
+    "password": "",
+    "host": "",
+    "hostname": "",
+    "port": "",
+    "pathname": "blank",
+    "search": "",
+    "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9"
   }
 ]
diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json
index 7441abd..2264882 100644
--- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json
+++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json
@@ -18,6 +18,8 @@
 { "input": "?x", "base": "sc://ñ", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `sc://ñ`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `ñ`", "source": { "input": "?x", "base": "sc://ñ", "href": "sc://%C3%B1?x", "origin": "null", "protocol": "sc:", "username": "", "password": "", "host": "%C3%B1", "hostname": "%C3%B1", "port": "", "pathname": "", "search": "?x", "hash": "" } },
 { "input": "C|\n/", "base": "file://host/dir/file", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `C|\n/`", "exceptionCauseType": "IllegalPath", "exceptionCauseMessage": "Illegal path: `C|\n/`", "source": { "input": "C|\n/", "base": "file://host/dir/file", "href": "file://host/C:/", "protocol": "file:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/C:/", "search": "", "hash": "" } },
 { "input": "[61:24:74]:98", "base": "http://example.org/foo/bar", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `[61:24:74]:98`", "exceptionCauseType": "IllegalScheme", "exceptionCauseMessage": "Illegal scheme `[61`; Scheme must match [a-zA-Z][a-zA-Z0-9+\\-.]{0,255}", "source": { "input": "[61:24:74]:98", "base": "http://example.org/foo/bar", "href": "http://example.org/foo/[61:24:74]:98", "origin": "http://example.org", "protocol": "http:", "username": "", "password": "", "host": "example.org", "hostname": "example.org", "port": "", "pathname": "/foo/[61:24:74]:98", "search": "", "hash": "" } },
+{ "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "base": null, "exceptionType": "IllegalAbsoluteUrl", "exceptionMessage": "Illegal absolute url: `about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé`", "exceptionCauseType": null, "exceptionCauseMessage": null, "source": { "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } },
+{ "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "base": null, "exceptionType": "IllegalAbsoluteUrl", "exceptionMessage": "Illegal absolute url: `data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé`", "exceptionCauseType": null, "exceptionCauseMessage": null, "source": { "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } },
 { "input": "file://C|/", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://C|/`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `C|`", "source": { "input": "file://C|/", "href": "file:///C:/", "protocol": "file:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "/C:/", "search": "", "hash": "" } },
 { "input": "file://\\/localhost//cat", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://\\/localhost//cat`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `\\`", "source": { "input": "file://\\/localhost//cat", "href": "file:////localhost//cat", "protocol": "file:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "//localhost//cat", "search": "", "hash": "" } },
 { "input": "file://a­b/p", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://a­b/p`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `a­b`", "source": { "input": "file://a­b/p", "href": "file://ab/p", "protocol": "file:", "username": "", "password": "", "host": "ab", "hostname": "ab", "port": "", "pathname": "/p", "search": "", "hash": "" } },
diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json
index c77cdf4..cf14c9e 100644
--- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json
+++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json
@@ -1535,6 +1535,18 @@
     "source" : { "input" : "about:/../", "href" : "about:/", "origin" : "null", "protocol" : "about:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "/", "search" : "", "hash" : "" },
     "matchesWhatWg" : true
   },
+  {
+    "input" : "about:blank#<foo> <bar>",
+    "base" : null,
+    "inputExpected" : { "stringValue" : "about:blank#<foo> <bar>", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "<foo> <bar>" },
+    "inputNormalised" : { "stringValue" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" },
+    "baseExpected" : null,
+    "baseNormalised" : null,
+    "resolved" : { "stringValue" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" },
+    "origin" : null,
+    "source" : { "input" : "about:blank#<foo> <bar>", "href" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "protocol" : "about:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "blank", "search" : "", "hash" : "#%3Cfoo%3E%20%3Cbar%3E" },
+    "matchesWhatWg" : true
+  },
   {
     "input" : "android-app://x:0",
     "base" : null,
@@ -1919,6 +1931,18 @@
     "source" : { "input" : "data:text/html,test#test", "base" : "http://example.org/foo/bar", "href" : "data:text/html,test#test", "origin" : "null", "protocol" : "data:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "text/html,test", "search" : "", "hash" : "#test" },
     "matchesWhatWg" : true
   },
+  {
+    "input" : "data:text/plain,test#<foo> <bar>",
+    "base" : null,
+    "inputExpected" : { "stringValue" : "data:text/plain,test#<foo> <bar>", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "<foo> <bar>" },
+    "inputNormalised" : { "stringValue" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" },
+    "baseExpected" : null,
+    "baseNormalised" : null,
+    "resolved" : { "stringValue" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" },
+    "origin" : null,
+    "source" : { "input" : "data:text/plain,test#<foo> <bar>", "href" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "protocol" : "data:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "text/plain,test", "search" : "", "hash" : "#%3Cfoo%3E%20%3Cbar%3E" },
+    "matchesWhatWg" : true
+  },
   {
     "input" : "dns://fw.example.org:9999/foo.bar.org?type=TXT",
     "base" : null,
@Malandril Malandril force-pushed the feature/switch_to_bouncycastle branch from bb8a369 to 5ea7e22 Compare April 22, 2026 19:19
@tomakehurst tomakehurst merged commit 1ae1d8c into wiremock:master Apr 23, 2026
5 checks passed
@tomakehurst

Copy link
Copy Markdown
Member

Thank you for your patience seeing this through!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Main in the middle certificate generation does not work with Java 25 Move certificate management from private sun classes to bouncy castle

3 participants