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