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.channel.ChannelHandlerContext;
19 import io.netty.channel.ChannelInboundHandlerAdapter;
20 import io.netty.handler.ssl.SslHandler;
21 import io.netty.handler.ssl.SslHandshakeCompletionEvent;
22 import io.netty.resolver.dns.DnsNameResolver;
23 import io.netty.resolver.dns.DnsNameResolverBuilder;
24 import io.netty.util.AttributeKey;
25 import io.netty.util.concurrent.Future;
26 import io.netty.util.concurrent.GenericFutureListener;
27 import io.netty.util.concurrent.Promise;
28 import org.bouncycastle.cert.ocsp.BasicOCSPResp;
29 import org.bouncycastle.cert.ocsp.OCSPException;
30 import org.bouncycastle.cert.ocsp.RevokedStatus;
31 import org.bouncycastle.cert.ocsp.SingleResp;
32
33 import java.security.cert.Certificate;
34 import java.security.cert.X509Certificate;
35 import java.util.Date;
36
37 import static io.netty.util.internal.ObjectUtil.checkNotNull;
38
39 /**
40 * {@link OcspServerCertificateValidator} validates incoming server's certificate
41 * using OCSP. Once TLS handshake is completed, {@link SslHandshakeCompletionEvent#SUCCESS} is fired, validator
42 * will perform certificate validation using OCSP over HTTP/1.1 with the server's certificate issuer OCSP responder.
43 */
44 public class OcspServerCertificateValidator extends ChannelInboundHandlerAdapter {
45 /**
46 * An attribute used to mark all channels created by the {@link OcspServerCertificateValidator}.
47 */
48 public static final AttributeKey<Boolean> OCSP_PIPELINE_ATTRIBUTE =
49 AttributeKey.newInstance("io.netty.handler.ssl.ocsp.pipeline");
50
51 private final boolean closeAndThrowIfNotValid;
52 private final boolean validateNonce;
53 private final IoTransport ioTransport;
54 private final DnsNameResolver dnsNameResolver;
55
56 /**
57 * Create a new {@link OcspServerCertificateValidator} instance without nonce validation
58 * on OCSP response, using default {@link IoTransport#DEFAULT} instance,
59 * default {@link DnsNameResolver} implementation and with {@link #closeAndThrowIfNotValid}
60 * set to {@code true}
61 */
62 public OcspServerCertificateValidator() {
63 this(false);
64 }
65
66 /**
67 * Create a new {@link OcspServerCertificateValidator} instance with
68 * default {@link IoTransport#DEFAULT} instance and default {@link DnsNameResolver} implementation
69 * and {@link #closeAndThrowIfNotValid} set to {@code true}.
70 *
71 * @param validateNonce Set to {@code true} if we should force nonce validation on
72 * OCSP response else set to {@code false}
73 */
74 public OcspServerCertificateValidator(boolean validateNonce) {
75 this(validateNonce, IoTransport.DEFAULT);
76 }
77
78 /**
79 * Create a new {@link OcspServerCertificateValidator} instance
80 *
81 * @param validateNonce Set to {@code true} if we should force nonce validation on
82 * OCSP response else set to {@code false}
83 * @param ioTransport {@link IoTransport} to use
84 */
85 public OcspServerCertificateValidator(boolean validateNonce, IoTransport ioTransport) {
86 this(validateNonce, ioTransport, createDefaultResolver(ioTransport));
87 }
88
89 /**
90 * Create a new {@link IoTransport} instance with {@link #closeAndThrowIfNotValid} set to {@code true}
91 *
92 * @param validateNonce Set to {@code true} if we should force nonce validation on
93 * OCSP response else set to {@code false}
94 * @param ioTransport {@link IoTransport} to use
95 * @param dnsNameResolver {@link DnsNameResolver} implementation to use
96 */
97 public OcspServerCertificateValidator(boolean validateNonce, IoTransport ioTransport,
98 DnsNameResolver dnsNameResolver) {
99 this(true, validateNonce, ioTransport, dnsNameResolver);
100 }
101
102 /**
103 * Create a new {@link IoTransport} instance
104 *
105 * @param closeAndThrowIfNotValid If set to {@code true} then we will close the channel and throw an exception
106 * when certificate is not {@link OcspResponse.Status#VALID}.
107 * If set to {@code false} then we will simply pass the {@link OcspValidationEvent}
108 * to the next handler in pipeline and let it decide what to do.
109 * @param validateNonce Set to {@code true} if we should force nonce validation on
110 * OCSP response else set to {@code false}
111 * @param ioTransport {@link IoTransport} to use
112 * @param dnsNameResolver {@link DnsNameResolver} implementation to use
113 */
114 public OcspServerCertificateValidator(boolean closeAndThrowIfNotValid, boolean validateNonce,
115 IoTransport ioTransport, DnsNameResolver dnsNameResolver) {
116 this.closeAndThrowIfNotValid = closeAndThrowIfNotValid;
117 this.validateNonce = validateNonce;
118 this.ioTransport = checkNotNull(ioTransport, "IoTransport");
119 this.dnsNameResolver = checkNotNull(dnsNameResolver, "DnsNameResolver");
120 }
121
122 protected static DnsNameResolver createDefaultResolver(final IoTransport ioTransport) {
123 return new DnsNameResolverBuilder()
124 .eventLoop(ioTransport.eventLoop())
125 .datagramChannelFactory(ioTransport.datagramChannel())
126 .socketChannelFactory(ioTransport.socketChannel())
127 .build();
128 }
129
130 @Override
131 public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception {
132 ctx.fireUserEventTriggered(evt);
133
134 if (evt instanceof SslHandshakeCompletionEvent) {
135 SslHandshakeCompletionEvent sslHandshakeCompletionEvent = (SslHandshakeCompletionEvent) evt;
136
137 // If TLS handshake was successful then only we will perform OCSP certificate validation.
138 // If not, then just forward the event to next handler in pipeline and remove ourselves from pipeline.
139 if (sslHandshakeCompletionEvent.isSuccess()) {
140 Certificate[] certificates = ctx.pipeline().get(SslHandler.class)
141 .engine()
142 .getSession()
143 .getPeerCertificates();
144
145 assert certificates.length >= 2 : "There must an end-entity certificate and issuer certificate";
146
147 Promise<BasicOCSPResp> ocspRespPromise = OcspClient.query((X509Certificate) certificates[0],
148 (X509Certificate) certificates[1], validateNonce, ioTransport, dnsNameResolver);
149
150 ocspRespPromise.addListener(new GenericFutureListener<Future<BasicOCSPResp>>() {
151 @Override
152 public void operationComplete(Future<BasicOCSPResp> future) throws Exception {
153 // If Future is success then we have successfully received OCSP response
154 // from OCSP responder. We will validate it now and process.
155 if (future.isSuccess()) {
156 SingleResp response = future.get().getResponses()[0];
157
158 Date current = new Date();
159 if (!(current.after(response.getThisUpdate()) &&
160 current.before(response.getNextUpdate()))) {
161 ctx.fireExceptionCaught(new IllegalStateException("OCSP Response is out-of-date"));
162 }
163
164 OcspResponse.Status status;
165 if (response.getCertStatus() == null) {
166 // 'null' means certificate is valid
167 status = OcspResponse.Status.VALID;
168 } else if (response.getCertStatus() instanceof RevokedStatus) {
169 status = OcspResponse.Status.REVOKED;
170 } else {
171 status = OcspResponse.Status.UNKNOWN;
172 }
173
174 ctx.fireUserEventTriggered(new OcspValidationEvent(
175 new OcspResponse(status, response.getThisUpdate(), response.getNextUpdate())));
176
177 // If Certificate is not VALID and 'closeAndThrowIfNotValid' is set
178 // to 'true' then close the channel and throw an exception.
179 if (status != OcspResponse.Status.VALID && closeAndThrowIfNotValid) {
180 ctx.channel().close();
181 // Certificate is not valid. Throw
182 ctx.fireExceptionCaught(new OCSPException(
183 "Certificate not valid. Status: " + status));
184 }
185 } else {
186 ctx.fireExceptionCaught(future.cause());
187 }
188 }
189 });
190 }
191 // Lets remove ourselves from the pipeline because we are done processing validation.
192 ctx.pipeline().remove(this);
193 }
194 }
195
196 @Override
197 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
198 ctx.channel().close();
199 }
200 }