1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.handler.ssl.ocsp;
17
18 import io.netty.bootstrap.Bootstrap;
19 import io.netty.buffer.ByteBuf;
20 import io.netty.buffer.Unpooled;
21 import io.netty.channel.ChannelFuture;
22 import io.netty.channel.ChannelInitializer;
23 import io.netty.channel.ChannelOption;
24 import io.netty.channel.ChannelPipeline;
25 import io.netty.channel.EventLoop;
26 import io.netty.channel.socket.SocketChannel;
27 import io.netty.handler.codec.http.DefaultFullHttpRequest;
28 import io.netty.handler.codec.http.FullHttpRequest;
29 import io.netty.handler.codec.http.HttpClientCodec;
30 import io.netty.handler.codec.http.HttpHeaderNames;
31 import io.netty.handler.codec.http.HttpObjectAggregator;
32 import io.netty.resolver.dns.DnsNameResolver;
33 import io.netty.util.concurrent.Future;
34 import io.netty.util.concurrent.FutureListener;
35 import io.netty.util.concurrent.GenericFutureListener;
36 import io.netty.util.concurrent.Promise;
37 import io.netty.util.internal.SystemPropertyUtil;
38 import io.netty.util.internal.logging.InternalLogger;
39 import io.netty.util.internal.logging.InternalLoggerFactory;
40 import org.bouncycastle.asn1.DEROctetString;
41 import org.bouncycastle.asn1.x509.AccessDescription;
42 import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
43 import org.bouncycastle.asn1.x509.Extension;
44 import org.bouncycastle.asn1.x509.Extensions;
45 import org.bouncycastle.cert.X509CertificateHolder;
46 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
47 import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
48 import org.bouncycastle.cert.ocsp.BasicOCSPResp;
49 import org.bouncycastle.cert.ocsp.CertificateID;
50 import org.bouncycastle.cert.ocsp.OCSPException;
51 import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
52 import org.bouncycastle.cert.ocsp.OCSPResp;
53 import org.bouncycastle.operator.ContentVerifierProvider;
54 import org.bouncycastle.operator.OperatorCreationException;
55 import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
56 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
57
58 import java.net.InetAddress;
59 import java.net.URL;
60 import java.security.InvalidAlgorithmParameterException;
61 import java.security.NoSuchAlgorithmException;
62 import java.security.SecureRandom;
63 import java.security.cert.CertPathBuilder;
64 import java.security.cert.CertPathBuilderException;
65 import java.security.cert.CertStore;
66 import java.security.cert.CertificateEncodingException;
67 import java.security.cert.CertificateException;
68 import java.security.cert.CollectionCertStoreParameters;
69 import java.security.cert.PKIXBuilderParameters;
70 import java.security.cert.TrustAnchor;
71 import java.security.cert.X509CertSelector;
72 import java.security.cert.X509Certificate;
73 import java.util.ArrayList;
74 import java.util.Collections;
75 import java.util.List;
76
77 import static io.netty.handler.codec.http.HttpMethod.POST;
78 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
79 import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_REQUEST_TYPE;
80 import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_RESPONSE_TYPE;
81 import static io.netty.util.internal.ObjectUtil.checkNotNull;
82 import static org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers.id_pkix_ocsp_nonce;
83 import static org.bouncycastle.asn1.x509.X509ObjectIdentifiers.id_ad_ocsp;
84 import static org.bouncycastle.cert.ocsp.CertificateID.HASH_SHA1;
85
86 final class OcspClient {
87
88 private static final InternalLogger logger = InternalLoggerFactory.getInstance(OcspClient.class);
89
90 private static final SecureRandom SECURE_RANDOM = new SecureRandom();
91 private static final int OCSP_RESPONSE_MAX_SIZE = SystemPropertyUtil.getInt(
92 "io.netty.ocsp.responseSize", 1024 * 10);
93
94 static {
95 logger.debug("-Dio.netty.ocsp.responseSize: {} bytes", OCSP_RESPONSE_MAX_SIZE);
96 }
97
98
99
100
101
102
103
104
105
106
107 static Promise<BasicOCSPResp> query(final X509Certificate x509Certificate,
108 final X509Certificate issuer, final boolean validateResponseNonce,
109 final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
110 final EventLoop eventLoop = ioTransport.eventLoop();
111 final Promise<BasicOCSPResp> responsePromise = eventLoop.newPromise();
112 eventLoop.execute(new Runnable() {
113 @Override
114 public void run() {
115 try {
116 CertificateID certificateID = new CertificateID(new JcaDigestCalculatorProviderBuilder()
117 .build().get(HASH_SHA1), new JcaX509CertificateHolder(issuer),
118 x509Certificate.getSerialNumber());
119
120
121 OCSPReqBuilder builder = new OCSPReqBuilder();
122 builder.addRequest(certificateID);
123
124
125
126
127
128
129
130 byte[] nonce = new byte[16];
131 SECURE_RANDOM.nextBytes(nonce);
132 final DEROctetString derNonce = new DEROctetString(nonce);
133 builder.setRequestExtensions(new Extensions(new Extension(id_pkix_ocsp_nonce, false, derNonce)));
134
135
136 URL uri = new URL(parseOcspUrlFromCertificate(x509Certificate));
137
138
139 int port = uri.getPort();
140 if (port == -1) {
141 port = uri.getDefaultPort();
142 }
143
144
145 String path = uri.getPath();
146 if (path.isEmpty()) {
147 path = "/";
148 } else {
149 if (uri.getQuery() != null) {
150 path = path + '?' + uri.getQuery();
151 }
152 }
153
154 Promise<OCSPResp> ocspResponsePromise = query(eventLoop,
155 Unpooled.wrappedBuffer(builder.build().getEncoded()),
156 uri.getHost(), port, path, ioTransport, dnsNameResolver);
157
158
159 ocspResponsePromise.addListener((GenericFutureListener<Future<OCSPResp>>) future -> {
160
161
162 if (future.isSuccess()) {
163 try {
164 BasicOCSPResp resp = (BasicOCSPResp) future.getNow().getResponseObject();
165 validateResponse(responsePromise, resp, derNonce, issuer, validateResponseNonce);
166 } catch (Throwable t) {
167 responsePromise.tryFailure(t);
168 }
169 } else {
170 responsePromise.tryFailure(future.cause());
171 }
172 });
173
174 } catch (Exception ex) {
175 responsePromise.tryFailure(ex);
176 }
177 }
178 });
179 return responsePromise;
180 }
181
182
183
184
185
186
187
188
189
190
191
192
193 private static Promise<OCSPResp> query(final EventLoop eventLoop, final ByteBuf ocspRequest,
194 final String host, final int port, final String path,
195 final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
196 final Promise<OCSPResp> responsePromise = eventLoop.newPromise();
197
198 try {
199 final Bootstrap bootstrap = new Bootstrap()
200 .group(ioTransport.eventLoop())
201 .option(ChannelOption.TCP_NODELAY, true)
202 .channelFactory(ioTransport.socketChannel())
203 .attr(OcspServerCertificateValidator.OCSP_PIPELINE_ATTRIBUTE, Boolean.TRUE)
204 .handler(new Initializer(responsePromise));
205 dnsNameResolver.resolve(host).addListener((FutureListener<InetAddress>) future -> {
206
207
208
209 if (future.isSuccess()) {
210
211 InetAddress hostAddress = future.getNow();
212 final ChannelFuture channelFuture = bootstrap.connect(hostAddress, port);
213 channelFuture.addListener(f -> {
214
215
216 if (f.isSuccess()) {
217 FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, path,
218 ocspRequest);
219 request.headers().add(HttpHeaderNames.HOST, host);
220 request.headers().add(HttpHeaderNames.USER_AGENT, "Netty OCSP Client");
221 request.headers().add(HttpHeaderNames.CONTENT_TYPE, OCSP_REQUEST_TYPE);
222 request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, OCSP_RESPONSE_TYPE);
223 request.headers().add(HttpHeaderNames.CONTENT_LENGTH, ocspRequest.readableBytes());
224
225
226 channelFuture.channel().writeAndFlush(request);
227 } else {
228 responsePromise.tryFailure(new IllegalStateException(
229 "Connection to OCSP Responder Failed", f.cause()));
230 }
231 });
232 } else {
233 responsePromise.tryFailure(future.cause());
234 }
235 });
236 } catch (Exception ex) {
237 responsePromise.tryFailure(ex);
238 }
239
240 return responsePromise;
241 }
242
243 private static void validateResponse(Promise<BasicOCSPResp> responsePromise, BasicOCSPResp basicResponse,
244 DEROctetString derNonce, X509Certificate issuer, boolean validateNonce) {
245 try {
246
247
248 int responses = basicResponse.getResponses().length;
249 if (responses != 1) {
250 throw new IllegalArgumentException("Expected number of responses was 1 but got: " + responses);
251 }
252
253 if (validateNonce) {
254 validateNonce(basicResponse, derNonce);
255 }
256 validateSignature(basicResponse, issuer);
257 responsePromise.trySuccess(basicResponse);
258 } catch (Exception ex) {
259 responsePromise.tryFailure(ex);
260 }
261 }
262
263
264
265
266 private static void validateNonce(BasicOCSPResp basicResponse, DEROctetString encodedNonce) throws OCSPException {
267 Extension nonceExt = basicResponse.getExtension(id_pkix_ocsp_nonce);
268 if (nonceExt != null) {
269 DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue();
270 if (!responseNonceString.equals(encodedNonce)) {
271 throw new OCSPException("Nonce does not match");
272 }
273 } else {
274 throw new IllegalArgumentException("Nonce is not present");
275 }
276 }
277
278
279
280
281 static void validateSignature(BasicOCSPResp resp, X509Certificate issuerCertificate) throws OCSPException {
282 try {
283 X509CertificateHolder[] certs = resp.getCerts();
284 JcaContentVerifierProviderBuilder providerBuilder = new JcaContentVerifierProviderBuilder();
285
286
287 if (certs != null && certs.length > 0) {
288
289
290 X509CertificateHolder responderCert = certs[0];
291
292
293 ContentVerifierProvider responderVerifier = providerBuilder.build(responderCert);
294
295 if (!resp.isSignatureValid(responderVerifier)) {
296 throw new OCSPException("OCSP response signature is not valid");
297 }
298
299
300 validateCertificateChain(responderCert, certs, issuerCertificate);
301 } else {
302
303 ContentVerifierProvider issuerVerifier = providerBuilder.build(issuerCertificate);
304
305 if (!resp.isSignatureValid(issuerVerifier)) {
306 throw new OCSPException("OCSP response signature is not valid");
307 }
308 }
309 } catch (OperatorCreationException e) {
310 throw new OCSPException("Error validating OCSP-Signature", e);
311 } catch (CertificateException e) {
312 throw new OCSPException("Error while processing certificates for OCSP signature validation", e);
313 }
314 }
315
316
317
318
319
320 private static void validateCertificateChain(X509CertificateHolder responderCert,
321 X509CertificateHolder[] allCerts,
322 X509Certificate issuerCertificate) throws OCSPException {
323 try {
324
325 List<X509Certificate> certList = new ArrayList<>(allCerts.length);
326 for (X509CertificateHolder certHolder : allCerts) {
327 certList.add(new JcaX509CertificateConverter().getCertificate(certHolder));
328 }
329
330
331 CertStore certStore = CertStore.getInstance("Collection",
332 new CollectionCertStoreParameters(certList));
333
334
335 X509CertSelector targetConstraints = new X509CertSelector();
336 targetConstraints.setCertificate(new JcaX509CertificateConverter().getCertificate(responderCert));
337
338
339 TrustAnchor trustAnchor = new TrustAnchor(issuerCertificate, null);
340
341
342 PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(
343 Collections.singleton(trustAnchor), targetConstraints);
344 pkixParams.addCertStore(certStore);
345 pkixParams.setRevocationEnabled(false);
346
347
348 CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
349 builder.build(pkixParams);
350
351
352 } catch (CertPathBuilderException e) {
353 throw new OCSPException("OCSP responder certificate is not trusted by issuer: " + e.getMessage(), e);
354 } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
355 throw new OCSPException("Error setting up certificate path validation", e);
356 } catch (CertificateException e) {
357 throw new OCSPException("Error converting certificates for path validation", e);
358 }
359 }
360
361
362
363
364
365
366
367
368
369 private static String parseOcspUrlFromCertificate(X509Certificate cert) {
370 X509CertificateHolder holder;
371 try {
372 holder = new JcaX509CertificateHolder(cert);
373 } catch (CertificateEncodingException e) {
374
375 throw new IllegalArgumentException("Error while parsing X509Certificate into JcaX509CertificateHolder", e);
376 }
377
378 AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(holder.getExtensions());
379
380
381 for (AccessDescription accessDescription : aiaExtension.getAccessDescriptions()) {
382 if (accessDescription.getAccessMethod().equals(id_ad_ocsp)) {
383 return accessDescription.getAccessLocation().getName().toASN1Primitive().toString();
384 }
385 }
386
387 throw new NullPointerException("Unable to find OCSP responder URL in Certificate");
388 }
389
390 static final class Initializer extends ChannelInitializer<SocketChannel> {
391
392 private final Promise<OCSPResp> responsePromise;
393
394 Initializer(Promise<OCSPResp> responsePromise) {
395 this.responsePromise = checkNotNull(responsePromise, "ResponsePromise");
396 }
397
398 @Override
399 protected void initChannel(SocketChannel socketChannel) {
400 ChannelPipeline pipeline = socketChannel.pipeline();
401 pipeline.addLast(new HttpClientCodec());
402 pipeline.addLast(new HttpObjectAggregator(OCSP_RESPONSE_MAX_SIZE));
403 pipeline.addLast(new OcspHttpHandler(responsePromise));
404 }
405 }
406
407 private OcspClient() {
408
409 }
410 }