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