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