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

kezhuw pushed a commit to branch branch-3.9
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/branch-3.9 by this push:
     new e907bac20 ZOOKEEPER-4955: Fix intererence with jvm wide ssl properties 
for ssl.crl and ssl.ocsp (#2339)
e907bac20 is described below

commit e907bac201fe0b7f1793cc514d9d5e3ae1cdba91
Author: Kezhu Wang <[email protected]>
AuthorDate: Mon Dec 15 08:41:17 2025 +0800

    ZOOKEEPER-4955: Fix intererence with jvm wide ssl properties for ssl.crl 
and ssl.ocsp (#2339)
    
    This is a cherry-pick commit from
    770804bef861bbfc9e150b63774f8557f1f8d995 and
    45b49a5aa9d7f2f699619eb9f486e2b89bdc19d7.
---
 .../src/main/resources/markdown/zookeeperAdmin.md  |   4 +
 .../java/org/apache/zookeeper/common/X509Util.java |  38 ++-
 .../server/auth/X509AuthenticationProvider.java    |   4 +-
 .../apache/zookeeper/common/X509TestContext.java   |   4 +-
 .../apache/zookeeper/common/X509TestHelpers.java   |  42 +++-
 .../org/apache/zookeeper/common/X509UtilTest.java  |  49 ----
 .../java/org/apache/zookeeper/common/ssl/Ca.java   | 193 ++++++++++++++
 .../java/org/apache/zookeeper/common/ssl/Cert.java |  91 +++++++
 .../apache/zookeeper/common/ssl/CertSigner.java    | 174 +++++++++++++
 .../apache/zookeeper/common/ssl/OCSPHandler.java   | 128 ++++++++++
 .../org/apache/zookeeper/common/ssl/PemFile.java   |  31 +++
 .../zookeeper/common/ssl/X509CertBuilder.java      |  26 ++
 .../zookeeper/server/ClientSSLRevocationTest.java  | 278 +++++++++++++++++++++
 .../zookeeper/server/quorum/QuorumSSLTest.java     |   1 +
 .../java/org/apache/zookeeper/test/ClientBase.java |  17 +-
 15 files changed, 1019 insertions(+), 61 deletions(-)

diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md 
b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
index c90de4f30..7b104257f 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
@@ -1779,6 +1779,10 @@ and [SASL authentication for 
ZooKeeper](https://cwiki.apache.org/confluence/disp
     (Java system properties: **zookeeper.ssl.ocsp** and 
**zookeeper.ssl.quorum.ocsp**)
     **New in 3.5.5:**
     Specifies whether Online Certificate Status Protocol is enabled in client 
and quorum TLS protocols.
+    **Changed in next feature version:**
+    Currently, *ssl.ocsp* and *ssl.quorum.ocsp* implies *ssl.crl* and 
*ssl.quorum.crl* correspondingly.
+    In next feature release, one has to setup both *ssl.crl* and *ssl.ocsp* 
(or *ssl.quorum.crl* and *ssl.quorum.ocsp*)
+    to enable OCSP. This is consistent with jdk's method of [Setting up a Java 
Client to use Client-Driven 
OCSP](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/ocsp.html#setting-up-a-java-client-to-use-client-driven-ocsp).
     Default: false
 
 * *ssl.clientAuth* and *ssl.quorum.clientAuth* :
diff --git 
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java 
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index 4cc54d605..9cfa79bc1 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -31,12 +31,16 @@
 import java.security.KeyStore;
 import java.security.NoSuchAlgorithmException;
 import java.security.Security;
+import java.security.cert.CertPathValidator;
 import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXRevocationChecker;
 import java.security.cert.X509CertSelector;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -598,12 +602,36 @@ public static X509TrustManager createTrustManager(
             KeyStore ts = loadTrustStore(trustStoreLocation, 
trustStorePassword, trustStoreTypeProp);
             PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new 
X509CertSelector());
             if (crlEnabled || ocspEnabled) {
-                pbParams.setRevocationEnabled(true);
-                System.setProperty("com.sun.net.ssl.checkRevocation", "true");
-                System.setProperty("com.sun.security.enableCRLDP", "true");
-                if (ocspEnabled) {
-                    Security.setProperty("ocsp.enable", "true");
+                // See [RevocationChecker][1] for details. Basically, we are 
mimicking the legacy path,
+                // which relies significantly on jvm wide properties[2], as 
that is the path we are routing
+                // before (i.e. no explicit `PKIXRevocationChecker`).
+                //
+                // By reading but not writing these properties, we conform to 
but not interfere with what
+                // admin set while still keep backward compatibility.
+                // 1. Default "zookeeper.ssl.crl" to jvm property 
"com.sun.net.ssl.checkRevocation" if it is unset in upcoming feature version.
+                // 2. Default "zookeeper.ssl.ocsp" to jvm security property 
"ocsp.enable" if it is unset in upcoming feature version.
+                // 3. Set `Option.ONLY_END_ENTITY` for jvm security property 
"com.sun.security.onlyCheckRevocationOfEECert".
+                // 4. Don't set "com.sun.security.enableCRLDP" as it is always 
enabled in no legacy path.
+                //
+                // See also:
+                // * 
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/ocsp.html
+                // * 
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html
+                // * 
https://docs.oracle.com/javase/8/docs/technotes/guides/security/certpath/CertPathProgGuide.html#PKIXRevocationChecker
+                //
+                // [1]: 
https://github.com/openjdk/jdk/blob/jdk-11%2B28/src/java.base/share/classes/sun/security/provider/certpath/RevocationChecker.java#L124
+                // [2]: 
https://github.com/openjdk/jdk/blob/jdk-11%2B28/src/java.base/share/classes/sun/security/provider/certpath/RevocationChecker.java#L179
+                Set<PKIXRevocationChecker.Option> options = new HashSet<>();
+                if (!ocspEnabled) {
+                    options.add(PKIXRevocationChecker.Option.NO_FALLBACK);
+                    options.add(PKIXRevocationChecker.Option.PREFER_CRLS);
                 }
+                if 
(Boolean.parseBoolean(Security.getProperty("com.sun.security.onlyCheckRevocationOfEECert")))
  {
+                    options.add(PKIXRevocationChecker.Option.ONLY_END_ENTITY);
+                }
+
+                PKIXRevocationChecker revocationChecker = 
(PKIXRevocationChecker) 
CertPathValidator.getInstance("PKIX").getRevocationChecker();
+                revocationChecker.setOptions(options);
+                pbParams.addCertPathChecker(revocationChecker);
             } else {
                 pbParams.setRevocationEnabled(false);
             }
diff --git 
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
 
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
index 3fcb28978..24f166e6c 100644
--- 
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
+++ 
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
@@ -85,8 +85,8 @@ public X509AuthenticationProvider() throws X509Exception {
                     x509Util.getSslKeystorePasswdPathProperty());
             String keyStoreTypeProp = 
config.getProperty(x509Util.getSslKeystoreTypeProperty());
 
-            boolean crlEnabled = 
Boolean.parseBoolean(config.getProperty(x509Util.getSslCrlEnabledProperty()));
-            boolean ocspEnabled = 
Boolean.parseBoolean(config.getProperty(x509Util.getSslOcspEnabledProperty()));
+            boolean crlEnabled = 
config.getBoolean(x509Util.getSslCrlEnabledProperty());
+            boolean ocspEnabled = 
config.getBoolean(x509Util.getSslOcspEnabledProperty());
             boolean hostnameVerificationEnabled = 
Boolean.parseBoolean(config.getProperty(x509Util.getSslHostnameVerificationEnabledProperty()));
             boolean clientHostnameVerificationEnabled = 
x509Util.isClientHostnameVerificationEnabled(config);
             boolean allowReverseDnsLookup = 
Boolean.parseBoolean(config.getProperty(x509Util.getSslAllowReverseDnsLookupProperty()));
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
index f672bf468..2bb759e63 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
@@ -80,7 +80,7 @@ public class X509TestContext {
      * @throws GeneralSecurityException
      * @throws OperatorCreationException
      */
-    private X509TestContext(File tempDir, KeyPair trustStoreKeyPair, long 
trustStoreCertExpirationMillis, String trustStorePassword, KeyPair 
keyStoreKeyPair, long keyStoreCertExpirationMillis, String keyStorePassword, 
Boolean hostnameVerification) throws IOException, GeneralSecurityException, 
OperatorCreationException {
+    private X509TestContext(File tempDir, KeyPair trustStoreKeyPair, long 
trustStoreCertExpirationMillis, String trustStorePassword, KeyPair 
keyStoreKeyPair, long keyStoreCertExpirationMillis, String keyStorePassword, 
Boolean hostnameVerification) throws Exception {
         if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
             throw new IllegalStateException("BC Security provider was not 
found");
         }
@@ -425,7 +425,7 @@ public Builder() {
          * @throws GeneralSecurityException
          * @throws OperatorCreationException
          */
-        public X509TestContext build() throws IOException, 
GeneralSecurityException, OperatorCreationException {
+        public X509TestContext build() throws Exception {
             KeyPair trustStoreKeyPair = 
X509TestHelpers.generateKeyPair(trustStoreKeyType);
             KeyPair keyStoreKeyPair = 
X509TestHelpers.generateKeyPair(keyStoreKeyType);
             return new X509TestContext(tempDir, trustStoreKeyPair, 
trustStoreCertExpirationMillis, trustStorePassword, keyStoreKeyPair, 
keyStoreCertExpirationMillis, keyStorePassword, hostnameVerification);
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
index 067bf26bc..761c718b8 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java
@@ -36,11 +36,14 @@
 import java.security.cert.X509Certificate;
 import java.security.spec.ECGenParameterSpec;
 import java.security.spec.RSAKeyGenParameterSpec;
+import java.time.Duration;
 import java.util.Date;
 import org.bouncycastle.asn1.DERIA5String;
 import org.bouncycastle.asn1.DEROctetString;
 import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
 import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
 import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
 import org.bouncycastle.asn1.x509.BasicConstraints;
 import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
@@ -53,6 +56,7 @@
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.X509v3CertificateBuilder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
 import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
 import org.bouncycastle.crypto.util.PrivateKeyFactory;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -85,6 +89,27 @@ public class X509TestHelpers {
     // Per RFC 5280 section 4.1.2.2, X509 certificates can use up to 20 bytes 
== 160 bits for serial numbers.
     private static final int SERIAL_NUMBER_MAX_BITS = 20 * Byte.SIZE;
 
+    public static X509Certificate newSelfSignedCert(String name, KeyPair 
keyPair) throws IOException, OperatorCreationException, 
GeneralSecurityException {
+        X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+        caNameBuilder.addRDN(BCStyle.CN, name);
+        return newSelfSignedCACert(caNameBuilder.build(), keyPair, 
Duration.ofDays(1).toMillis());
+    }
+
+    @FunctionalInterface
+    public interface CertificateCustomization {
+        void customize(X509v3CertificateBuilder builder) throws Exception;
+    }
+
+    public static X509Certificate newCert(X509Certificate caCert, KeyPair 
caKeyPair, String name, PublicKey certPublicKey, CertificateCustomization 
customization) throws Exception {
+        X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+        nameBuilder.addRDN(BCStyle.CN, name);
+        return newCert(caCert, caKeyPair, nameBuilder.build(), certPublicKey, 
Duration.ofDays(1).toMillis(), customization);
+    }
+
+    public static X509Certificate newCert(X509Certificate caCert, KeyPair 
caKeyPair, String name, PublicKey certPublicKey) throws Exception {
+        return newCert(caCert, caKeyPair, name, certPublicKey, null);
+    }
+
     /**
      * Uses the private key of the given key pair to create a self-signed CA 
certificate with the public half of the
      * key pair and the given subject and expiration. The issuer of the new 
cert will be equal to the subject.
@@ -102,6 +127,11 @@ public class X509TestHelpers {
      */
     public static X509Certificate newSelfSignedCACert(
             X500Name subject, KeyPair keyPair, long expirationMillis) throws 
IOException, OperatorCreationException, GeneralSecurityException {
+        return newSelfSignedCACert(subject, keyPair, expirationMillis, null);
+    }
+
+    public static X509Certificate newSelfSignedCACert(
+            X500Name subject, KeyPair keyPair, long expirationMillis, 
CertificateCustomization customization) throws IOException, 
OperatorCreationException, GeneralSecurityException {
         Date now = new Date();
         X509v3CertificateBuilder builder = initCertBuilder(subject, // for 
self-signed certs, issuer == subject
                                                            now, new 
Date(now.getTime()
@@ -129,7 +159,12 @@ now, new Date(now.getTime()
      * @throws GeneralSecurityException
      */
     public static X509Certificate newCert(
-            X509Certificate caCert, KeyPair caKeyPair, X500Name certSubject, 
PublicKey certPublicKey, long expirationMillis) throws IOException, 
OperatorCreationException, GeneralSecurityException {
+            X509Certificate caCert, KeyPair caKeyPair, X500Name certSubject, 
PublicKey certPublicKey, long expirationMillis) throws Exception {
+        return newCert(caCert, caKeyPair, certSubject, certPublicKey, 
expirationMillis, null);
+    }
+
+    public static X509Certificate newCert(
+            X509Certificate caCert, KeyPair caKeyPair, X500Name certSubject, 
PublicKey certPublicKey, long expirationMillis, CertificateCustomization 
customization) throws Exception {
         if (!caKeyPair.getPublic().equals(caCert.getPublicKey())) {
             throw new IllegalArgumentException("CA private key does not match 
the public key in the CA cert");
         }
@@ -143,6 +178,9 @@ public static X509Certificate newCert(
         builder.addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, 
KeyPurposeId.id_kp_clientAuth}));
 
         builder.addExtension(Extension.subjectAlternativeName, false, 
getLocalhostSubjectAltNames());
+        if (customization != null) {
+            customization.customize(builder);
+        }
         return buildAndSignCertificate(caKeyPair.getPrivate(), builder);
     }
 
@@ -172,7 +210,7 @@ private static GeneralNames getLocalhostSubjectAltNames() 
throws UnknownHostExce
      */
     private static X509v3CertificateBuilder initCertBuilder(
             X500Name issuer, Date notBefore, Date notAfter, X500Name subject, 
PublicKey subjectPublicKey) {
-        return new X509v3CertificateBuilder(issuer, new 
BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG), notBefore, notAfter, subject, 
SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded()));
+        return new JcaX509v3CertificateBuilder(issuer, new 
BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG), notBefore, notAfter, subject, 
SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded()));
     }
 
     /**
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
index fe9eea348..b5ac140ff 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
@@ -20,7 +20,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import io.netty.buffer.UnpooledByteBufAllocator;
@@ -32,7 +31,6 @@
 import java.net.Socket;
 import java.nio.file.Path;
 import java.security.NoSuchAlgorithmException;
-import java.security.Security;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Callable;
@@ -90,10 +88,6 @@ public void cleanUp() {
         System.clearProperty(x509Util.getCipherSuitesProperty());
         System.clearProperty(x509Util.getSslProtocolProperty());
         
System.clearProperty(x509Util.getSslHandshakeDetectionTimeoutMillisProperty());
-        System.clearProperty("com.sun.net.ssl.checkRevocation");
-        System.clearProperty("com.sun.security.enableCRLDP");
-        Security.setProperty("ocsp.enable", Boolean.FALSE.toString());
-        Security.setProperty("com.sun.security.enableCRLDP", 
Boolean.FALSE.toString());
         System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
         System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
         x509Util.close();
@@ -227,49 +221,6 @@ public void testCreateSSLContextWithCustomCipherSuites(
         assertArrayEquals(customCipherSuites, 
sslSocket.getEnabledCipherSuites());
     }
 
-    // It would be great to test the value of 
PKIXBuilderParameters#setRevocationEnabled but it does not appear to be
-    // possible
-    @ParameterizedTest
-    @MethodSource("data")
-    @Timeout(value = 5)
-    public void testCRLEnabled(
-            X509KeyType caKeyType, X509KeyType certKeyType, String 
keyPassword, Integer paramIndex)
-            throws Exception {
-        init(caKeyType, certKeyType, keyPassword, paramIndex);
-        System.setProperty(x509Util.getSslCrlEnabledProperty(), "true");
-        x509Util.getDefaultSSLContext();
-        
assertTrue(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation")));
-        
assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP")));
-        assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable")));
-    }
-
-    @ParameterizedTest
-    @MethodSource("data")
-    @Timeout(value = 5)
-    public void testCRLDisabled(
-            X509KeyType caKeyType, X509KeyType certKeyType, String 
keyPassword, Integer paramIndex)
-            throws Exception {
-        init(caKeyType, certKeyType, keyPassword, paramIndex);
-        x509Util.getDefaultSSLContext();
-        
assertFalse(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation")));
-        
assertFalse(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP")));
-        assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable")));
-    }
-
-    @ParameterizedTest
-    @MethodSource("data")
-    @Timeout(value = 5)
-    public void testOCSPEnabled(
-            X509KeyType caKeyType, X509KeyType certKeyType, String 
keyPassword, Integer paramIndex)
-            throws Exception {
-        init(caKeyType, certKeyType, keyPassword, paramIndex);
-        System.setProperty(x509Util.getSslOcspEnabledProperty(), "true");
-        x509Util.getDefaultSSLContext();
-        
assertTrue(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation")));
-        
assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP")));
-        assertTrue(Boolean.valueOf(Security.getProperty("ocsp.enable")));
-    }
-
     @ParameterizedTest
     @MethodSource("data")
     @Timeout(value = 5)
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Ca.java 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Ca.java
new file mode 100644
index 000000000..dfeea3012
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Ca.java
@@ -0,0 +1,193 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import com.sun.net.httpserver.HttpServer;
+import java.io.FileWriter;
+import java.math.BigInteger;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.zookeeper.common.X509TestHelpers;
+import org.bouncycastle.asn1.ASN1GeneralizedTime;
+import org.bouncycastle.asn1.ocsp.RevokedInfo;
+import org.bouncycastle.asn1.x509.CRLNumber;
+import org.bouncycastle.asn1.x509.CRLReason;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.cert.X509CRLHolder;
+import org.bouncycastle.cert.X509v2CRLBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
+import org.bouncycastle.openssl.MiscPEMGenerator;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.util.io.pem.PemWriter;
+
+public class Ca implements AutoCloseable {
+    public static class CaBuilder {
+        private final Path dir;
+        private String name = "CA";
+        private boolean ocsp = false;
+
+        CaBuilder(Path dir) {
+            this.dir = dir;
+        }
+
+        public CaBuilder withName(String name) {
+            this.name = Objects.requireNonNull(name);
+            return this;
+        }
+
+        public CaBuilder withOcsp() {
+            this.ocsp = true;
+            return this;
+        }
+
+        public Ca build() throws Exception {
+            KeyPair caKey = X509TestHelpers.generateRSAKeyPair();
+            X509Certificate caCert = X509TestHelpers.newSelfSignedCert(name, 
caKey);
+            if (ocsp) {
+                HttpServer ocspServer = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
+                Ca ca = new Ca(dir, name, caKey, caCert, ocspServer);
+                ca.ocspServer.createContext("/", new OCSPHandler(ca));
+                ca.ocspServer.start();
+                return ca;
+            }
+            return new Ca(dir, name, caKey, caCert, null);
+        }
+    }
+
+    public final Path dir;
+    public final String name;
+    public final KeyPair key;
+    public final X509Certificate cert;
+    public final Map<X509Certificate, RevokedInfo> crlRevokedCerts = 
Collections.synchronizedMap(new HashMap<>());
+    public final Map<X509Certificate, RevokedInfo> ocspRevokedCerts = 
Collections.synchronizedMap(new HashMap<>());
+    public final HttpServer ocspServer;
+    public final AtomicLong crlNumber = new AtomicLong(1);
+    public final PemFile pemFile;
+
+    Ca(Path dir, String name, KeyPair key, X509Certificate cert, HttpServer 
ocspServer) throws Exception {
+        this.dir = dir;
+        this.name = name;
+        this.key = key;
+        this.cert = cert;
+        this.ocspServer = ocspServer;
+        this.pemFile = writePem();
+    }
+
+    private PemFile writePem() throws Exception {
+        String pem = X509TestHelpers.pemEncodeX509Certificate(cert);
+        Path file = Files.createTempFile(dir, name, ".pem");
+        Files.write(file, pem.getBytes());
+        return new PemFile(file, "");
+    }
+
+    // Check result of crldp could be cached, so use per-cert crl file.
+    public void flush_crl(Cert cert) throws Exception {
+        Objects.requireNonNull(cert.crl, "cert is signed with no crldp");
+        Instant now = Instant.now();
+
+        X509v2CRLBuilder builder = new 
JcaX509v2CRLBuilder(cert.cert.getIssuerX500Principal(), Date.from(now));
+        builder.setNextUpdate(Date.from(now.plusSeconds(2)));
+
+        builder.addExtension(Extension.authorityKeyIdentifier, false, new 
JcaX509ExtensionUtils().createAuthorityKeyIdentifier(this.cert));
+        builder.addExtension(Extension.cRLNumber, false, new 
CRLNumber(BigInteger.valueOf(crlNumber.getAndAdd(1L))));
+
+        for (Map.Entry<X509Certificate, RevokedInfo> entry : 
crlRevokedCerts.entrySet()) {
+            builder.addCRLEntry(entry.getKey().getSerialNumber(), 
entry.getValue().getRevocationTime().getDate(), CRLReason.cACompromise);
+        }
+
+        ContentSigner contentSigner = new 
JcaContentSignerBuilder("SHA256WithRSAEncryption").build(this.key.getPrivate());
+        X509CRLHolder crlHolder = builder.build(contentSigner);
+
+        Path tmpFile = Files.createTempFile(dir, "crldp-", ".pem.tmp");
+        PemWriter pemWriter = new PemWriter(new FileWriter(tmpFile.toFile()));
+        pemWriter.writeObject(new MiscPEMGenerator(crlHolder));
+        pemWriter.flush();
+        pemWriter.close();
+
+        Files.move(tmpFile, cert.crl, StandardCopyOption.REPLACE_EXISTING, 
StandardCopyOption.ATOMIC_MOVE);
+    }
+
+    public void revoke_through_crldp(Cert cert) throws Exception {
+        Date now = new Date();
+        RevokedInfo revokedInfo = new RevokedInfo(new 
ASN1GeneralizedTime(now), CRLReason.lookup(CRLReason.cACompromise));
+        this.crlRevokedCerts.put(cert.cert, revokedInfo);
+        flush_crl(cert);
+    }
+
+    public void revoke_through_ocsp(X509Certificate cert) throws Exception {
+        Date now = new Date();
+        RevokedInfo revokedInfo = new RevokedInfo(new 
ASN1GeneralizedTime(now), CRLReason.lookup(CRLReason.cACompromise));
+        this.ocspRevokedCerts.put(cert, revokedInfo);
+    }
+
+    public CertSigner signer(String name) throws Exception {
+        return new CertSigner(this, name);
+    }
+
+    public Cert sign(String name) throws Exception {
+        return signer(name).sign();
+    }
+
+    public Cert sign_with_crldp(String name) throws Exception {
+        return signer(name).withCrldp().sign();
+    }
+
+    public Cert sign_with_ocsp(String name) throws Exception {
+        return signer(name).withOcsp().sign();
+    }
+
+    public static CaBuilder builder(Path dir) {
+        return new CaBuilder(dir);
+    }
+
+    public static Ca create(Path dir) throws Exception {
+        return Ca.builder(dir).build();
+    }
+
+    public static Ca create(String name, Path dir) throws Exception {
+        return Ca.builder(dir).withName(name).build();
+    }
+
+    public String getOcspAddress() {
+        if (ocspServer != null) {
+            return String.format("http://127.0.0.1:%d";, 
ocspServer.getAddress().getPort());
+        }
+        throw new IllegalStateException("No OCSP server available");
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (ocspServer != null) {
+            ocspServer.stop(0);
+        }
+    }
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Cert.java 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Cert.java
new file mode 100644
index 000000000..d388a7a5d
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/Cert.java
@@ -0,0 +1,91 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.util.Properties;
+import java.util.UUID;
+import org.apache.zookeeper.client.ZKClientConfig;
+import org.apache.zookeeper.common.X509TestHelpers;
+
+public class Cert {
+    public final String name;
+    public final KeyPair key;
+    public final X509Certificate cert;
+    public final Path dir;
+    public final Path crl;
+
+    Cert(String name, KeyPair key, X509Certificate cert, Path dir, Path crl) {
+        this.name = name;
+        this.key = key;
+        this.cert = cert;
+        this.dir = dir;
+        this.crl = crl;
+    }
+
+    public PemFile writePem() throws Exception {
+        String password = UUID.randomUUID().toString();
+        String pem = X509TestHelpers.pemEncodeCertAndPrivateKey(cert, 
key.getPrivate(), password);
+        Path file = Files.createTempFile(dir, name, ".pem");
+        Files.write(file, pem.getBytes());
+        return new PemFile(file, password);
+    }
+
+    public Properties buildServerProperties(Ca ca) throws Exception {
+        final Properties config = new Properties();
+        config.put("clientPort", "0");
+        config.put("secureClientPort", "0");
+
+        // explicitly ipv4 to avoid dns lookup issue
+        config.put("clientPortAddress", "127.0.0.1");
+        config.put("secureClientPortAddress", "127.0.0.1");
+
+        config.put("admin.enableServer", "false");
+        config.put("admin.rateLimiterIntervalInMS", "0");
+
+        PemFile serverPem = writePem();
+
+        // TLS config fields
+        config.put("ssl.keyStore.location", serverPem.file.toString());
+        config.put("ssl.keyStore.password", serverPem.password);
+        config.put("ssl.trustStore.location", ca.pemFile.file.toString());
+
+        // Netty is required for TLS
+        config.put("serverCnxnFactory", 
org.apache.zookeeper.server.NettyServerCnxnFactory.class.getName());
+        config.put("4lw.commands.whitelist", "*");
+        return config;
+    }
+
+    public ZKClientConfig buildClientConfig(Ca ca) throws Exception {
+        PemFile pemFile = writePem();
+
+        ZKClientConfig config = new ZKClientConfig();
+        config.setProperty("zookeeper.client.secure", "true");
+        config.setProperty("zookeeper.ssl.keyStore.password", 
pemFile.password);
+        config.setProperty("zookeeper.ssl.keyStore.location", 
pemFile.file.toString());
+        config.setProperty("zookeeper.ssl.trustStore.location", 
ca.pemFile.file.toString());
+
+        // only netty supports TLS
+        config.setProperty("zookeeper.clientCnxnSocket", 
org.apache.zookeeper.ClientCnxnSocketNetty.class.getName());
+        return config;
+    }
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/CertSigner.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/CertSigner.java
new file mode 100644
index 000000000..28ae6d61e
--- /dev/null
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/CertSigner.java
@@ -0,0 +1,174 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import org.apache.zookeeper.common.X509TestHelpers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.CRLDistPoint;
+import org.bouncycastle.asn1.x509.DistributionPoint;
+import org.bouncycastle.asn1.x509.DistributionPointName;
+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.X509ObjectIdentifiers;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+public class CertSigner {
+    private final Ca ca;
+    private final String name;
+
+    private Path crldp;
+    private boolean ocsp;
+
+    private final List<String> dnsNames = new ArrayList<>();
+    private final List<String> ipAddresses = new ArrayList<>();
+    private Duration expiration = Duration.ofDays(1);
+    private X509CertBuilder certBuilder;
+
+    CertSigner(Ca ca, String name) {
+        this.ca = ca;
+        this.name = name;
+    }
+
+    public CertSigner withCrldp() throws Exception {
+        this.crldp = Files.createTempFile(ca.dir, String.format("%s-crldp-", 
name), ".pem");
+        return this;
+    }
+
+    public CertSigner withOcsp() {
+        this.ocsp = true;
+        return this;
+    }
+
+    public CertSigner withDnsName(String name) {
+        dnsNames.add(name);
+        return this;
+    }
+
+    public CertSigner withResolvedDns(String name) throws Exception {
+        dnsNames.add(name);
+        InetAddress[] localAddresses = InetAddress.getAllByName("localhost");
+        for (InetAddress addr : localAddresses) {
+            ipAddresses.add(addr.getHostAddress());
+        }
+        return this;
+    }
+
+    public CertSigner withIpAddress(String ipAddress) {
+        ipAddresses.add(ipAddress);
+        return this;
+    }
+
+    /**
+     * Default to {@code Duration.ofDays(1)}.
+     */
+    public CertSigner withExpiration(Duration expiration) {
+        this.expiration = expiration;
+        return this;
+    }
+
+    public CertSigner withCertBuilder(X509CertBuilder certBuilder) {
+        this.certBuilder = certBuilder;
+        return this;
+    }
+
+    public Cert sign() throws Exception {
+        X509CertificateHolder holder = new JcaX509CertificateHolder(ca.cert);
+        ContentSigner signer = new 
JcaContentSignerBuilder("SHA256WithRSAEncryption").build(ca.key.getPrivate());
+
+        List<GeneralName> generalNames = new ArrayList<>();
+        for (String dnsName : dnsNames) {
+            generalNames.add(new GeneralName(GeneralName.dNSName, dnsName));
+        }
+        for (String ipAddress : ipAddresses) {
+            generalNames.add(new GeneralName(GeneralName.iPAddress, 
ipAddress));
+        }
+
+        Instant now = Instant.now();
+        KeyPair key = X509TestHelpers.generateRSAKeyPair();
+        JcaX509v3CertificateBuilder jcaX509v3CertificateBuilder = new 
JcaX509v3CertificateBuilder(
+                holder.getSubject(),
+                new BigInteger(128, new Random()),
+                Date.from(now.minus(Duration.ofSeconds(10))),
+                Date.from(now.plus(expiration)),
+                new X500Name(String.format("CN=%s", name)),
+                key.getPublic());
+        X509v3CertificateBuilder certificateBuilder = 
jcaX509v3CertificateBuilder
+                .addExtension(Extension.basicConstraints, true, new 
BasicConstraints(false))
+                .addExtension(Extension.keyUsage, true, new 
KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
+
+        if (!generalNames.isEmpty()) {
+            certificateBuilder.addExtension(
+                    Extension.subjectAlternativeName,
+                    true,
+                    new GeneralNames(generalNames.toArray(new 
GeneralName[]{})));
+        }
+
+        if (crldp != null) {
+            DistributionPointName distPointOne = new DistributionPointName(
+                    new GeneralNames(new 
GeneralName(GeneralName.uniformResourceIdentifier, "file://" + 
crldp.toAbsolutePath())));
+
+            certificateBuilder.addExtension(
+                    Extension.cRLDistributionPoints,
+                    false,
+                    new CRLDistPoint(new DistributionPoint[]{new 
DistributionPoint(distPointOne, null, null)}));
+        }
+
+        if (ocsp) {
+            certificateBuilder.addExtension(
+                    Extension.authorityInfoAccess,
+                    false,
+                    new AuthorityInformationAccess(
+                            X509ObjectIdentifiers.ocspAccessMethod,
+                            new 
GeneralName(GeneralName.uniformResourceIdentifier, ca.getOcspAddress())));
+        }
+
+        if (certBuilder != null) {
+            certBuilder.build(certificateBuilder);
+        }
+
+        X509Certificate certificate = new 
JcaX509CertificateConverter().getCertificate(certificateBuilder.build(signer));
+        Cert cert = new Cert(name, key, certificate, ca.dir, crldp);
+        if (crldp != null) {
+            ca.flush_crl(cert);
+        }
+        return cert;
+    }
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/OCSPHandler.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/OCSPHandler.java
new file mode 100644
index 000000000..5cf37ac38
--- /dev/null
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/OCSPHandler.java
@@ -0,0 +1,128 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpHandler;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.bouncycastle.asn1.ocsp.OCSPResponse;
+import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
+import org.bouncycastle.asn1.ocsp.RevokedInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+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.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.Req;
+import org.bouncycastle.cert.ocsp.RevokedStatus;
+import org.bouncycastle.cert.ocsp.jcajce.JcaBasicOCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class OCSPHandler implements HttpHandler {
+    private static final Logger LOG = 
LoggerFactory.getLogger(OCSPHandler.class);
+
+    private final Ca ca;
+
+    public OCSPHandler(Ca ca) {
+        this.ca = ca;
+    }
+
+    @Override
+    public void handle(com.sun.net.httpserver.HttpExchange httpExchange) 
throws IOException {
+        byte[] responseBytes;
+        try {
+            String uri = httpExchange.getRequestURI().toString();
+            LOG.info("OCSP request: {} {}", httpExchange.getRequestMethod(), 
uri);
+            httpExchange.getRequestHeaders().entrySet().forEach((e) -> {
+                LOG.info("OCSP request header: {} {}", e.getKey(), 
e.getValue());
+            });
+            InputStream request = httpExchange.getRequestBody();
+            byte[] requestBytes = new byte[10000];
+            int len = request.read(requestBytes);
+            LOG.info("OCSP request size {}", len);
+
+            if (len < 0) {
+                String removedUriEncoding = 
URLDecoder.decode(uri.substring(1), "utf-8");
+                LOG.info("OCSP request from URI no encoding {}", 
removedUriEncoding);
+                requestBytes = Base64.getDecoder().decode(removedUriEncoding);
+            }
+            OCSPReq ocspRequest = new OCSPReq(requestBytes);
+            Req[] requestList = ocspRequest.getRequestList();
+            LOG.info("requestList {}", Arrays.toString(requestList));
+
+            DigestCalculator digestCalculator = new 
JcaDigestCalculatorProviderBuilder().build().get(CertificateID.HASH_SHA1);
+
+            Map<CertificateID, RevokedInfo> revokedCerts = 
ca.ocspRevokedCerts.entrySet().stream().collect(Collectors.toMap(entry -> {
+                try {
+                    return new JcaCertificateID(digestCalculator, ca.cert, 
entry.getKey().getSerialNumber());
+                } catch (Exception ex) {
+                    throw new RuntimeException(ex);
+                }
+            }, Map.Entry::getValue));
+
+            BasicOCSPRespBuilder responseBuilder = new 
JcaBasicOCSPRespBuilder(ca.key.getPublic(), digestCalculator);
+            for (Req req : requestList) {
+                CertificateID certId = req.getCertID();
+                CertificateStatus certificateStatus = CertificateStatus.GOOD;
+                RevokedInfo revokedInfo = revokedCerts.get(certId);
+                if (revokedInfo != null) {
+                    certificateStatus = new RevokedStatus(revokedInfo);
+                }
+                responseBuilder.addResponse(certId, certificateStatus, null);
+            }
+
+            X509CertificateHolder[] chain = new X509CertificateHolder[]{new 
JcaX509CertificateHolder(ca.cert)};
+            ContentSigner signer = new 
JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(ca.key.getPrivate());
+            BasicOCSPResp ocspResponse = responseBuilder.build(signer, chain, 
Calendar.getInstance().getTime());
+            LOG.info("response {}", ocspResponse);
+            responseBytes = new 
OCSPRespBuilder().build(OCSPRespBuilder.SUCCESSFUL, ocspResponse).getEncoded();
+            LOG.error("OCSP server response OK");
+        } catch (Throwable exception) {
+            LOG.error("Internal OCSP server error", exception);
+            responseBytes = new OCSPResp(new OCSPResponse(new 
OCSPResponseStatus(OCSPRespBuilder.INTERNAL_ERROR), null)).getEncoded();
+        }
+
+        Headers rh = httpExchange.getResponseHeaders();
+        rh.set("Content-Type", "application/ocsp-response");
+        httpExchange.sendResponseHeaders(200, responseBytes.length);
+
+        OutputStream os = httpExchange.getResponseBody();
+        os.write(responseBytes);
+        os.close();
+    }
+
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/PemFile.java 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/PemFile.java
new file mode 100644
index 000000000..c7f95a9e0
--- /dev/null
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/PemFile.java
@@ -0,0 +1,31 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import java.nio.file.Path;
+
+public class PemFile {
+    public final Path file;
+    public final String password;
+
+    public PemFile(Path file, String password) {
+        this.file = file;
+        this.password = password;
+    }
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/X509CertBuilder.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/X509CertBuilder.java
new file mode 100644
index 000000000..962a099fc
--- /dev/null
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ssl/X509CertBuilder.java
@@ -0,0 +1,26 @@
+/*
+ * 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.zookeeper.common.ssl;
+
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+
+@FunctionalInterface
+public interface X509CertBuilder {
+    void build(X509v3CertificateBuilder builder) throws Exception;
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/server/ClientSSLRevocationTest.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/ClientSSLRevocationTest.java
new file mode 100644
index 000000000..c1a603165
--- /dev/null
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/ClientSSLRevocationTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.zookeeper.server;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Security;
+import java.util.Properties;
+import org.apache.zookeeper.client.ZKClientConfig;
+import org.apache.zookeeper.common.ssl.Ca;
+import org.apache.zookeeper.common.ssl.Cert;
+import org.apache.zookeeper.server.embedded.ExitHandler;
+import org.apache.zookeeper.server.embedded.ZooKeeperServerEmbedded;
+import org.apache.zookeeper.test.ClientBase;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class ClientSSLRevocationTest {
+    @BeforeEach
+    public void setup() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    @AfterEach
+    public void cleanup() throws Exception {
+        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+
+        Security.setProperty("ocsp.enable", "false");
+        System.clearProperty("com.sun.net.ssl.checkRevocation");
+        System.clearProperty("zookeeper.ssl.crl");
+        System.clearProperty("zookeeper.ssl.ocsp");
+    }
+
+    @Test
+    public void testRevocationDisabled(@TempDir Path tmpDir) throws Exception {
+        // given: crl not enabled
+        try (Ca ca = Ca.builder(tmpDir).withOcsp().build()) {
+            Cert serverCert = ca.sign_with_ocsp("server");
+            final Properties config = serverCert.buildServerProperties(ca);
+            // given: revoked server cert
+            ca.revoke_through_ocsp(serverCert.cert);
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server.data"))
+                    .configuration(config)
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                Cert client1Cert = ca.sign_with_crldp("client1");
+                ca.revoke_through_crldp(client1Cert);
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                // when: connect with revoked cert.
+                // then: connected
+                ZKClientConfig client1Config = 
client1Cert.buildClientConfig(ca);
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, client1Config));
+            }
+        }
+    }
+
+    @ParameterizedTest(name = "clientRevoked = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testRevocationInClientUsingCrldp(boolean clientRevoked, 
@TempDir Path tmpDir) throws Exception {
+        try (Ca ca = Ca.create(tmpDir)) {
+            // given: server cert with crldp
+            Cert server1Cert = ca.sign_with_crldp("server1");
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server.data"))
+                    .configuration(server1Cert.buildServerProperties(ca))
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                Cert clientCert = ca.sign_with_crldp("client1");
+                if (clientRevoked) {
+                    // crl in server side is disabled, so it does not matter 
whether
+                    // client cert is revoked or not.
+                    ca.revoke_through_crldp(clientCert);
+                }
+
+                // then: ssl authentication succeed when crl is disabled
+                ZKClientConfig clientConfig = clientCert.buildClientConfig(ca);
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, clientConfig));
+
+                // when: valid server cert
+                // then: ssl authentication succeed when crl is enabled
+                clientConfig.setProperty("zookeeper.ssl.crl", "true");
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, clientConfig));
+            }
+
+            // crldp check is not realtime, so we have to start a new server 
with revoked cert
+
+            // given: revoked server cert with crldp
+            Cert server2Cert = ca.sign_with_crldp("server2");
+            ca.revoke_through_crldp(server2Cert);
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server2.data"))
+                    .configuration(server2Cert.buildServerProperties(ca))
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                Cert clientCert = ca.sign_with_crldp("client1");
+                if (clientRevoked) {
+                    // crl in server side is disabled, so it does not matter 
whether
+                    // client cert is revoked or not.
+                    ca.revoke_through_crldp(clientCert);
+                }
+
+                // then: ssl authentication succeed when crl is disabled
+                ZKClientConfig clientConfig = clientCert.buildClientConfig(ca);
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, clientConfig));
+
+                // then: ssl authentication failed when crl is enabled
+                clientConfig.setProperty("zookeeper.ssl.crl", "true");
+                
assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 
6000, true, clientConfig));
+            }
+        }
+    }
+
+    @ParameterizedTest(name = "clientRevoked = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testRevocationInClientUsingOCSP(boolean clientRevoked, 
@TempDir Path tmpDir) throws Exception {
+        try (Ca ca = Ca.builder(tmpDir).withOcsp().build()) {
+            // given: server cert with ocsp
+            Cert serverCert = ca.sign_with_ocsp("server1");
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server.data"))
+                    .configuration(serverCert.buildServerProperties(ca))
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                Cert clientCert = ca.sign_with_ocsp("client");
+                if (clientRevoked) {
+                    // crl in server side is disabled, so it does not matter 
whether
+                    // client cert is revoked or not.
+                    ca.revoke_through_ocsp(clientCert.cert);
+                }
+
+                ZKClientConfig clientConfig = clientCert.buildClientConfig(ca);
+
+                // when: connect to serve with valid cert
+                // then: connected
+                //
+                // we can't config crl using jvm properties as server will 
access them also
+                // see: https://issues.apache.org/jira/browse/ZOOKEEPER-4875
+                clientConfig.setProperty("zookeeper.ssl.crl", "true");
+                clientConfig.setProperty("zookeeper.ssl.ocsp", "true");
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, clientConfig));
+
+                // when: server cert get revoked
+                ca.revoke_through_ocsp(serverCert.cert);
+
+                // then: ssl authentication failed when crl is enabled
+                
assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 
6000, true, clientConfig));
+
+                // then: ssl authentication succeed when crl is disabled
+                clientConfig.setProperty("zookeeper.ssl.crl", "false");
+                clientConfig.setProperty("zookeeper.ssl.ocsp", "false");
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, clientConfig));
+            }
+        }
+    }
+
+    @ParameterizedTest(name = "serverRevoked = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testRevocationInServerUsingCrldp(boolean serverRevoked, 
@TempDir Path tmpDir) throws Exception {
+        try (Ca ca = Ca.create(tmpDir)) {
+            // given: server with crl enabled
+            System.setProperty("zookeeper.ssl.crl", "true");
+            Cert serverCert = ca.sign_with_crldp("server1");
+            if (serverRevoked) {
+                // crl in client side will be disabled, so it does not matter 
whether
+                // server cert is revoked or not.
+                ca.revoke_through_crldp(serverCert);
+            }
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server.data"))
+                    .configuration(serverCert.buildServerProperties(ca))
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                // when: valid client cert with crldp
+                // then: ssl authentication failed when crl is enabled
+                Cert client1Cert = ca.sign_with_crldp("client1");
+                ZKClientConfig client1Config = 
client1Cert.buildClientConfig(ca);
+                client1Config.setProperty("zookeeper.ssl.crl", "false");
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, client1Config));
+
+                Cert client2Cert = ca.sign_with_crldp("client2");
+                ca.revoke_through_crldp(client2Cert);
+
+                // when: revoked client cert with crldp
+                // then: ssl authentication failed when crl is enabled
+                ZKClientConfig client2Config = 
client2Cert.buildClientConfig(ca);
+                client2Config.setProperty("zookeeper.ssl.crl", "false");
+                
assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 
6000, true, client2Config));
+            }
+        }
+    }
+
+    @ParameterizedTest(name = "serverRevoked = {0}")
+    @ValueSource(booleans = {true, false})
+    public void testRevocationInServerUsingOCSP(boolean serverRevoked, 
@TempDir Path tmpDir) throws Exception {
+        try (Ca ca = Ca.builder(tmpDir).withOcsp().build()) {
+            // given: server with crl and ocsp enabled
+            System.setProperty("zookeeper.ssl.crl", "true");
+            System.setProperty("zookeeper.ssl.ocsp", "true");
+            Cert serverCert = ca.sign("server1");
+            if (serverRevoked) {
+                // crl in client side will be disabled, so it does not matter 
whether
+                // server cert is revoked or not.
+                ca.revoke_through_ocsp(serverCert.cert);
+            }
+            try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded
+                    .builder()
+                    .baseDir(Files.createTempDirectory(tmpDir, "server.data"))
+                    .configuration(serverCert.buildServerProperties(ca))
+                    .exitHandler(ExitHandler.LOG_ONLY)
+                    .build()) {
+                server.start();
+
+                
assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000));
+
+                // when: valid client cert with crldp
+                // then: ssl authentication failed when crl is enabled
+                Cert client1Cert = ca.sign_with_ocsp("client1");
+                ZKClientConfig client1Config = 
client1Cert.buildClientConfig(ca);
+                client1Config.setProperty("zookeeper.ssl.crl", "false");
+                client1Config.setProperty("zookeeper.ssl.ocsp", "false");
+                
assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, 
true, client1Config));
+
+                // ocsp is realtime, so we can reuse this client.
+                ca.revoke_through_ocsp(client1Cert.cert);
+                
assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 
6000, true, client1Config));
+            }
+        }
+    }
+}
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/QuorumSSLTest.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/QuorumSSLTest.java
index 3177024f5..95ff31be9 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/QuorumSSLTest.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/QuorumSSLTest.java
@@ -905,6 +905,7 @@ public void testOCSP(boolean fipsEnabled) throws Exception {
             assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + 
clientPortQp3, CONNECTION_TIMEOUT));
 
             setSSLSystemProperties();
+            System.setProperty(quorumX509Util.getSslCrlEnabledProperty(), 
"true");
             System.setProperty(quorumX509Util.getSslOcspEnabledProperty(), 
"true");
 
             X509Certificate validCertificate = buildEndEntityCert(
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientBase.java 
b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientBase.java
index b8b8e4832..effc39687 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientBase.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientBase.java
@@ -40,6 +40,7 @@
 import java.util.concurrent.TimeoutException;
 import javax.management.MBeanServerConnection;
 import javax.management.ObjectName;
+import javax.net.ssl.SSLContext;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.PortAssignment;
 import org.apache.zookeeper.TestableZooKeeper;
@@ -49,8 +50,11 @@
 import org.apache.zookeeper.ZKTestCase;
 import org.apache.zookeeper.ZooKeeper;
 import org.apache.zookeeper.client.ZKClientConfig;
+import org.apache.zookeeper.common.ClientX509Util;
 import org.apache.zookeeper.common.Time;
 import org.apache.zookeeper.common.X509Exception.SSLContextException;
+import org.apache.zookeeper.common.X509Util;
+import org.apache.zookeeper.common.ZKConfig;
 import org.apache.zookeeper.server.ServerCnxnFactory;
 import org.apache.zookeeper.server.ZKDatabase;
 import org.apache.zookeeper.server.ZooKeeperServer;
@@ -262,12 +266,23 @@ public static boolean waitForServerUp(String hp, long 
timeout) {
     }
 
     public static boolean waitForServerUp(String hp, long timeout, boolean 
secure) {
+        return waitForServerUp(hp, timeout, secure, null);
+    }
+
+    public static boolean waitForServerUp(String hp, long timeout, boolean 
secure, ZKConfig zkConfig) {
         long start = Time.currentElapsedTime();
         while (true) {
             try {
                 // if there are multiple hostports, just take the first one
                 HostPort hpobj = parseHostPortList(hp).get(0);
-                String result = send4LetterWord(hpobj.host, hpobj.port, 
"stat", secure);
+                SSLContext sslContext = null;
+                String result;
+                if (zkConfig != null) {
+                    try (X509Util x509Util = new ClientX509Util()) {
+                        sslContext = x509Util.createSSLContext(zkConfig);
+                    }
+                }
+                result = send4LetterWord(hpobj.host, hpobj.port, "stat", 
secure, 5000, sslContext);
                 if (result.startsWith("Zookeeper version:") && 
!result.contains("READ-ONLY")) {
                     return true;
                 }

Reply via email to