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