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