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.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      * Query the certificate status using OCSP
102      *
103      * @param x509Certificate       Client {@link X509Certificate} to validate
104      * @param issuer                {@link X509Certificate} issuer of client certificate
105      * @param validateResponseNonce Set to {@code true} to enable OCSP response validation
106      * @param ioTransport           {@link IoTransport} to use
107      * @return                      {@link Promise} of {@link BasicOCSPResp}
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                     // Initialize OCSP Request Builder and add CertificateID into it.
126                     OCSPReqBuilder builder = new OCSPReqBuilder();
127                     builder.addRequest(certificateID);
128 
129                     // Generate 16-bytes (octets) of nonce and add it into OCSP Request builder.
130                     // Because as per RFC-8954#2.1:
131                     //
132                     //   OCSP responders MUST accept lengths of at least
133                     //   16 octets and MAY choose to ignore the Nonce extension for requests
134                     //   where the length of the nonce is less than 16 octets.
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                     // Get OCSP URL from Certificate and query it.
141                     URL uri = new URL(parseOcspUrlFromCertificate(x509Certificate));
142 
143                     // Find port
144                     int port = uri.getPort();
145                     if (port == -1) {
146                         port = uri.getDefaultPort();
147                     }
148 
149                     // Configure path
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                     // Validate OCSP response
164                     ocspResponsePromise.addListener((GenericFutureListener<Future<OCSPResp>>) future -> {
165                         // If Future was successful then we have received OCSP response
166                         // We will now validate it.
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      * Query the OCSP responder for certificate status using HTTP/1.1
195      *
196      * @param eventLoop   {@link EventLoop} for HTTP request execution
197      * @param ocspRequest {@link ByteBuf} containing OCSP request data
198      * @param host        OCSP responder hostname
199      * @param port        OCSP responder port
200      * @param path        OCSP responder path
201      * @param ioTransport {@link IoTransport} to use
202      * @return Returns {@link Promise} containing {@link OCSPResp}
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                 // If Future was successful then we have successfully resolved OCSP server address.
219                 // If not, mark 'responsePromise' as failure.
220                 if (future.isSuccess()) {
221                     // Get the resolved InetAddress
222                     InetAddress hostAddress = future.getNow();
223                     final ChannelFuture channelFuture = bootstrap.connect(hostAddress, port);
224                     channelFuture.addListener(f -> {
225                         // If Future was successful then connection to OCSP responder was successful.
226                         // We will send a OCSP request now
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                             // Send the OCSP HTTP Request
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             // Validate number of responses. We only requested for 1 certificate
260             // so number of responses must be 1. If not, we will throw an error.
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      * Validate OCSP response nonce
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      * Validate OCSP response signature
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             // If responder certificate is included, validate the chain
310             if (certs != null && certs.length > 0) {
311 
312                 // Use the first included certificate to verify the OCSP response signature.
313                 X509CertificateHolder responderCert = certs[0];
314 
315                 // Verify OCSP response signature using responder cert
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                 // Build chain from responder certificate to issuer using CertPathBuilder
323                 validateCertificateChain(responderCert, certs, issuerCertificate);
324             } else {
325                 // Validate signature using issuer certificate
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      * Validates that a certificate chain can be built from the responder certificate to the issuer.
341      * Uses Java's CertPathBuilder to construct and validate the chain.
342      */
343     private static void validateCertificateChain(X509CertificateHolder responderCert,
344                                                    X509CertificateHolder[] allCerts,
345                                                    X509Certificate issuerCertificate) throws OCSPException {
346         try {
347             // Convert BouncyCastle certificate holders to Java X509Certificates
348             List<X509Certificate> certList = new ArrayList<>(allCerts.length);
349             for (X509CertificateHolder certHolder : allCerts) {
350                 certList.add(new JcaX509CertificateConverter().getCertificate(certHolder));
351             }
352 
353             // Create a CertStore with all the certificates from the OCSP response
354             CertStore certStore = CertStore.getInstance("Collection",
355                     new CollectionCertStoreParameters(certList));
356 
357             // Set up the target certificate selector for the responder certificate
358             X509CertSelector targetConstraints = new X509CertSelector();
359             targetConstraints.setCertificate(new JcaX509CertificateConverter().getCertificate(responderCert));
360 
361             // Set up trust anchor with the issuer certificate
362             TrustAnchor trustAnchor = new TrustAnchor(issuerCertificate, null);
363 
364             // Build PKIX parameters
365             PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(
366                     Collections.singleton(trustAnchor), targetConstraints);
367             pkixParams.addCertStore(certStore);
368             pkixParams.setRevocationEnabled(false); // Don't check revocation when validating OCSP response
369 
370             // Build and validate the certificate path
371             CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
372             builder.build(pkixParams);
373 
374             // If we reach here, the chain is valid
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      * Parse OCSP endpoint URL from Certificate
386      *
387      * @param cert Certificate to be parsed
388      * @return OCSP endpoint URL
389      * @throws NullPointerException     If we couldn't locate OCSP responder URL
390      * @throws IllegalArgumentException If we couldn't parse X509Certificate into JcaX509CertificateHolder
391      */
392     private static String parseOcspUrlFromCertificate(X509Certificate cert) {
393         X509CertificateHolder holder;
394         try {
395             holder = new JcaX509CertificateHolder(cert);
396         } catch (CertificateEncodingException e) {
397             // Though this should never happen
398             throw new IllegalArgumentException("Error while parsing X509Certificate into JcaX509CertificateHolder", e);
399         }
400 
401         AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(holder.getExtensions());
402 
403         // Lookup for OCSP responder url
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         // Prevent outside initialization
434     }
435 }