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