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