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