View Javadoc
1   /*
2    * Copyright 2017 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version
5    * 2.0 (the "License"); you may not use this file except in compliance with the
6    * 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 under
14   * the License.
15   */
16  
17  package io.netty5.example.ocsp;
18  
19  import io.netty5.bootstrap.Bootstrap;
20  import io.netty5.channel.Channel;
21  import io.netty5.channel.ChannelFutureListeners;
22  import io.netty5.channel.ChannelHandler;
23  import io.netty5.channel.ChannelHandlerContext;
24  import io.netty5.channel.ChannelInitializer;
25  import io.netty5.channel.ChannelOption;
26  import io.netty5.channel.ChannelPipeline;
27  import io.netty5.channel.EventLoopGroup;
28  import io.netty5.channel.MultithreadEventLoopGroup;
29  import io.netty5.channel.nio.NioHandler;
30  import io.netty5.channel.socket.nio.NioSocketChannel;
31  import io.netty5.handler.codec.http.DefaultFullHttpRequest;
32  import io.netty5.handler.codec.http.FullHttpRequest;
33  import io.netty5.handler.codec.http.FullHttpResponse;
34  import io.netty5.handler.codec.http.HttpClientCodec;
35  import io.netty5.handler.codec.http.HttpHeaderNames;
36  import io.netty5.handler.codec.http.HttpMethod;
37  import io.netty5.handler.codec.http.HttpObjectAggregator;
38  import io.netty5.handler.codec.http.HttpVersion;
39  import io.netty5.handler.ssl.OpenSsl;
40  import io.netty5.handler.ssl.ReferenceCountedOpenSslContext;
41  import io.netty5.handler.ssl.ReferenceCountedOpenSslEngine;
42  import io.netty5.handler.ssl.SslContextBuilder;
43  import io.netty5.handler.ssl.SslHandler;
44  import io.netty5.handler.ssl.SslProvider;
45  import io.netty5.handler.ssl.ocsp.OcspClientHandler;
46  import io.netty5.util.Resource;
47  import io.netty5.util.concurrent.Promise;
48  import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
49  import org.bouncycastle.cert.ocsp.BasicOCSPResp;
50  import org.bouncycastle.cert.ocsp.CertificateStatus;
51  import org.bouncycastle.cert.ocsp.OCSPResp;
52  import org.bouncycastle.cert.ocsp.SingleResp;
53  
54  import javax.net.ssl.SSLSession;
55  import javax.security.cert.X509Certificate;
56  import java.math.BigInteger;
57  
58  /**
59   * This is a very simple example for an HTTPS client that uses OCSP stapling.
60   * The client connects to an HTTPS server that has OCSP stapling enabled and
61   * then uses BC to parse and validate it.
62   */
63  public class OcspClientExample {
64      public static void main(String[] args) throws Exception {
65          if (!OpenSsl.isAvailable()) {
66              throw new IllegalStateException("OpenSSL is not available!");
67          }
68  
69          if (!OpenSsl.isOcspSupported()) {
70              throw new IllegalStateException("OCSP is not supported!");
71          }
72  
73          // Using Wikipedia as an example. I'd rather use Netty's own website
74          // but the server (Cloudflare) doesn't support OCSP stapling. A few
75          // other examples could be Microsoft or Squarespace. Use OpenSSL's
76          // CLI client to assess if a server supports OCSP stapling. E.g.:
77          //
78          // openssl s_client -tlsextdebug -status -connect www.squarespace.com:443
79          //
80          String host = "www.wikipedia.org";
81  
82          ReferenceCountedOpenSslContext context
83              = (ReferenceCountedOpenSslContext) SslContextBuilder.forClient()
84                  .sslProvider(SslProvider.OPENSSL)
85                  .enableOcsp(true)
86                  .build();
87  
88          try {
89              EventLoopGroup group = new MultithreadEventLoopGroup(NioHandler.newFactory());
90              try {
91                  Promise<FullHttpResponse> promise = group.next().newPromise();
92  
93                  Bootstrap bootstrap = new Bootstrap()
94                          .channel(NioSocketChannel.class)
95                          .group(group)
96                          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000)
97                          .handler(newClientHandler(context, host, promise));
98  
99                  Channel channel = bootstrap.connect(host, 443).asStage().get();
100 
101                 try {
102                     FullHttpResponse response = promise.asFuture().asStage().get();
103                     Resource.dispose(response);
104                 } finally {
105                     channel.close();
106                 }
107             } finally {
108                 group.shutdownGracefully();
109             }
110         } finally {
111             context.release();
112         }
113     }
114 
115     private static ChannelInitializer<Channel> newClientHandler(final ReferenceCountedOpenSslContext context,
116             final String host, final Promise<FullHttpResponse> promise) {
117 
118         return new ChannelInitializer<>() {
119             @Override
120             protected void initChannel(Channel ch) throws Exception {
121                 SslHandler sslHandler = context.newHandler(ch.bufferAllocator());
122                 ReferenceCountedOpenSslEngine engine =
123                         (ReferenceCountedOpenSslEngine) sslHandler.engine();
124 
125                 ChannelPipeline pipeline = ch.pipeline();
126                 pipeline.addLast(sslHandler);
127                 pipeline.addLast(new ExampleOcspClientHandler(engine));
128 
129                 pipeline.addLast(new HttpClientCodec());
130                 pipeline.addLast(new HttpObjectAggregator(1024 * 1024));
131                 pipeline.addLast(new HttpClientHandler(host, promise));
132             }
133 
134             @Override
135             public void channelInactive(ChannelHandlerContext ctx) throws Exception {
136                 if (!promise.isDone()) {
137                     promise.tryFailure(new IllegalStateException("Connection closed and Promise was not done."));
138                 }
139                 ctx.fireChannelInactive();
140             }
141 
142             @Override
143             public void channelExceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
144                 if (!promise.tryFailure(cause)) {
145                     ctx.fireChannelExceptionCaught(cause);
146                 }
147             }
148         };
149     }
150 
151     private static class HttpClientHandler implements ChannelHandler {
152 
153         private final String host;
154 
155         private final Promise<FullHttpResponse> promise;
156 
157         HttpClientHandler(String host, Promise<FullHttpResponse> promise) {
158             this.host = host;
159             this.promise = promise;
160         }
161 
162         @Override
163         public void channelActive(ChannelHandlerContext ctx) throws Exception {
164             FullHttpRequest request = new DefaultFullHttpRequest(
165                     HttpVersion.HTTP_1_1, HttpMethod.GET, "/", ctx.bufferAllocator().allocate(0));
166             request.headers().set(HttpHeaderNames.HOST, host);
167             request.headers().set(HttpHeaderNames.USER_AGENT, "netty-ocsp-example/1.0");
168 
169             ctx.writeAndFlush(request).addListener(ctx.channel(), ChannelFutureListeners.FIRE_EXCEPTION_ON_FAILURE);
170 
171             ctx.fireChannelActive();
172         }
173 
174         @Override
175         public void channelInactive(ChannelHandlerContext ctx) throws Exception {
176             if (!promise.isDone()) {
177                 promise.tryFailure(new IllegalStateException("Connection closed and Promise was not done."));
178             }
179             ctx.fireChannelInactive();
180         }
181 
182         @Override
183         public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
184             if (msg instanceof FullHttpResponse) {
185                 if (!promise.trySuccess((FullHttpResponse) msg)) {
186                     Resource.dispose(msg);
187                 }
188                 return;
189             }
190 
191             ctx.fireChannelRead(msg);
192         }
193 
194         @Override
195         public void channelExceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
196             if (!promise.tryFailure(cause)) {
197                 ctx.fireChannelExceptionCaught(cause);
198             }
199         }
200     }
201 
202     private static class ExampleOcspClientHandler extends OcspClientHandler {
203 
204         ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) {
205             super(engine);
206         }
207 
208         @Override
209         protected boolean verify(ChannelHandlerContext ctx, ReferenceCountedOpenSslEngine engine) throws Exception {
210             byte[] staple = engine.getOcspResponse();
211             if (staple == null) {
212                 throw new IllegalStateException("Server didn't provide an OCSP staple!");
213             }
214 
215             OCSPResp response = new OCSPResp(staple);
216             if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
217                 return false;
218             }
219 
220             SSLSession session = engine.getSession();
221             X509Certificate[] chain = session.getPeerCertificateChain();
222             BigInteger certSerial = chain[0].getSerialNumber();
223 
224             BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject();
225             SingleResp first = basicResponse.getResponses()[0];
226 
227             // ATTENTION: CertificateStatus.GOOD is actually a null value! Do not use
228             // equals() or you'll NPE!
229             CertificateStatus status = first.getCertStatus();
230             BigInteger ocspSerial = first.getCertID().getSerialNumber();
231             String message = new StringBuilder()
232                 .append("OCSP status of ").append(ctx.channel().remoteAddress())
233                 .append("\n  Status: ").append(status == CertificateStatus.GOOD ? "Good" : status)
234                 .append("\n  This Update: ").append(first.getThisUpdate())
235                 .append("\n  Next Update: ").append(first.getNextUpdate())
236                 .append("\n  Cert Serial: ").append(certSerial)
237                 .append("\n  OCSP Serial: ").append(ocspSerial)
238                 .toString();
239             System.out.println(message);
240 
241             return status == CertificateStatus.GOOD && certSerial.equals(ocspSerial);
242         }
243     }
244 }