Skip to content

Commit 2a38d8b

Browse files
committed
ZOOKEEPER-4955: [ADDENDUM] Refactor Ca, Cert and etc. so to be reusable
This is a fixup commit to #2292. It contains only test code changes to make SSL related classes, e.g. Ca, Cert and etcd., to be reusable.
1 parent 770804b commit 2a38d8b

File tree

7 files changed

+670
-365
lines changed

7 files changed

+670
-365
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.zookeeper.common.ssl;
20+
21+
import com.sun.net.httpserver.HttpServer;
22+
import java.io.FileWriter;
23+
import java.math.BigInteger;
24+
import java.net.InetSocketAddress;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.StandardCopyOption;
28+
import java.security.KeyPair;
29+
import java.security.cert.X509Certificate;
30+
import java.time.Instant;
31+
import java.util.Collections;
32+
import java.util.Date;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
import java.util.Objects;
36+
import java.util.concurrent.atomic.AtomicLong;
37+
import org.apache.zookeeper.common.X509TestHelpers;
38+
import org.bouncycastle.asn1.ASN1GeneralizedTime;
39+
import org.bouncycastle.asn1.ocsp.RevokedInfo;
40+
import org.bouncycastle.asn1.x509.CRLNumber;
41+
import org.bouncycastle.asn1.x509.CRLReason;
42+
import org.bouncycastle.asn1.x509.Extension;
43+
import org.bouncycastle.cert.X509CRLHolder;
44+
import org.bouncycastle.cert.X509v2CRLBuilder;
45+
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
46+
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
47+
import org.bouncycastle.openssl.MiscPEMGenerator;
48+
import org.bouncycastle.operator.ContentSigner;
49+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
50+
import org.bouncycastle.util.io.pem.PemWriter;
51+
52+
public class Ca implements AutoCloseable {
53+
public static class CaBuilder {
54+
private final Path dir;
55+
private String name = "CA";
56+
private boolean ocsp = false;
57+
58+
CaBuilder(Path dir) {
59+
this.dir = dir;
60+
}
61+
62+
public CaBuilder withName(String name) {
63+
this.name = Objects.requireNonNull(name);
64+
return this;
65+
}
66+
67+
public CaBuilder withOcsp() {
68+
this.ocsp = true;
69+
return this;
70+
}
71+
72+
public Ca build() throws Exception {
73+
KeyPair caKey = X509TestHelpers.generateRSAKeyPair();
74+
X509Certificate caCert = X509TestHelpers.newSelfSignedCert(name, caKey);
75+
if (ocsp) {
76+
HttpServer ocspServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
77+
Ca ca = new Ca(dir, name, caKey, caCert, ocspServer);
78+
ca.ocspServer.createContext("/", new OCSPHandler(ca));
79+
ca.ocspServer.start();
80+
return ca;
81+
}
82+
return new Ca(dir, name, caKey, caCert, null);
83+
}
84+
}
85+
86+
public final Path dir;
87+
public final String name;
88+
public final KeyPair key;
89+
public final X509Certificate cert;
90+
public final Map<X509Certificate, RevokedInfo> crlRevokedCerts = Collections.synchronizedMap(new HashMap<>());
91+
public final Map<X509Certificate, RevokedInfo> ocspRevokedCerts = Collections.synchronizedMap(new HashMap<>());
92+
public final HttpServer ocspServer;
93+
public final AtomicLong crlNumber = new AtomicLong(1);
94+
public final PemFile pemFile;
95+
96+
Ca(Path dir, String name, KeyPair key, X509Certificate cert, HttpServer ocspServer) throws Exception {
97+
this.dir = dir;
98+
this.name = name;
99+
this.key = key;
100+
this.cert = cert;
101+
this.ocspServer = ocspServer;
102+
this.pemFile = writePem();
103+
}
104+
105+
private PemFile writePem() throws Exception {
106+
String pem = X509TestHelpers.pemEncodeX509Certificate(cert);
107+
Path file = Files.createTempFile(dir, name, ".pem");
108+
Files.write(file, pem.getBytes());
109+
return new PemFile(file, "");
110+
}
111+
112+
// Check result of crldp could be cached, so use per-cert crl file.
113+
public void flush_crl(Cert cert) throws Exception {
114+
Objects.requireNonNull(cert.crl, "cert is signed with no crldp");
115+
Instant now = Instant.now();
116+
117+
X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(cert.cert.getIssuerX500Principal(), Date.from(now));
118+
builder.setNextUpdate(Date.from(now.plusSeconds(2)));
119+
120+
builder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(this.cert));
121+
builder.addExtension(Extension.cRLNumber, false, new CRLNumber(BigInteger.valueOf(crlNumber.getAndAdd(1L))));
122+
123+
for (Map.Entry<X509Certificate, RevokedInfo> entry : crlRevokedCerts.entrySet()) {
124+
builder.addCRLEntry(entry.getKey().getSerialNumber(), entry.getValue().getRevocationTime().getDate(), CRLReason.cACompromise);
125+
}
126+
127+
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(this.key.getPrivate());
128+
X509CRLHolder crlHolder = builder.build(contentSigner);
129+
130+
Path tmpFile = Files.createTempFile(dir, "crldp-", ".pem.tmp");
131+
PemWriter pemWriter = new PemWriter(new FileWriter(tmpFile.toFile()));
132+
pemWriter.writeObject(new MiscPEMGenerator(crlHolder));
133+
pemWriter.flush();
134+
pemWriter.close();
135+
136+
Files.move(tmpFile, cert.crl, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
137+
}
138+
139+
public void revoke_through_crldp(Cert cert) throws Exception {
140+
Date now = new Date();
141+
RevokedInfo revokedInfo = new RevokedInfo(new ASN1GeneralizedTime(now), CRLReason.lookup(CRLReason.cACompromise));
142+
this.crlRevokedCerts.put(cert.cert, revokedInfo);
143+
flush_crl(cert);
144+
}
145+
146+
public void revoke_through_ocsp(X509Certificate cert) throws Exception {
147+
Date now = new Date();
148+
RevokedInfo revokedInfo = new RevokedInfo(new ASN1GeneralizedTime(now), CRLReason.lookup(CRLReason.cACompromise));
149+
this.ocspRevokedCerts.put(cert, revokedInfo);
150+
}
151+
152+
public CertSigner signer(String name) throws Exception {
153+
return new CertSigner(this, name);
154+
}
155+
156+
public Cert sign(String name) throws Exception {
157+
return signer(name).sign();
158+
}
159+
160+
public Cert sign_with_crldp(String name) throws Exception {
161+
return signer(name).withCrldp().sign();
162+
}
163+
164+
public Cert sign_with_ocsp(String name) throws Exception {
165+
return signer(name).withOcsp().sign();
166+
}
167+
168+
public static CaBuilder builder(Path dir) {
169+
return new CaBuilder(dir);
170+
}
171+
172+
public static Ca create(Path dir) throws Exception {
173+
return Ca.builder(dir).build();
174+
}
175+
176+
public static Ca create(String name, Path dir) throws Exception {
177+
return Ca.builder(dir).withName(name).build();
178+
}
179+
180+
public String getOcspAddress() {
181+
if (ocspServer != null) {
182+
return String.format("http://127.0.0.1:%d", ocspServer.getAddress().getPort());
183+
}
184+
throw new IllegalStateException("No OCSP server available");
185+
}
186+
187+
@Override
188+
public void close() throws Exception {
189+
if (ocspServer != null) {
190+
ocspServer.stop(0);
191+
}
192+
}
193+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.zookeeper.common.ssl;
20+
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.security.KeyPair;
24+
import java.security.cert.X509Certificate;
25+
import java.util.Properties;
26+
import java.util.UUID;
27+
import org.apache.zookeeper.client.ZKClientConfig;
28+
import org.apache.zookeeper.common.X509TestHelpers;
29+
30+
public class Cert {
31+
public final String name;
32+
public final KeyPair key;
33+
public final X509Certificate cert;
34+
public final Path dir;
35+
public final Path crl;
36+
37+
Cert(String name, KeyPair key, X509Certificate cert, Path dir, Path crl) {
38+
this.name = name;
39+
this.key = key;
40+
this.cert = cert;
41+
this.dir = dir;
42+
this.crl = crl;
43+
}
44+
45+
public PemFile writePem() throws Exception {
46+
String password = UUID.randomUUID().toString();
47+
String pem = X509TestHelpers.pemEncodeCertAndPrivateKey(cert, key.getPrivate(), password);
48+
Path file = Files.createTempFile(dir, name, ".pem");
49+
Files.write(file, pem.getBytes());
50+
return new PemFile(file, password);
51+
}
52+
53+
public Properties buildServerProperties(Ca ca) throws Exception {
54+
final Properties config = new Properties();
55+
config.put("clientPort", "0");
56+
config.put("secureClientPort", "0");
57+
config.put("host", "localhost");
58+
config.put("ticktime", "4000");
59+
60+
PemFile serverPem = writePem();
61+
62+
// TLS config fields
63+
config.put("ssl.keyStore.location", serverPem.file.toString());
64+
config.put("ssl.keyStore.password", serverPem.password);
65+
config.put("ssl.trustStore.location", ca.pemFile.file.toString());
66+
67+
// Netty is required for TLS
68+
config.put("serverCnxnFactory", org.apache.zookeeper.server.NettyServerCnxnFactory.class.getName());
69+
config.put("4lw.commands.whitelist", "*");
70+
return config;
71+
}
72+
73+
public ZKClientConfig buildClientConfig(Ca ca) throws Exception {
74+
PemFile pemFile = writePem();
75+
76+
ZKClientConfig config = new ZKClientConfig();
77+
config.setProperty("zookeeper.client.secure", "true");
78+
config.setProperty("zookeeper.ssl.keyStore.password", pemFile.password);
79+
config.setProperty("zookeeper.ssl.keyStore.location", pemFile.file.toString());
80+
config.setProperty("zookeeper.ssl.trustStore.location", ca.pemFile.file.toString());
81+
82+
// only netty supports TLS
83+
config.setProperty("zookeeper.clientCnxnSocket", org.apache.zookeeper.ClientCnxnSocketNetty.class.getName());
84+
return config;
85+
}
86+
}

0 commit comments

Comments
 (0)