This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 4c1f613b0b Replace OpenSSL OCSP responder with an in-process 
BouncyCastle solution
4c1f613b0b is described below

commit 4c1f613b0bd2ff67f4fefa8609f036340f14ba4e
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Mar 6 08:59:01 2026 +0000

    Replace OpenSSL OCSP responder with an in-process BouncyCastle solution
---
 build.properties.default                           |  20 ++
 build.xml                                          |  33 +++
 res/ide-support/eclipse/eclipse.classpath          |   3 +
 res/ide-support/idea/tomcat.iml                    |  27 +++
 .../netbeans/nb-tomcat-build.properties            |   2 +-
 res/ide-support/netbeans/project.xml               |   2 +-
 .../tomcat/security/TestSecurity2017Ocsp.java      |   4 +-
 .../tomcat/util/net/ocsp/TestOcspEnabled.java      |   5 +-
 .../tomcat/util/net/ocsp/TesterOcspResponder.java  | 139 +++++-------
 .../util/net/ocsp/TesterOcspResponderServlet.java  | 238 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   4 +
 11 files changed, 385 insertions(+), 92 deletions(-)

diff --git a/build.properties.default b/build.properties.default
index ee293d5600..d1be9797a7 100644
--- a/build.properties.default
+++ b/build.properties.default
@@ -349,3 +349,23 @@ 
derby-shared.jar=${derby.home}/derby-shared-${derby.version}.jar
 
derby-shared.loc=${base-maven.loc}/org/apache/derby/derbyshared/${derby.version}/derbyshared-${derby.version}.jar
 derby-tools.jar=${derby.home}/derby-tools-${derby.version}.jar
 
derby-tools.loc=${base-maven.loc}/org/apache/derby/derbytools/${derby.version}/derbytools-${derby.version}.jar
+
+# ----- Bouncy Castle, used by unit tests ----
+bouncycastle.version=1.83
+bouncycastle-provider.checksum.enabled=true
+bouncycastle-provider.checksum.algorithm=SHA-512
+bouncycastle-provider.checksum.value=e51cc843ca130ad4a15ff667360063bbb583af3f22e14193840a734da6665b470ba1855ce975f88a94ddd6419aa020d3a9966980bc1deb7514f92a7215d6e229
+bouncycastle-pkix.checksum.enabled=true
+bouncycastle-pkix.checksum.algorithm=SHA-512
+bouncycastle-pkix.checksum.value=9c67d990a56a5c448f9bb9edbb8b99dc15971e16de7e6f10c3eab129a1389eef4882c5a5d910ac4ddc27d44f7bc9fa4054c7f56dc031154c131bccc50ebd67b9
+bouncycastle-util.checksum.enabled=true
+bouncycastle-util.checksum.algorithm=SHA-512
+bouncycastle-util.checksum.value=e19831d4afc0a709fd57694f33bfe3a8e881cba287c34fb076a44ef436e56e6bdf49299ca9525028a4de9bf5fadeaa254654d0e527855f1f5659c5a7be538576
+
+bouncycastle.home=${base.path}/bouncycastle-${bouncycastle.version}
+bouncycastle-provider.jar=${bouncycastle.home}/bouncycastle-provider-${bouncycastle.version}.jar
+bouncycastle-provider.loc=${base-maven.loc}/org/bouncycastle/bcprov-jdk18on/${bouncycastle.version}/bcprov-jdk18on-${bouncycastle.version}.jar
+bouncycastle-pkix.jar=${bouncycastle.home}/bouncycastle-pkix-${bouncycastle.version}.jar
+bouncycastle-pkix.loc=${base-maven.loc}/org/bouncycastle/bcpkix-jdk18on/${bouncycastle.version}/bcpkix-jdk18on-${bouncycastle.version}.jar
+bouncycastle-util.jar=${bouncycastle.home}/bouncycastle-util-${bouncycastle.version}.jar
+bouncycastle-util.loc=${base-maven.loc}/org/bouncycastle/bcutil-jdk18on/${bouncycastle.version}/bcutil-jdk18on-${bouncycastle.version}.jar
diff --git a/build.xml b/build.xml
index 31fe59cb9a..6c26f11dc9 100644
--- a/build.xml
+++ b/build.xml
@@ -257,6 +257,9 @@
     <pathelement location="${derby.jar}"/>
     <pathelement location="${derby-shared.jar}"/>
     <pathelement location="${derby-tools.jar}"/>
