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.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.JcaX509CertificateHolder;
48  import org.bouncycastle.cert.ocsp.BasicOCSPResp;
49  import org.bouncycastle.cert.ocsp.CertificateID;
50  import org.bouncycastle.cert.ocsp.OCSPException;
51  import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
52  import org.bouncycastle.cert.ocsp.OCSPResp;
53  import org.bouncycastle.operator.ContentVerifierProvider;
54  import org.bouncycastle.operator.OperatorCreationException;
55  import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
56  import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
57  
58  import java.net.InetAddress;
59  import java.net.URL;
60  import java.security.SecureRandom;
61  import java.security.cert.CertificateEncodingException;
62  import java.security.cert.X509Certificate;
63  
64  import static io.netty.handler.codec.http.HttpMethod.POST;
65  import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
66  import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_REQUEST_TYPE;
67  import static io.netty.handler.ssl.ocsp.OcspHttpHandler.OCSP_RESPONSE_TYPE;
68  import static io.netty.util.internal.ObjectUtil.checkNotNull;
69  import static org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers.id_pkix_ocsp_nonce;
70  import static org.bouncycastle.asn1.x509.X509ObjectIdentifiers.id_ad_ocsp;
71  import static org.bouncycastle.cert.ocsp.CertificateID.HASH_SHA1;
72  
73  final class OcspClient {
74  
75      private static final InternalLogger logger = InternalLoggerFactory.getInstance(OcspClient.class);
76  
77      private static final SecureRandom SECURE_RANDOM = new SecureRandom();
78      private static final int OCSP_RESPONSE_MAX_SIZE = SystemPropertyUtil.getInt(
79              "io.netty.ocsp.responseSize", 1024 * 10);
80  
81      static {
82          logger.debug("-Dio.netty.ocsp.responseSize: {} bytes", OCSP_RESPONSE_MAX_SIZE);
83      }
84  
85      /**
86       * Query the certificate status using OCSP
87       *
88       * @param x509Certificate       Client {@link X509Certificate} to validate
89       * @param issuer                {@link X509Certificate} issuer of client certificate
90       * @param validateResponseNonce Set to {@code true} to enable OCSP response validation
91       * @param ioTransport           {@link IoTransport} to use
92       * @return {@link Promise} of {@link BasicOCSPResp}
93       */
94      static Promise<BasicOCSPResp> query(final X509Certificate x509Certificate,
95                                          final X509Certificate issuer, final boolean validateResponseNonce,
96                                          final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
97          final EventLoop eventLoop = ioTransport.eventLoop();
98          final Promise<BasicOCSPResp> responsePromise = eventLoop.newPromise();
99          eventLoop.execute(new Runnable() {
100             @Override
101             public void run() {
102                 try {
103                     CertificateID certificateID = new CertificateID(new JcaDigestCalculatorProviderBuilder()
104                             .build().get(HASH_SHA1), new JcaX509CertificateHolder(issuer),
105                             x509Certificate.getSerialNumber());
106 
107                     // Initialize OCSP Request Builder and add CertificateID into it.
108                     OCSPReqBuilder builder = new OCSPReqBuilder();
109                     builder.addRequest(certificateID);
110 
111                     // Generate 16-bytes (octets) of nonce and add it into OCSP Request builder.
112                     // Because as per RFC-8954#2.1:
113                     //
114                     //   OCSP responders MUST accept lengths of at least
115                     //   16 octets and MAY choose to ignore the Nonce extension for requests
116                     //   where the length of the nonce is less than 16 octets.
117                     byte[] nonce = new byte[16];
118                     SECURE_RANDOM.nextBytes(nonce);
119                     final DEROctetString derNonce = new DEROctetString(nonce);
120                     builder.setRequestExtensions(new Extensions(new Extension(id_pkix_ocsp_nonce, false, derNonce)));
121 
122                     // Get OCSP URL from Certificate and query it.
123                     URL uri = new URL(parseOcspUrlFromCertificate(x509Certificate));
124 
125                     // Find port
126                     int port = uri.getPort();
127                     if (port == -1) {
128                         port = uri.getDefaultPort();
129                     }
130 
131                     // Configure path
132                     String path = uri.getPath();
133                     if (path.isEmpty()) {
134                         path = "/";
135                     } else {
136                         if (uri.getQuery() != null) {
137                             path = path + '?' + uri.getQuery();
138                         }
139                     }
140 
141                     Promise<OCSPResp> ocspResponsePromise = query(eventLoop,
142                             Unpooled.wrappedBuffer(builder.build().getEncoded()),
143                             uri.getHost(), port, path, ioTransport, dnsNameResolver);
144 
145                     // Validate OCSP response
146                     ocspResponsePromise.addListener(new GenericFutureListener<Future<OCSPResp>>() {
147                         @Override
148                         public void operationComplete(Future<OCSPResp> future) throws Exception {
149                             // If Future was successful then we have received OCSP response
150                             // We will now validate it.
151                             if (future.isSuccess()) {
152                                 BasicOCSPResp resp = (BasicOCSPResp) future.get().getResponseObject();
153                                 validateResponse(responsePromise, resp, derNonce, issuer, validateResponseNonce);
154                             } else {
155                                 responsePromise.tryFailure(future.cause());
156                             }
157                         }
158                     });
159 
160                 } catch (Exception ex) {
161                     responsePromise.tryFailure(ex);
162                 }
163             }
164         });
165         return responsePromise;
166     }
167 
168     /**
169      * Query the OCSP responder for certificate status using HTTP/1.1
170      *
171      * @param eventLoop   {@link EventLoop} for HTTP request execution
172      * @param ocspRequest {@link ByteBuf} containing OCSP request data
173      * @param host        OCSP responder hostname
174      * @param port        OCSP responder port
175      * @param path        OCSP responder path
176      * @param ioTransport {@link IoTransport} to use
177      * @return Returns {@link Promise} containing {@link OCSPResp}
178      */
179     private static Promise<OCSPResp> query(final EventLoop eventLoop, final ByteBuf ocspRequest,
180                                            final String host, final int port, final String path,
181                                            final IoTransport ioTransport, final DnsNameResolver dnsNameResolver) {
182         final Promise<OCSPResp> responsePromise = eventLoop.newPromise();
183 
184         try {
185             final Bootstrap bootstrap = new Bootstrap()
186                     .group(ioTransport.eventLoop())
187                     .option(ChannelOption.TCP_NODELAY, true)
188                     .channelFactory(ioTransport.socketChannel())
189                     .attr(OcspServerCertificateValidator.OCSP_PIPELINE_ATTRIBUTE, Boolean.TRUE)
190                     .handler(new Initializer(responsePromise));
191 
192             dnsNameResolver.resolve(host).addListener(new FutureListener<InetAddress>() {
193                 @Override
194                 public void operationComplete(Future<InetAddress> future) throws Exception {
195 
196                     // If Future was successful then we have successfully resolved OCSP server address.
197                     // If not, mark 'responsePromise' as failure.
198                     if (future.isSuccess()) {
199                         // Get the resolved InetAddress
200                         InetAddress hostAddress = future.get();
201                         final ChannelFuture channelFuture = bootstrap.connect(hostAddress, port);
202                         channelFuture.addListener(new ChannelFutureListener() {
203                             @Override
204                             public void operationComplete(ChannelFuture future) {
205                                 // If Future was successful then connection to OCSP responder was successful.
206                                 // We will send a OCSP request now
207                                 if (future.isSuccess()) {
208                                     FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, path,
209                                             ocspRequest);
210                                     request.headers().add(HttpHeaderNames.HOST, host);
211                                     request.headers().add(HttpHeaderNames.USER_AGENT, "Netty OCSP Client");
212                                     request.headers().add(HttpHeaderNames.CONTENT_TYPE, OCSP_REQUEST_TYPE);
213                                     request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, OCSP_RESPONSE_TYPE);
214                                     request.headers().add(HttpHeaderNames.CONTENT_LENGTH, ocspRequest.readableBytes());
215 
216                                     // Send the OCSP HTTP Request
217                                     channelFuture.channel().writeAndFlush(request);
218                                 } else {
219                                     responsePromise.tryFailure(new IllegalStateException(
220                                             "Connection to OCSP Responder Failed", future.cause()));
221                                 }
222                             }
223                         });
224                     } else {
225                         responsePromise.tryFailure(future.cause());
226                     }
227                 }
228             });
229         } catch (Exception ex) {
230             responsePromise.tryFailure(ex);
231         }
232 
233         return responsePromise;
234     }
235 
236     private static void validateResponse(Promise<BasicOCSPResp> responsePromise, BasicOCSPResp basicResponse,
237                                          DEROctetString derNonce, X509Certificate issuer, boolean validateNonce) {
238         try {
239             // Validate number of responses. We only requested for 1 certificate
240             // so number of responses must be 1. If not, we will throw an error.
241             int responses = basicResponse.getResponses().length;
242             if (responses != 1) {
243                 throw new IllegalArgumentException("Expected number of responses was 1 but got: " + responses);
244             }
245 
246             if (validateNonce) {
247                 validateNonce(basicResponse, derNonce);
248             }
249             validateSignature(basicResponse, issuer);
250             responsePromise.trySuccess(basicResponse);
251         } catch (Exception ex) {
252             responsePromise.tryFailure(ex);
253         }
254     }
255 
256     /**
257      * Validate OCSP response nonce
258      */
259     private static void validateNonce(BasicOCSPResp basicResponse, DEROctetString encodedNonce) throws OCSPException {
260         Extension nonceExt = basicResponse.getExtension(id_pkix_ocsp_nonce);
261         if (nonceExt != null) {
262             DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue();
263             if (!responseNonceString.equals(encodedNonce)) {
264                 throw new OCSPException("Nonce does not match");
265             }
266         } else {
267             throw new IllegalArgumentException("Nonce is not present");
268         }
269     }
270 
271     /**
272      * Validate OCSP response signature
273      */
274     private static void validateSignature(BasicOCSPResp resp, X509Certificate certificate) throws OCSPException {
275         try {
276             ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder().build(certificate);
277             if (!resp.isSignatureValid(verifier)) {
278                 throw new OCSPException("OCSP signature is not valid");
279             }
280         } catch (OperatorCreationException e) {
281             throw new OCSPException("Error validating OCSP-Signature", e);
282         }
283     }
284 
285     /**
286      * Parse OCSP endpoint URL from Certificate
287      *
288      * @param cert Certificate to be parsed
289      * @return OCSP endpoint URL
290      * @throws NullPointerException     If we couldn't locate OCSP responder URL
291      * @throws IllegalArgumentException If we couldn't parse X509Certificate into JcaX509CertificateHolder
292      */
293     private static String parseOcspUrlFromCertificate(X509Certificate cert) {
294         X509CertificateHolder holder;
295         try {
296             holder = new JcaX509CertificateHolder(cert);
297         } catch (CertificateEncodingException e) {
298             // Though this should never happen
299             throw new IllegalArgumentException("Error while parsing X509Certificate into JcaX509CertificateHolder", e);
300         }
301 
302         AuthorityInformationAccess aiaExtension = AuthorityInformationAccess.fromExtensions(holder.getExtensions());
303 
304         // Lookup for OCSP responder url
305         for (AccessDescription accessDescription : aiaExtension.getAccessDescriptions()) {
306             if (accessDescription.getAccessMethod().equals(id_ad_ocsp)) {
307                 return accessDescription.getAccessLocation().getName().toASN1Primitive().toString();
308             }
309         }
310 
311         throw new NullPointerException("Unable to find OCSP responder URL in Certificate");
312     }
313 
314     static final class Initializer extends ChannelInitializer<SocketChannel> {
315 
316         private final Promise<OCSPResp> responsePromise;
317 
318         Initializer(Promise<OCSPResp> responsePromise) {
319             this.responsePromise = checkNotNull(responsePromise, "ResponsePromise");
320         }
321 
322         @Override
323         protected void initChannel(SocketChannel socketChannel) {
324             ChannelPipeline pipeline = socketChannel.pipeline();
325             pipeline.addLast(new HttpClientCodec());
326             pipeline.addLast(new HttpObjectAggregator(OCSP_RESPONSE_MAX_SIZE));
327             pipeline.addLast(new OcspHttpHandler(responsePromise));
328         }
329     }
330 
331     private OcspClient() {
332         // Prevent outside initialization
333     }
334 }