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