+    <pathelement location="${bouncycastle-provider.jar}"/>
+    <pathelement location="${bouncycastle-pkix.jar}"/>
+    <pathelement location="${bouncycastle-util.jar}"/>
     <path refid="compile.classpath" />
     <path refid="tomcat.classpath" />
   </path>
@@ -273,6 +276,9 @@
     <filter token="MIGRATION_JAR" value="${migration-lib.jar}"/>
     <filter token="UNBOUNDID_JAR" value="${unboundid.jar}"/>
     <filter token="JUNIT_JAR" value="${junit.jar}"/>
+    <filter token="BC_PROVIDER_JAR" value="${bouncycastle-provider.jar}"/>
+    <filter token="BC_PKIX_JAR" value="${bouncycastle-pkix.jar}"/>
+    <filter token="BC_UTIL_JAR" value="${bouncycastle-util.jar}"/>
     <filter token="OUTPUT_DIR" value="${tomcat.output}"/>
   </filterset>
 
@@ -3753,6 +3759,33 @@ Configured for ${release.asfusername} to release Tomcat 
${version.major}.${versi
       <param name="checksum.value" value="${derby-tools.checksum.value}"/>
     </antcall>
 
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${bouncycastle-provider.loc}"/>
+      <param name="destfile" value="${bouncycastle-provider.jar}"/>
+      <param name="destdir" value="${bouncycastle.home}"/>
+      <param name="checksum.enabled" 
value="${bouncycastle-provider.checksum.enabled}"/>
+      <param name="checksum.algorithm" 
value="${bouncycastle-provider.checksum.algorithm}"/>
+      <param name="checksum.value" 
value="${bouncycastle-provider.checksum.value}"/>
+    </antcall>
+
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${bouncycastle-pkix.loc}"/>
+      <param name="destfile" value="${bouncycastle-pkix.jar}"/>
+      <param name="destdir" value="${bouncycastle.home}"/>
+      <param name="checksum.enabled" 
value="${bouncycastle-pkix.checksum.enabled}"/>
+      <param name="checksum.algorithm" 
value="${bouncycastle-pkix.checksum.algorithm}"/>
+      <param name="checksum.value" 
value="${bouncycastle-pkix.checksum.value}"/>
+    </antcall>
+
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${bouncycastle-util.loc}"/>
+      <param name="destfile" value="${bouncycastle-util.jar}"/>
+      <param name="destdir" value="${bouncycastle.home}"/>
+      <param name="checksum.enabled" 
value="${bouncycastle-util.checksum.enabled}"/>
+      <param name="checksum.algorithm" 
value="${bouncycastle-util.checksum.algorithm}"/>
+      <param name="checksum.value" 
value="${bouncycastle-util.checksum.value}"/>
+    </antcall>
+
   </target>
 
   <target name="download-jacoco"
diff --git a/res/ide-support/eclipse/eclipse.classpath 
b/res/ide-support/eclipse/eclipse.classpath
index f4059ab458..fbba3531e2 100644
--- a/res/ide-support/eclipse/eclipse.classpath
+++ b/res/ide-support/eclipse/eclipse.classpath
@@ -38,5 +38,8 @@
     <classpathentry kind="lib" path="@BND_JAR@"/>
     <classpathentry kind="lib" path="@MIGRATION_JAR@"/>
     <classpathentry kind="lib" path="@UNBOUNDID_JAR@"/>
+    <classpathentry kind="lib" path="@BC_PROVIDER_JAR@"/>
+    <classpathentry kind="lib" path="@BC_PKIX_JAR@"/>
+    <classpathentry kind="lib" path="@BC_UTIL_JAR@"/>
     <classpathentry kind="output" path=".settings/output"/>
 </classpath>
diff --git a/res/ide-support/idea/tomcat.iml b/res/ide-support/idea/tomcat.iml
index 354b60eeea..419b561a2f 100644
--- a/res/ide-support/idea/tomcat.iml
+++ b/res/ide-support/idea/tomcat.iml
@@ -126,6 +126,33 @@
         <SOURCES />
       </library>
     </orderEntry>
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://@BC_PROVIDER_JAR@!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://@BC_PKIX_JAR@!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
+      <library>
+        <CLASSES>
+          <root url="jar://@BC_UTIL_JAR@!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES />
+      </library>
+    </orderEntry>
 
     <orderEntry type="module-library">
       <library>
diff --git a/res/ide-support/netbeans/nb-tomcat-build.properties 
b/res/ide-support/netbeans/nb-tomcat-build.properties
index 0f9c96d049..a97609bf18 100644
--- a/res/ide-support/netbeans/nb-tomcat-build.properties
+++ b/res/ide-support/netbeans/nb-tomcat-build.properties
@@ -37,7 +37,7 @@ nb-test.io-method=org.apache.coyote.http11.Http11NioProtocol
 # it is not possible to retrieve the classpaths from the build to
 # use in the NetBeans targets, so they must be explicitly declared
 
-nb-test.classpath=${test.classes}:${tomcat.build}/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@ECJ_JAR@:@UNBOUNDID_JAR@:${tomcat.classes}
+nb-test.classpath=${test.classes}:${tomcat.build}/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@ECJ_JAR@:@UNBOUNDID_JAR@:@BC_PROVIDER_JAR@:@BC_PKIX_JAR@:@BC_UTIL_JAR@:${tomcat.classes}
 
 # Extra properties used by the Tomcat project additional NetBeans targets.
 
diff --git a/res/ide-support/netbeans/project.xml 
b/res/ide-support/netbeans/project.xml
index 42a3aa6a3c..8315b50bf0 100644
--- a/res/ide-support/netbeans/project.xml
+++ b/res/ide-support/netbeans/project.xml
@@ -189,7 +189,7 @@
             <compilation-unit>
                 <package-root>test</package-root>
                 <unit-tests/>
-                <classpath 
mode="compile">output/classes:output/testclasses:output/build/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@UNBOUNDID_JAR@</classpath>
+                <classpath 
mode="compile">output/classes:output/testclasses:output/build/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@UNBOUNDID_JAR@:@BC_PROVIDER_JAR@:@BC_PKIX_JAR@:@BC_UTIL_JAR@</classpath>
                 <source-level>@BUILD_JAVA_VERSION@</source-level>
             </compilation-unit>
         </java-data>
diff --git a/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java 
b/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java
index e6cb01e0ae..570d7510ff 100644
--- a/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java
+++ b/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java
@@ -16,8 +16,6 @@
  */
 package org.apache.tomcat.security;
 
-import java.io.IOException;
-
 import javax.net.ssl.SSLHandshakeException;
 
 import jakarta.servlet.http.HttpServletResponse;
@@ -50,7 +48,7 @@ public class TestSecurity2017Ocsp extends OcspBaseTest {
         ocspResponder = new TesterOcspResponder();
         try {
             ocspResponder.start();
-        } catch (IOException ioe) {
+        } catch (Exception e) {
             ocspResponder = null;
         }
     }
diff --git a/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 
b/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java
index e47d06eb38..df96e97e0a 100644
--- a/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java
+++ b/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java
@@ -16,7 +16,6 @@
  */
 package org.apache.tomcat.util.net.ocsp;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -43,8 +42,8 @@ public class TestOcspEnabled extends OcspBaseTest {
         ocspResponder = new TesterOcspResponder();
         try {
             ocspResponder.start();
-        } catch (IOException ioe) {
-            ocspResponder = null;
+        } catch (Exception e) {
+            e.printStackTrace();
         }
     }
 
diff --git a/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 
b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java
index c6aa974b2c..ac069c3052 100644
--- a/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java
+++ b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java
@@ -16,108 +16,79 @@
  */
 package org.apache.tomcat.util.net.ocsp;
 
-import java.io.IOException;
-import java.io.PrintStream;
-import java.io.Reader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
+import java.io.File;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
-import org.junit.Assert;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.ExpandWar;
+import org.apache.catalina.startup.Tomcat;
 
-import org.apache.tomcat.util.net.TesterSupport;
-
-/*
- * The OpenSSL ocsp tool is great, but it does generate a lot of output. That 
output needs to be swallowed, else the
- * process will freeze when the output buffers (stdout and stderr) are full.
- *
- * There is a command line option to redirect stdout (which could be 
redirected to /dev/null), but there is no option to
- * redirect stderr. Therefore, this class uses a couple of dedicated threads 
to read stdout and stderr. By default, the
- * output is ignored, but it can be dumped to Java's stdout/stderr if required 
for debugging purposes.
- */
 public class TesterOcspResponder {
 
-    private static List<String> ocspArgs = Arrays.asList("ocsp", "-port", 
"8888", "-text", "-index",
-            TesterSupport.DB_INDEX, "-CA", TesterSupport.CA_CERT_PEM, "-rkey", 
TesterSupport.OCSP_RESPONDER_RSA_KEY,
-            "-rsigner", TesterSupport.OCSP_RESPONDER_RSA_CERT, "-nmin", "60");
+    private File catalinaBase;
+    private Tomcat ocspResponder;
 
-    private Process p;
+    public void start() throws Exception {
+        ocspResponder = new Tomcat();
 
-    public void start() throws IOException {
-        if (p != null) {
-            throw new IllegalStateException("Already started");
-        }
+        Connector connector = new Connector("HTTP/1.1");
+        connector.setPort(8888);
+        connector.setThrowOnFailure(true);
+        connector.setEncodedSolidusHandling("passthrough");
+        ocspResponder.getService().addConnector(connector);
 
-        String openSSLPath = System.getProperty("tomcat.test.openssl.path");
-        String openSSLLibPath = null;
-        if (openSSLPath == null || openSSLPath.length() == 0) {
-            openSSLPath = "openssl";
-        } else {
-            // Explicit OpenSSL path may also need explicit lib path
-            // (e.g. Gump needs this)
-            openSSLLibPath = openSSLPath.substring(0, 
openSSLPath.lastIndexOf('/'));
-            openSSLLibPath = openSSLLibPath + "/../:" + openSSLLibPath + 
"/../lib:" + openSSLLibPath + "/../lib64";
+        // Create a temporary directory structure for the OCSP responder
+        File tempBase = new File(System.getProperty("tomcat.test.temp", 
"output/tmp"));
+        if (!tempBase.mkdirs() && !tempBase.isDirectory()) {
+            throw new IllegalStateException("Unable to create tempBase");
         }
-        List<String> cmd = new ArrayList<>();
-        cmd.add(openSSLPath);
-        cmd.addAll(ocspArgs);
 
-        ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0]));
-
-        if (openSSLLibPath != null) {
-            Map<String,String> env = pb.environment();
-            String libraryPath = env.get("LD_LIBRARY_PATH");
-            if (libraryPath == null) {
-                libraryPath = openSSLLibPath;
-            } else {
-                libraryPath = libraryPath + ":" + openSSLLibPath;
-            }
-            env.put("LD_LIBRARY_PATH", libraryPath);
+        // Create and configure CATALINA_BASE
+        Path tempBasePath = 
FileSystems.getDefault().getPath(tempBase.getAbsolutePath());
+        catalinaBase = Files.createTempDirectory(tempBasePath, 
"ocsp").toFile();
+        if (!catalinaBase.isDirectory()) {
+            throw new IllegalStateException("Unable to create CATALINA_BASE 
for OCSP responder");
         }
+        ocspResponder.setBaseDir(catalinaBase.getAbsolutePath());
 
-        p = pb.start();
+        // Create and configure a web apps directory
+        File appBase = new File(catalinaBase, "webapps");
+        if (!appBase.exists() && !appBase.mkdir()) {
+            throw new IllegalStateException("Unable to create appBase for OCSP 
responder");
+        }
+        ocspResponder.getHost().setAppBase(appBase.getAbsolutePath());
 
-        redirect(p.inputReader(), System.out, true);
-        redirect(p.errorReader(), System.err, true);
+        // Configure the ROOT web application
+        // No file system docBase required
+        Context ctx = ocspResponder.addContext("", null);
+        Tomcat.addServlet(ctx, "responder", new TesterOcspResponderServlet());
+        ctx.addServletMappingDecoded("/", "responder");
 
-        Assert.assertTrue(p.isAlive());
+        // Start the responder
+        ocspResponder.start();
     }
 
     public void stop() {
-        if (p == null) {
-            throw new IllegalStateException("Not started");
-        }
-        p.destroy();
-
-        try {
-            if (!p.waitFor(30, TimeUnit.SECONDS)) {
-                throw new IllegalStateException("Failed to stop");
+        if (ocspResponder != null) {
+            try {
+                ocspResponder.stop();
+            } catch (LifecycleException e) {
+                // Good enough for testing
+                e.printStackTrace();
             }
-        } catch (InterruptedException e) {
-            throw new IllegalStateException("Interrupted while waiting to 
stop", e);
-        }
-    }
-
-
-    private void redirect(final Reader r, final PrintStream os, final boolean 
swallow) {
-        /*
-         * InputStream will close when process ends. Thread will exit once 
stream closes.
-         */
-        new Thread( () -> {
-            char[] cbuf = new char[1024];
             try {
-                int read;
-                while ((read = r.read(cbuf)) > 0) {
-                    if (!swallow) {
-                        os.print(new String(cbuf, 0, read));
-                    }
-                }
-            } catch (IOException ignore) {
-                // Ignore
+                ocspResponder.destroy();
+            } catch (LifecycleException e) {
+                // Good enough for testing
+                e.printStackTrace();
             }
-
-        }).start();
+        }
+        if (catalinaBase != null) {
+            ExpandWar.deleteDir(catalinaBase);
+        }
     }
 }
diff --git 
a/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java 
b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java
new file mode 100644
index 0000000000..a61de5210b
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.net.ocsp;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.Security;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Date;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.util.IOTools;
+import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream;
+import org.apache.tomcat.util.net.TesterSupport;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.CertificateID;
+import org.bouncycastle.cert.ocsp.CertificateStatus;
+import org.bouncycastle.cert.ocsp.OCSPException;
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.Req;
+import org.bouncycastle.cert.ocsp.RespID;
+import org.bouncycastle.cert.ocsp.RevokedStatus;
+import org.bouncycastle.cert.ocsp.UnknownStatus;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
+import org.bouncycastle.operator.DigestCalculatorProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+/*
+ * Based on https://github.com/wdawson/revoker - ALv2 licensed
+ */
+public class TesterOcspResponderServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    private DigestCalculatorProvider digestCalculatorProvider;
+    private X509CertificateHolder[] responderCertificateChain;
+    private RespID responderID;
+    private ContentSigner contentSigner;
+
+
+    @Override
+    public void init(ServletConfig config) throws ServletException {
+        // Enable the Bouncy Castle Provider
+        Provider provider = new BouncyCastleProvider();
+        Security.addProvider(provider);
+
+        // Create the digest provider
+        try {
+            this.digestCalculatorProvider = new 
JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
+        } catch (OperatorCreationException e) {
+            throw new ServletException(e);
+        }
+
+        // Parse the OCSP responder cert
+        X509Certificate responderCert;
+        try (PEMParser pemParser = new PEMParser(new 
FileReader(TesterSupport.OCSP_RESPONDER_RSA_CERT))) {
+            JcaX509CertificateConverter x509Converter = new 
JcaX509CertificateConverter().setProvider(provider);
+            responderCert = 
x509Converter.getCertificate((X509CertificateHolder) pemParser.readObject());
+        } catch (IOException | CertificateException e) {
+            throw new ServletException(e);
+        }
+
+        // Parse the OCSP responder issuer certificate
+        X509Certificate issuerCert;
+        try (PEMParser pemParser = new PEMParser(new 
FileReader(TesterSupport.CA_CERT_PEM))) {
+            JcaX509CertificateConverter x509Converter = new 
JcaX509CertificateConverter().setProvider(provider);
+            issuerCert = x509Converter.getCertificate((X509CertificateHolder) 
pemParser.readObject());
+        } catch (IOException | CertificateException e) {
+            throw new ServletException(e);
+        }
+
+        // Create the responder certificate chain
+        try {
+            responderCertificateChain = new X509CertificateHolder[] { new 
JcaX509CertificateHolder(responderCert),
+                    new JcaX509CertificateHolder(issuerCert) };
+        } catch (CertificateEncodingException e) {
+            throw new ServletException(e);
+        }
+
+        // Create the responder ID
+        SubjectPublicKeyInfo publicKeyInfo =
+                
SubjectPublicKeyInfo.getInstance(responderCert.getPublicKey().getEncoded());
+
+        try {
+            // Only SHA-1 supported
+            responderID = new RespID(publicKeyInfo,
+                    digestCalculatorProvider.get(new 
DefaultDigestAlgorithmIdentifierFinder().find("SHA-1")));
+        } catch (OperatorCreationException | OCSPException e) {
+            throw new ServletException(e);
+        }
+
+        // Parse the private key
+        PrivateKey responderKey;
+        try (PEMParser pemParser = new PEMParser(new 
FileReader(TesterSupport.OCSP_RESPONDER_RSA_KEY))) {
+            PrivateKeyInfo privateKeyInfo = 
PrivateKeyInfo.getInstance(pemParser.readObject());
+            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
+            responderKey = converter.getPrivateKey(privateKeyInfo);
+        } catch (IOException e) {
+            throw new ServletException(e);
+        }
+
+        // Create the content signer
+        try {
+            contentSigner = new 
JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(responderKey);
+        } catch (OperatorCreationException e) {
+            throw new ServletException(e);
+        }
+    }
+
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+
+        // The request is base64 encoded and passed as the path (less the 
leading '/')
+        String urlEncoded = req.getRequestURI().substring(1);
+
+        // Handle longer URI used for TestSecurity2017Ocsp
+        if (urlEncoded.startsWith("xxxxxxxx")) {
+            urlEncoded = urlEncoded.substring(urlEncoded.indexOf("/") + 1);
+        }
+        String base64 = URLDecoder.decode(urlEncoded, 
StandardCharsets.US_ASCII);
+        byte[] derEncodeOCSPRequest = Base64.getDecoder().decode(base64);
+
+        // Process the OCSP request
+        OCSPResp ocspResponse = processOscpRequest(derEncodeOCSPRequest);
+
+        // Write the OCSP response
+        ServletOutputStream sos = resp.getOutputStream();
+        sos.write(ocspResponse.getEncoded());
+    }
+
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        // The request is passed in the request body
+
+        // Determine request content length (or start with a reasonable 
default)
+        int contentLength = req.getContentLength();
+        if (contentLength == -1) {
+            // OCSP requests are small. 1k should be plenty and it can expand 
if necessary.
+            contentLength = 1024;
+        }
+
+        // Read the body into a byte array
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(contentLength);
+        IOTools.flow(req.getInputStream(), baos);
+
+        // Process the OCSP request
+        OCSPResp ocspResponse = processOscpRequest(baos.toByteArray());
+
+        // Write the OCSP response
+        ServletOutputStream sos = resp.getOutputStream();
+        sos.write(ocspResponse.getEncoded());
+    }
+
+
+    private OCSPResp processOscpRequest(byte[] derEncodeOCSPRequest) throws 
ServletException, IOException {
+
+        OCSPReq ocspReq = new OCSPReq(derEncodeOCSPRequest);
+
+        // For the tests as currently written it is safe to assume the request 
is valid
+
+        // Set the responses for each certificate
+        BasicOCSPRespBuilder responseBuilder = new 
BasicOCSPRespBuilder(responderID);
+        Req[] requests = ocspReq.getRequestList();
+        for (Req request : requests) {
+            CertificateID certificateID = request.getCertID();
+            switch (certificateID.getSerialNumber().intValue()) {
+                // TODO read index.db rather than hard-code certificate serial 
numbers
+                case 4096:
+                case 4098:
+                case 4100:
+                case 4101:
+                    responseBuilder.addResponse(certificateID, 
CertificateStatus.GOOD);
+                    break;
+                case 4097:
+                case 4099:
+                case 4102:
+                    responseBuilder.addResponse(certificateID, new 
RevokedStatus(new Date(0)));
+                    break;
+                default:
+                    responseBuilder.addResponse(certificateID, new 
UnknownStatus());
+            }
+        }
+
+        // Build and sign the response
+        OCSPResp ocspResponse;
+        try {
+            BasicOCSPResp basicResponse = responseBuilder.build(contentSigner, 
responderCertificateChain, new Date());
+            ocspResponse = new 
OCSPRespBuilder().build(OCSPRespBuilder.SUCCESSFUL, basicResponse);
+        } catch (OCSPException e) {
+            throw new ServletException(e);
+        }
+
+        return ocspResponse;
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index b1e79bfa6d..c905cdcb7d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -284,6 +284,10 @@
         <code>Integer</code> rather than a <code>Long</code> to be consistent
         with how port is exposed in the Servlet API. (markt)
       </fix>
+      <scode>
+        Replace the external OpenSSL based OCSP responder used during unit 
tests
+        with a Bouncy Castle based, in-process Java OCSP responder. (markt)
+      </scode>
     </changelog>
   </subsection>
   <subsection name="Jasper">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to