1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.pkitesting;
17
18 import com.sun.net.httpserver.HttpServer;
19
20 import java.io.OutputStream;
21 import java.math.BigInteger;
22 import java.net.InetAddress;
23 import java.net.InetSocketAddress;
24 import java.net.URI;
25 import java.security.Provider;
26 import java.security.cert.X509Certificate;
27 import java.time.Instant;
28 import java.util.Collections;
29 import java.util.Map;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ConcurrentMap;
32 import java.util.concurrent.ForkJoinPool;
33 import java.util.concurrent.atomic.AtomicInteger;
34
35
36
37
38
39
40
41
42
43
44
45
46 public final class RevocationServer {
47 private static volatile RevocationServer instance;
48
49 private final HttpServer crlServer;
50 private final String crlBaseAddress;
51 private final AtomicInteger issuerCounter;
52 private final ConcurrentMap<X509Certificate, CrlInfo> issuers;
53 private final ConcurrentMap<String, CrlInfo> paths;
54
55
56
57
58
59
60
61 public static RevocationServer getInstance() throws Exception {
62 if (instance != null) {
63 return instance;
64 }
65 synchronized (RevocationServer.class) {
66 RevocationServer server = instance;
67 if (server == null) {
68 server = new RevocationServer();
69 server.start();
70 instance = server;
71 }
72 return server;
73 }
74 }
75
76 private RevocationServer() throws Exception {
77
78 crlServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
79 crlBaseAddress = "http://localhost:" + crlServer.getAddress().getPort();
80 issuerCounter = new AtomicInteger();
81 issuers = new ConcurrentHashMap<>();
82 paths = new ConcurrentHashMap<>();
83 crlServer.createContext("/", exchange -> {
84 if ("GET".equals(exchange.getRequestMethod())) {
85 String path = exchange.getRequestURI().getPath();
86 CrlInfo info = paths.get(path);
87 if (info == null) {
88 exchange.sendResponseHeaders(404, 0);
89 exchange.close();
90 return;
91 }
92 byte[] crl = generateCrl(info);
93 exchange.getResponseHeaders().put("Content-Type", Collections.singletonList("application/pkix-crl"));
94 exchange.sendResponseHeaders(200, crl.length);
95 try (OutputStream out = exchange.getResponseBody()) {
96 out.write(crl);
97 out.flush();
98 }
99 } else {
100 exchange.sendResponseHeaders(405, 0);
101 }
102 exchange.close();
103 });
104 }
105
106 private void start() {
107 if (Thread.currentThread().isDaemon()) {
108 crlServer.start();
109 } else {
110
111
112
113
114 ForkJoinPool.commonPool().execute(crlServer::start);
115 }
116 }
117
118
119
120
121
122
123 public void register(X509Bundle issuer) {
124 register(issuer, null);
125 }
126
127
128
129
130
131
132
133 public void register(X509Bundle issuer, Provider provider) {
134 issuers.computeIfAbsent(issuer.getCertificate(), bundle -> {
135 String path = "/crl/" + issuerCounter.incrementAndGet() + ".crl";
136 URI uri = URI.create(crlBaseAddress + path);
137 CrlInfo info = new CrlInfo(issuer, uri, provider);
138 paths.put(path, info);
139 return info;
140 });
141 }
142
143
144
145
146
147
148
149
150
151 public void revoke(X509Bundle cert, Instant time) {
152 X509Certificate[] certPath = cert.getCertificatePathWithRoot();
153 X509Certificate issuer = certPath.length == 1 ? certPath[0] : certPath[1];
154 CrlInfo info = issuers.get(issuer);
155 if (info != null) {
156 info.revokedCerts.put(cert.getCertificate().getSerialNumber(), time);
157 } else {
158 throw new IllegalArgumentException("Not a registered issuer: " + issuer.getSubjectX500Principal());
159 }
160 }
161
162
163
164
165
166
167
168 public URI getCrlUri(X509Bundle issuer) {
169 CrlInfo info = issuers.get(issuer.getCertificate());
170 if (info != null) {
171 return info.uri;
172 }
173 return null;
174 }
175
176 private static byte[] generateCrl(CrlInfo info) {
177 X509Bundle issuer = info.issuer;
178 Map<BigInteger, Instant> certs = info.revokedCerts;
179 Instant now = Instant.now();
180 CertificateList list = new CertificateList(issuer, now, now, certs.entrySet());
181 try {
182 Signed signed = new Signed(list.getEncoded(), issuer);
183 return signed.getEncoded(info.provider);
184 } catch (Exception e) {
185 throw new IllegalStateException("Failed to sign CRL", e);
186 }
187 }
188
189 private static final class CrlInfo {
190 private final X509Bundle issuer;
191 private final URI uri;
192 private final Map<BigInteger, Instant> revokedCerts;
193 private final Provider provider;
194
195 CrlInfo(X509Bundle issuer, URI uri, Provider provider) {
196 this.issuer = issuer;
197 this.uri = uri;
198 this.provider = provider;
199 revokedCerts = new ConcurrentHashMap<>();
200 }
201 }
202 }