View Javadoc
1   /*
2    * Copyright 2022 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
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       * Query the certificate status using OCSP
90       *
91       * @param x509Certificate       Client {@link X509Certificate} to validate
92       * @param issuer                {@link X509Certificate} issuer of client certificate
93       * @param validateResponseNonce Set to {@code true} to enable OCSP response validation
94       * @param ioTransport           {@link IoTransport} to use
95       * @return                      {@link Promise} of {@link BasicOCSPResp}
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                     // Initialize OCSP Request Builder and add CertificateID into it.
114                     OCSPReqBuilder builder = new OCSPReqBuilder();
115                     builder.addRequest(certificateID);
116 
117                     // Generate 16-bytes (octets) of nonce and add it into OCSP Request builder.
118                     // Because as per RFC-8954#2.1:
119                     //
120                     //   OCSP responders MUST accept lengths of at least
121                     //   16 octets and MAY choose to ignore the Nonce extension for requests
122                     //   where the length of the nonce is less than 16 octets.
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                     // Get OCSP URL from Certificate and query it.
129                     URL uri = new URL(parseOcspUrlFromCertificate(x509Certificate));
130 
131                     // Find port
132                     int port = uri.getPort();
133                     if (port == -1) {
134                         port = uri.getDefaultPort();
135                     }
136 
137                     // Configure path
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                     // Validate OCSP response
152                     ocspResponsePromise.addListener(new GenericFutureListener<Future<OCSPResp>>() {
153                         @Override
154                         public void operationComplete(Future<OCSPResp> future) throws Exception {
155                             // If Future was successful then we have received OCSP response
156                             // We will now validate it.
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      * Query the OCSP responder for certificate status using HTTP/1.1
186      *
187      * @param eventLoop   {@link EventLoop} for HTTP request execution
188      * @param ocspRequest {@link ByteBuf} containing OCSP request data
189      * @param host        OCSP responder hostname
190      * @param port        OCSP responder port
191      * @param path        OCSP responder path
192      * @param ioTransport {@link IoTransport} to use
193      * @return Returns {@link Promise} containing {@link OCSPResp}
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                     // If Future was successful then we have successfully resolved OCSP server address.
212                     // If not, mark 'responsePromise' as failure.
213                     if (future.isSuccess()) {
214                         // Get the resolved InetAddress
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                                 // If Future was successful then connection to OCSP responder was successful.
221                                 // We will send a OCSP request now
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                                     // Send the OCSP HTTP Request
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             // Validate number of responses. We only requested for 1 certificate
257             // so number of responses must be 1. If not, we will throw an error.
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      * Validate OCSP response nonce
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      * Validate OCSP response signature
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      * Parse OCSP endpoint URL from Certificate
314      *
315      * @param cert Certificate to be parsed
316      * @return OCSP endpoint URL
317      * @throws NullPointerException     If we couldn't locate OCSP responder URL
318      * @throws IllegalArgumentException If we couldn't parse X509Certificate into JcaX509CertificateHolder
319      */
320     private static String parseOcspUrlFromCertificate(X509Certificate cert) {
321         X509CertificateHolder holder;
322         try {
323             holder = new JcaX509CertificateHolder(cert);
324         } catch (CertificateEncodingException e) {
325             // Though this should never happen
326             throw new IllegalArgumentException("Error while parsing X509Certificate into JcaX509CertificateHolder", e);
327         }
328 
329         AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(holder.getExtensions());
330 
331         // Lookup for OCSP responder url
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         // Prevent outside initialization
362     }
363 }