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.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.SystemPropertyUtil;
38  import io.netty.util.internal.logging.InternalLogger;
39  import io.netty.util.internal.logging.InternalLoggerFactory;
40  import org.bouncycastle.asn1.DEROctetString;
41  import org.bouncycastle.asn1.x509.AccessDescription;
42  import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
43  import org.bouncycastle.asn1.x509.Extension;
44  import org.bouncycastle.asn1.x509.Extensions;
45  import org.bouncycastle.cert.X509CertificateHolder;
46  import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
47  import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
48  import org.bouncycastle.cert.ocsp.BasicOCSPResp;
49  import org.bouncycastle.cert.ocsp.CertificateID;
50  import org.bouncycastle.cert.ocsp.OCSPException;
51  import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
52  import org.bouncycastle.cert.ocsp.OCSPResp;
53  import org.bouncycastle.operator.ContentVerifierProvider;
54  import org.bouncycastle.operator.OperatorCreationException;
55  import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
56  import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
57  
58  import java.net.InetAddress;
59  import java.net.URL;
60  import java.security.InvalidAlgorithmParameterException;
61  import java.security.NoSuchAlgorithmException;
62  import java.security.SecureRandom;
63  import java.security.cert.CertPathBuilder;
64  import java.security.cert.CertPathBuilderException;
65  import java.security.cert.CertStore;
66  import java.security.cert.CertificateEncodingException;
67  import java.security.cert.CertificateException;
68  import java.security.cert.CollectionCertStoreParameters;
69  import java.security.cert.PKIXBuilderParameters;
70  import java.security.cert.TrustAnchor;
71  import java.security.cert.X509CertSelector;
72  import java.security.cert.X509Certificate;
73  import java.util.ArrayList;
74  import java.util.Collections;
75  import java.util.List;
76  
77  import static io.netty.handler.codec.http.HttpMethod.POST;
78  import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
79  import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_REQUEST_TYPE;
80  import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_RESPONSE_TYPE;
81  import static io.netty.util.internal.ObjectUtil.checkNotNull;
82  import static org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers.id_pkix_ocsp_nonce;
83  import static org.bouncycastle.asn1.x509.X509ObjectIdentifiers.id_ad_ocsp;
84  import static org.bouncycastle.cert.ocsp.CertificateID.HASH_SHA1;
85  
86  final class OcspClient {
87  
88      private static final InternalLogger logger = InternalLoggerFactory.getInstance(OcspClient.class);
89  
90      private static final SecureRandom SECURE_RANDOM = new SecureRandom();
91      private static final int OCSP_RESPONSE_MAX_SIZE = SystemPropertyUtil.getInt(
92              "io.netty.ocsp.responseSize", 1024 * 10);
93  
94      static {
95          logger.debug("-Dio.netty.ocsp.responseSize: {} bytes", OCSP_RESPONSE_MAX_SIZE);
96      }
97  
98      /**
99       * Query the certificate status using OCSP
100      *
101      * @param x509Certificate       Client {@link X509Certificate} to validate
102      * @param issuer                {@link X509Certificate} issuer of client certificate
103      * @param validateResponseNonce Set to {@code true} to enable OCSP response validation
104      * @param ioTransport           {@link IoTransport} to use
105      * @return {@link Promise} of {@link BasicOCSPResp}
106      */
107     static Promise<BasicOCSPResp> query(final X509Certificate x509Certificate,
108                                         final X509Certificate issuer, final boolean validateResponseNonce,
109                                         final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
110         final EventLoop eventLoop = ioTransport.eventLoop();
111         final Promise<BasicOCSPResp> responsePromise = eventLoop.newPromise();
112         eventLoop.execute(new Runnable() {
113             @Override
114             public void run() {
115                 try {
116                     CertificateID certificateID = new CertificateID(new JcaDigestCalculatorProviderBuilder()
117                             .build().get(HASH_SHA1), new JcaX509CertificateHolder(issuer),
118                             x509Certificate.getSerialNumber());
119 
120                     // Initialize OCSP Request Builder and add CertificateID into it.
121                     OCSPReqBuilder builder = new OCSPReqBuilder();
122                     builder.addRequest(certificateID);
123 
124                     // Generate 16-bytes (octets) of nonce and add it into OCSP Request builder.
125                     // Because as per RFC-8954#2.1:
126                     //
127                     //   OCSP responders MUST accept lengths of at least
128                     //   16 octets and MAY choose to ignore the Nonce extension for requests
129                     //   where the length of the nonce is less than 16 octets.
130                     byte[] nonce = new byte[16];
131                     SECURE_RANDOM.nextBytes(nonce);
132                     final DEROctetString derNonce = new DEROctetString(nonce);
133                     builder.setRequestExtensions(new Extensions(new Extension(id_pkix_ocsp_nonce, false, derNonce)));
134 
135                     // Get OCSP URL from Certificate and query it.
136                     URL uri = new URL(parseOcspUrlFromCertificate(x509Certificate));
137 
138                     // Find port
139                     int port = uri.getPort();
140                     if (port == -1) {
141                         port = uri.getDefaultPort();
142                     }
143 
144                     // Configure path
145                     String path = uri.getPath();
146                     if (path.isEmpty()) {
147                         path = "/";
148                     } else {
149                         if (uri.getQuery() != null) {
150                             path = path + '?' + uri.getQuery();
151                         }
152                     }
153 
154                     Promise<OCSPResp> ocspResponsePromise = query(eventLoop,
155                             Unpooled.wrappedBuffer(builder.build().getEncoded()),
156                             uri.getHost(), port, path, ioTransport, dnsNameResolver);
157 
158                     // Validate OCSP response
159                     ocspResponsePromise.addListener((GenericFutureListener<Future<OCSPResp>>) future -> {
160                         // If Future was successful then we have received OCSP response
161                         // We will now validate it.
162                         if (future.isSuccess()) {
163                             try {
164                                 BasicOCSPResp resp = (BasicOCSPResp) future.getNow().getResponseObject();
165                                 validateResponse(responsePromise, resp, derNonce, issuer, validateResponseNonce);
166                             } catch (Throwable t) {
167                                 responsePromise.tryFailure(t);
168                             }
169                         } else {
170                             responsePromise.tryFailure(future.cause());
171                         }
172                     });
173 
174                 } catch (Exception ex) {
175                     responsePromise.tryFailure(ex);
176                 }
177             }
178         });
179         return responsePromise;
180     }
181 
182     /**
183      * Query the OCSP responder for certificate status using HTTP/1.1
184      *
185      * @param eventLoop   {@link EventLoop} for HTTP request execution
186      * @param ocspRequest {@link ByteBuf} containing OCSP request data
187      * @param host        OCSP responder hostname
188      * @param port        OCSP responder port
189      * @param path        OCSP responder path
190      * @param ioTransport {@link IoTransport} to use
191      * @return Returns {@link Promise} containing {@link OCSPResp}
192      */
193     private static Promise<OCSPResp> query(final EventLoop eventLoop, final ByteBuf ocspRequest,
194                                            final String host, final int port, final String path,
195                                            final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
196         final Promise<OCSPResp> responsePromise = eventLoop.newPromise();
197 
198         try {
199             final Bootstrap bootstrap = new Bootstrap()
200                     .group(ioTransport.eventLoop())
201                     .option(ChannelOption.TCP_NODELAY, true)
202                     .channelFactory(ioTransport.socketChannel())
203                     .attr(OcspServerCertificateValidator.OCSP_PIPELINE_ATTRIBUTE, Boolean.TRUE)
204                     .handler(new Initializer(responsePromise));
205             dnsNameResolver.resolve(host).addListener((FutureListener<InetAddress>) future -> {
206 
207                 // If Future was successful then we have successfully resolved OCSP server address.
208                 // If not, mark 'responsePromise' as failure.
209                 if (future.isSuccess()) {
210                     // Get the resolved InetAddress
211                     InetAddress hostAddress = future.getNow();
212                     final ChannelFuture channelFuture = bootstrap.connect(hostAddress, port);
213                     channelFuture.addListener(f -> {
214                         // If Future was successful then connection to OCSP responder was successful.
215                         // We will send a OCSP request now
216                         if (f.isSuccess()) {
217                             FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, path,
218                                     ocspRequest);
219                             request.headers().add(HttpHeaderNames.HOST, host);
220                             request.headers().add(HttpHeaderNames.USER_AGENT, "Netty OCSP Client");
221                             request.headers().add(HttpHeaderNames.CONTENT_TYPE, OCSP_REQUEST_TYPE);
222                             request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, OCSP_RESPONSE_TYPE);
223                             request.headers().add(HttpHeaderNames.CONTENT_LENGTH, ocspRequest.readableBytes());
224 
225                             // Send the OCSP HTTP Request
226                             channelFuture.channel().writeAndFlush(request);
227                         } else {
228                             responsePromise.tryFailure(new IllegalStateException(
229                                     "Connection to OCSP Responder Failed", f.cause()));
230                         }
231                     });
232                 } else {
233                     responsePromise.tryFailure(future.cause());
234                 }
235             });
236         } catch (Exception ex) {
237             responsePromise.tryFailure(ex);
238         }
239 
240         return responsePromise;
241     }
242 
243     private static void validateResponse(Promise<BasicOCSPResp> responsePromise, BasicOCSPResp basicResponse,
244                                          DEROctetString derNonce, X509Certificate issuer, boolean validateNonce) {
245         try {
246             // Validate number of responses. We only requested for 1 certificate
247             // so number of responses must be 1. If not, we will throw an error.
248             int responses = basicResponse.getResponses().length;
249             if (responses != 1) {
250                 throw new IllegalArgumentException("Expected number of responses was 1 but got: " + responses);
251             }
252 
253             if (validateNonce) {
254                 validateNonce(basicResponse, derNonce);
255             }
256             validateSignature(basicResponse, issuer);
257             responsePromise.trySuccess(basicResponse);
258         } catch (Exception ex) {
259             responsePromise.tryFailure(ex);
260         }
261     }
262 
263     /**
264      * Validate OCSP response nonce
265      */
266     private static void validateNonce(BasicOCSPResp basicResponse, DEROctetString encodedNonce) throws OCSPException {
267         Extension nonceExt = basicResponse.getExtension(id_pkix_ocsp_nonce);
268         if (nonceExt != null) {
269             DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue();
270             if (!responseNonceString.equals(encodedNonce)) {
271                 throw new OCSPException("Nonce does not match");
272             }
273         } else {
274             throw new IllegalArgumentException("Nonce is not present");
275         }
276     }
277 
278     /**
279      * Validate OCSP response signature
280      */
281     static void validateSignature(BasicOCSPResp resp, X509Certificate issuerCertificate) throws OCSPException {
282         try {
283             X509CertificateHolder[] certs = resp.getCerts();
284             JcaContentVerifierProviderBuilder providerBuilder = new JcaContentVerifierProviderBuilder();
285 
286             // If responder certificate is included, validate the chain
287             if (certs != null && certs.length > 0) {
288 
289                 // Use the first included certificate to verify the OCSP response signature.
290                 X509CertificateHolder responderCert = certs[0];
291 
292                 // Verify OCSP response signature using responder cert
293                 ContentVerifierProvider responderVerifier = providerBuilder.build(responderCert);
294 
295                 if (!resp.isSignatureValid(responderVerifier)) {
296                     throw new OCSPException("OCSP response signature is not valid");
297                 }
298 
299                 // Build chain from responder certificate to issuer using CertPathBuilder
300                 validateCertificateChain(responderCert, certs, issuerCertificate);
301             } else {
302                 // Validate signature using issuer certificate
303                 ContentVerifierProvider issuerVerifier = providerBuilder.build(issuerCertificate);
304 
305                 if (!resp.isSignatureValid(issuerVerifier)) {
306                     throw new OCSPException("OCSP response signature is not valid");
307                 }
308             }
309         } catch (OperatorCreationException e) {
310             throw new OCSPException("Error validating OCSP-Signature", e);
311         } catch (CertificateException e) {
312             throw new OCSPException("Error while processing certificates for OCSP signature validation", e);
313         }
314     }
315 
316     /**
317      * Validates that a certificate chain can be built from the responder certificate to the issuer.
318      * Uses Java's CertPathBuilder to construct and validate the chain.
319      */
320     private static void validateCertificateChain(X509CertificateHolder responderCert,
321                                                    X509CertificateHolder[] allCerts,
322                                                    X509Certificate issuerCertificate) throws OCSPException {
323         try {
324             // Convert BouncyCastle certificate holders to Java X509Certificates
325             List<X509Certificate> certList = new ArrayList<>(allCerts.length);
326             for (X509CertificateHolder certHolder : allCerts) {
327                 certList.add(new JcaX509CertificateConverter().getCertificate(certHolder));
328             }
329 
330             // Create a CertStore with all the certificates from the OCSP response
331             CertStore certStore = CertStore.getInstance("Collection",
332                     new CollectionCertStoreParameters(certList));
333 
334             // Set up the target certificate selector for the responder certificate
335             X509CertSelector targetConstraints = new X509CertSelector();
336             targetConstraints.setCertificate(new JcaX509CertificateConverter().getCertificate(responderCert));
337 
338             // Set up trust anchor with the issuer certificate
339             TrustAnchor trustAnchor = new TrustAnchor(issuerCertificate, null);
340 
341             // Build PKIX parameters
342             PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(
343                     Collections.singleton(trustAnchor), targetConstraints);
344             pkixParams.addCertStore(certStore);
345             pkixParams.setRevocationEnabled(false); // Don't check revocation when validating OCSP response
346 
347             // Build and validate the certificate path
348             CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
349             builder.build(pkixParams);
350 
351             // If we reach here, the chain is valid
352         } catch (CertPathBuilderException e) {
353             throw new OCSPException("OCSP responder certificate is not trusted by issuer: " + e.getMessage(), e);
354         } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
355             throw new OCSPException("Error setting up certificate path validation", e);
356         } catch (CertificateException e) {
357             throw new OCSPException("Error converting certificates for path validation", e);
358         }
359     }
360 
361     /**
362      * Parse OCSP endpoint URL from Certificate
363      *
364      * @param cert Certificate to be parsed
365      * @return OCSP endpoint URL
366      * @throws NullPointerException     If we couldn't locate OCSP responder URL
367      * @throws IllegalArgumentException If we couldn't parse X509Certificate into JcaX509CertificateHolder
368      */
369     private static String parseOcspUrlFromCertificate(X509Certificate cert) {
370         X509CertificateHolder holder;
371         try {
372             holder = new JcaX509CertificateHolder(cert);
373         } catch (CertificateEncodingException e) {
374             // Though this should never happen
375             throw new IllegalArgumentException("Error while parsing X509Certificate into JcaX509CertificateHolder", e);
376         }
377 
378         AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(holder.getExtensions());
379 
380         // Lookup for OCSP responder url
381         for (AccessDescription accessDescription : aiaExtension.getAccessDescriptions()) {
382             if (accessDescription.getAccessMethod().equals(id_ad_ocsp)) {
383                 return accessDescription.getAccessLocation().getName().toASN1Primitive().toString();
384             }
385         }
386 
387         throw new NullPointerException("Unable to find OCSP responder URL in Certificate");
388     }
389 
390     static final class Initializer extends ChannelInitializer<SocketChannel> {
391 
392         private final Promise<OCSPResp> responsePromise;
393 
394         Initializer(Promise<OCSPResp> responsePromise) {
395             this.responsePromise = checkNotNull(responsePromise, "ResponsePromise");
396         }
397 
398         @Override
399         protected void initChannel(SocketChannel socketChannel) {
400             ChannelPipeline pipeline = socketChannel.pipeline();
401             pipeline.addLast(new HttpClientCodec());
402             pipeline.addLast(new HttpObjectAggregator(OCSP_RESPONSE_MAX_SIZE));
403             pipeline.addLast(new OcspHttpHandler(responsePromise));
404         }
405     }
406 
407     private OcspClient() {
408         // Prevent outside initialization
409     }
410 }