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