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