This is an automated email from the ASF dual-hosted git repository. andor pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push: new 45b49a5aa ZOOKEEPER-4955: [ADDENDUM] Refactor Ca, Cert and etc. so to be reusable 45b49a5aa is described below commit 45b49a5aa9d7f2f699619eb9f486e2b89bdc19d7 Author: Kezhu Wang <kez...@apache.org> AuthorDate: Wed Aug 27 23:51:34 2025 +0800 ZOOKEEPER-4955: [ADDENDUM] Refactor Ca, Cert and etc. so to be reusable Reviewers: anmolnar Author: kezhuw Closes #2300 from kezhuw/ZOOKEEPER-4955-addendum-refactor-test-helpers --- .../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 | 406 ++------------------- 7 files changed, 679 insertions(+), 370 deletions(-) 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 index 6e1fd2708..0471e7952 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/ClientSSLRevocationTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/ClientSSLRevocationTest.java @@ -20,328 +20,34 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.net.InetSocketAddress; -import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.security.KeyPair; import java.security.Security; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.util.Arrays; -import java.util.Base64; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; import org.apache.zookeeper.client.ZKClientConfig; -import org.apache.zookeeper.common.X509TestHelpers; +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.asn1.ASN1GeneralizedTime; -import org.bouncycastle.asn1.ocsp.OCSPResponse; -import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; -import org.bouncycastle.asn1.ocsp.RevokedInfo; -import org.bouncycastle.asn1.x509.AuthorityInformationAccess; -import org.bouncycastle.asn1.x509.CRLDistPoint; -import org.bouncycastle.asn1.x509.CRLNumber; -import org.bouncycastle.asn1.x509.CRLReason; -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.X509ObjectIdentifiers; -import org.bouncycastle.cert.X509CRLHolder; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v2CRLBuilder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder; -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.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.MiscPEMGenerator; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.DigestCalculator; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.util.io.pem.PemWriter; 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class ClientSSLRevocationTest { - private static final Logger LOG = LoggerFactory.getLogger(ClientSSLRevocationTest.class); - - static { + @BeforeEach + public void setup() throws Exception { Security.addProvider(new BouncyCastleProvider()); } - private static class OCSPHandler implements HttpHandler { - 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(); - } - - } - - private static class PemFile { - private final Path file; - private final String password; - - public PemFile(Path file, String password) { - this.file = file; - this.password = password; - } - } - - private static class CertWithCrl extends Cert { - public final Path crl; - - public CertWithCrl(String name, KeyPair key, X509Certificate cert, Path crl) { - super(name, key, cert, crl.getParent()); - this.crl = crl; - } - } - - private static class Cert { - public final String name; - public final KeyPair key; - public final X509Certificate cert; - public final Path dir; - - public Cert(String name, KeyPair key, X509Certificate cert, Path dir) { - this.name = name; - this.key = key; - this.cert = cert; - this.dir = dir; - } - - 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); - } - } - - private static class Ca implements AutoCloseable { - private final String name; - private final KeyPair key; - private final X509Certificate cert; - private final Path dir; - private final Map<X509Certificate, RevokedInfo> crlRevokedCerts = Collections.synchronizedMap(new HashMap<>()); - private final Map<X509Certificate, RevokedInfo> ocspRevokedCerts = Collections.synchronizedMap(new HashMap<>()); - private final HttpServer ocspServer; - private final AtomicLong crlNumber = new AtomicLong(1); - - private Ca(String name, KeyPair key, X509Certificate cert, Path dir, HttpServer ocspServer) throws Exception { - this.name = name; - this.key = key; - this.cert = cert; - this.dir = dir; - this.ocspServer = ocspServer; - } - - public 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(Path crl) throws Exception { - Instant now = Instant.now(); - - X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(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, crl, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } - - public void revoke_through_crldp(CertWithCrl 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.crl); - } - - 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 Cert sign(String name) throws Exception { - KeyPair key = X509TestHelpers.generateRSAKeyPair(); - X509Certificate cert = X509TestHelpers.newCert(this.cert, this.key, name, key.getPublic()); - return new Cert(name, key, cert, dir); - } - - public CertWithCrl sign_with_crl(String name) throws Exception { - KeyPair key = X509TestHelpers.generateRSAKeyPair(); - Path crl = Files.createTempFile(dir, String.format("%s-crldp-", name), ".pem"); - X509Certificate cert = X509TestHelpers.newCert(this.cert, this.key, name, key.getPublic(), builder -> { - DistributionPointName distPointOne = new DistributionPointName( - new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, "file://" + crl))); - builder.addExtension( - Extension.cRLDistributionPoints, - false, - new CRLDistPoint(new DistributionPoint[]{new DistributionPoint(distPointOne, null, null)})); - - }); - flush_crl(crl); - return new CertWithCrl(name, key, cert, crl); - } - - public Cert sign_with_ocsp(String name) throws Exception { - KeyPair key = X509TestHelpers.generateRSAKeyPair(); - X509Certificate cert = X509TestHelpers.newCert(this.cert, this.key, name, key.getPublic(), builder -> { - String addr = "http://127.0.0.1:" + ocspServer.getAddress().getPort(); - builder.addExtension( - Extension.authorityInfoAccess, - false, - new AuthorityInformationAccess( - X509ObjectIdentifiers.ocspAccessMethod, - new GeneralName(GeneralName.uniformResourceIdentifier, addr))); - }); - return new Cert(name, key, cert, dir); - } - - public static Ca create(Path dir) throws Exception { - return create(dir, "CA"); - } - - public static Ca create(Path dir, String name) throws Exception { - KeyPair caKey = X509TestHelpers.generateRSAKeyPair(); - X509Certificate caCert = X509TestHelpers.newSelfSignedCert(name, caKey); - HttpServer ocspServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); - Ca ca = new Ca(name, caKey, caCert, dir, ocspServer); - ca.ocspServer.createContext("/", new OCSPHandler(ca)); - ca.ocspServer.start(); - return ca; - } - - @Override - public void close() throws Exception { - ocspServer.stop(0); - } - } - @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"); @@ -351,11 +57,9 @@ public void cleanup() throws Exception { @Test public void testRevocationDisabled(@TempDir Path tmpDir) throws Exception { // given: crl not enabled - try (Ca ca = Ca.create(tmpDir)) { - PemFile caPem = ca.writePem(); - + try (Ca ca = Ca.builder(tmpDir).withOcsp().build()) { Cert serverCert = ca.sign_with_ocsp("server"); - final Properties config = getServerConfig(caPem, serverCert); + final Properties config = serverCert.buildServerProperties(ca); // given: revoked server cert ca.revoke_through_ocsp(serverCert.cert); try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded @@ -366,36 +70,36 @@ public void testRevocationDisabled(@TempDir Path tmpDir) throws Exception { .build()) { server.start(); - CertWithCrl client1Cert = ca.sign_with_crl("client1"); + 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 - assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, getZKClientConfig(caPem, client1Cert))); + ZKClientConfig client1Config = client1Cert.buildClientConfig(ca); + assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client1Config)); } } } - @ParameterizedTest + @ParameterizedTest(name = "clientRevoked = {0}") @ValueSource(booleans = {true, false}) public void testRevocationInClientUsingCrldp(boolean clientRevoked, @TempDir Path tmpDir) throws Exception { try (Ca ca = Ca.create(tmpDir)) { - PemFile caPem = ca.writePem(); // given: server cert with crldp - CertWithCrl server1Cert = ca.sign_with_crl("server1"); + Cert server1Cert = ca.sign_with_crldp("server1"); try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded .builder() .baseDir(Files.createTempDirectory(tmpDir, "server.data")) - .configuration(getServerConfig(caPem, server1Cert)) + .configuration(server1Cert.buildServerProperties(ca)) .exitHandler(ExitHandler.LOG_ONLY) .build()) { server.start(); assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); - CertWithCrl clientCert = ca.sign_with_crl("client1"); + 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. @@ -403,7 +107,7 @@ public void testRevocationInClientUsingCrldp(boolean clientRevoked, @TempDir Pat } // then: ssl authentication succeed when crl is disabled - ZKClientConfig clientConfig = getZKClientConfig(caPem, clientCert); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, clientConfig)); // when: valid server cert @@ -415,19 +119,19 @@ public void testRevocationInClientUsingCrldp(boolean clientRevoked, @TempDir Pat // crldp check is not realtime, so we have to start a new server with revoked cert // given: revoked server cert with crldp - CertWithCrl server2Cert = ca.sign_with_crl("server2"); + Cert server2Cert = ca.sign_with_crldp("server2"); ca.revoke_through_crldp(server2Cert); try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded .builder() .baseDir(Files.createTempDirectory(tmpDir, "server2.data")) - .configuration(getServerConfig(caPem, server2Cert)) + .configuration(server2Cert.buildServerProperties(ca)) .exitHandler(ExitHandler.LOG_ONLY) .build()) { server.start(); assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); - CertWithCrl clientCert = ca.sign_with_crl("client1"); + 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. @@ -435,7 +139,7 @@ public void testRevocationInClientUsingCrldp(boolean clientRevoked, @TempDir Pat } // then: ssl authentication succeed when crl is disabled - ZKClientConfig clientConfig = getZKClientConfig(caPem, clientCert); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, clientConfig)); // then: ssl authentication failed when crl is enabled @@ -445,17 +149,16 @@ public void testRevocationInClientUsingCrldp(boolean clientRevoked, @TempDir Pat } } - @ParameterizedTest + @ParameterizedTest(name = "clientRevoked = {0}") @ValueSource(booleans = {true, false}) public void testRevocationInClientUsingOCSP(boolean clientRevoked, @TempDir Path tmpDir) throws Exception { - try (Ca ca = Ca.create(tmpDir)) { - PemFile caPem = ca.writePem(); + 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(getServerConfig(caPem, serverCert)) + .configuration(serverCert.buildServerProperties(ca)) .exitHandler(ExitHandler.LOG_ONLY) .build()) { server.start(); @@ -469,7 +172,7 @@ public void testRevocationInClientUsingOCSP(boolean clientRevoked, @TempDir Path ca.revoke_through_ocsp(clientCert.cert); } - ZKClientConfig clientConfig = getZKClientConfig(caPem, clientCert); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); // when: connect to serve with valid cert // then: connected @@ -493,15 +196,13 @@ public void testRevocationInClientUsingOCSP(boolean clientRevoked, @TempDir Path } } - - @ParameterizedTest + @ParameterizedTest(name = "serverRevoked = {0}") @ValueSource(booleans = {true, false}) public void testRevocationInServerUsingCrldp(boolean serverRevoked, @TempDir Path tmpDir) throws Exception { try (Ca ca = Ca.create(tmpDir)) { - PemFile caPem = ca.writePem(); // given: server with crl enabled System.setProperty("zookeeper.ssl.crl", "true"); - CertWithCrl serverCert = ca.sign_with_crl("server1"); + 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. @@ -510,7 +211,7 @@ public void testRevocationInServerUsingCrldp(boolean serverRevoked, @TempDir Pat try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded .builder() .baseDir(Files.createTempDirectory(tmpDir, "server.data")) - .configuration(getServerConfig(caPem, serverCert)) + .configuration(serverCert.buildServerProperties(ca)) .exitHandler(ExitHandler.LOG_ONLY) .build()) { server.start(); @@ -519,28 +220,27 @@ public void testRevocationInServerUsingCrldp(boolean serverRevoked, @TempDir Pat // when: valid client cert with crldp // then: ssl authentication failed when crl is enabled - Cert client1Cert = ca.sign_with_crl("client1"); - ZKClientConfig client1Config = getZKClientConfig(caPem, client1Cert); + 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)); - CertWithCrl client2Cert = ca.sign_with_crl("client2"); + 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 = getZKClientConfig(caPem, client2Cert); + ZKClientConfig client2Config = client2Cert.buildClientConfig(ca); client2Config.setProperty("zookeeper.ssl.crl", "false"); assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client2Config)); } } } - @ParameterizedTest + @ParameterizedTest(name = "serverRevoked = {0}") @ValueSource(booleans = {true, false}) public void testRevocationInServerUsingOCSP(boolean serverRevoked, @TempDir Path tmpDir) throws Exception { - try (Ca ca = Ca.create(tmpDir)) { - PemFile caPem = ca.writePem(); + try (Ca ca = Ca.builder(tmpDir).withOcsp().build()) { // given: server with crl and ocsp enabled System.setProperty("com.sun.net.ssl.checkRevocation", "true"); System.setProperty("zookeeper.ssl.ocsp", "true"); @@ -553,7 +253,7 @@ public void testRevocationInServerUsingOCSP(boolean serverRevoked, @TempDir Path try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded .builder() .baseDir(Files.createTempDirectory(tmpDir, "server.data")) - .configuration(getServerConfig(caPem, serverCert)) + .configuration(serverCert.buildServerProperties(ca)) .exitHandler(ExitHandler.LOG_ONLY) .build()) { server.start(); @@ -563,7 +263,7 @@ public void testRevocationInServerUsingOCSP(boolean serverRevoked, @TempDir Path // when: valid client cert with crldp // then: ssl authentication failed when crl is enabled Cert client1Cert = ca.sign_with_ocsp("client1"); - ZKClientConfig client1Config = getZKClientConfig(caPem, client1Cert); + ZKClientConfig client1Config = client1Cert.buildClientConfig(ca); client1Config.setProperty("zookeeper.ssl.crl", "false"); assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client1Config)); @@ -573,38 +273,4 @@ public void testRevocationInServerUsingOCSP(boolean serverRevoked, @TempDir Path } } } - - private Properties getServerConfig(PemFile ca, Cert identity) throws Exception { - final Properties config = new Properties(); - config.put("clientPort", "0"); - config.put("secureClientPort", "0"); - config.put("host", "localhost"); - config.put("ticktime", "4000"); - - PemFile serverPem = identity.writePem(); - - // TLS config fields - //config.put("ssl.clientAuth", "need"); - config.put("ssl.keyStore.location", serverPem.file.toString()); - config.put("ssl.keyStore.password", serverPem.password); - config.put("ssl.trustStore.location", ca.file.toString()); - - // Netty is required for TLS - config.put("serverCnxnFactory", org.apache.zookeeper.server.NettyServerCnxnFactory.class.getName()); - config.put("4lw.commands.whitelist", "*"); - return config; - } - - private ZKClientConfig getZKClientConfig(PemFile ca, Cert cert) throws Exception { - PemFile pemFile = cert.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.file.toString()); - // only netty supports TLS - config.setProperty("zookeeper.clientCnxnSocket", org.apache.zookeeper.ClientCnxnSocketNetty.class.getName()); - return config; - } }