From 077a1988b9c96b5f3e5693a3bb1b2e118992f3a8 Mon Sep 17 00:00:00 2001 From: Roger Kapsi Date: Mon, 26 Dec 2016 18:04:56 -0500 Subject: [PATCH] OCSP stapling support for Netty using netty-tcnative. https://github.com/netty/netty-tcnative/pull/215 Motivation OCSP stapling (formally known as TLS Certificate Status Request extension) is alternative approach for checking the revocation status of X.509 Certificates. Servers can preemptively fetch the OCSP response from the CA's responder, cache it for some period of time, and pass it along during (a.k.a. staple) the TLS handshake. The client no longer has to reach out on its own to the CA to check the validity of a cetitficate. Some of the key benefits are: 1) Speed. The client doesn't have to crosscheck the certificate. 2) Efficiency. The Internet is no longer DDoS'ing the CA's OCSP responder servers. 3) Safety. Less operational dependence on the CA. Certificate owners can sustain short CA outages. 4) Privacy. The CA can lo longer track the users of a certificate. https://en.wikipedia.org/wiki/OCSP_stapling https://letsencrypt.org/2016/10/24/squarespace-ocsp-impl.html Modifications https://www.openssl.org/docs/man1.0.2/ssl/SSL_set_tlsext_status_type.html Result High-level API to enable OCSP stapling --- example/pom.xml | 10 + .../java/io/netty/example/ocsp/Digester.java | 81 +++ .../netty/example/ocsp/OcspClientExample.java | 246 +++++++++ .../example/ocsp/OcspRequestBuilder.java | 104 ++++ .../netty/example/ocsp/OcspServerExample.java | 209 ++++++++ .../java/io/netty/example/ocsp/OcspUtils.java | 182 +++++++ .../io/netty/example/ocsp/README.txt | 8 + .../io/netty/example/ocsp/netty_io_chain.pem | 63 +++ .../java/io/netty/handler/ssl/OpenSsl.java | 7 + .../handler/ssl/OpenSslClientContext.java | 7 +- .../io/netty/handler/ssl/OpenSslContext.java | 9 +- .../handler/ssl/OpenSslServerContext.java | 15 +- .../ReferenceCountedOpenSslClientContext.java | 6 +- .../ssl/ReferenceCountedOpenSslContext.java | 23 +- .../ssl/ReferenceCountedOpenSslEngine.java | 43 ++ .../ReferenceCountedOpenSslServerContext.java | 13 +- .../java/io/netty/handler/ssl/SslContext.java | 26 +- .../netty/handler/ssl/SslContextBuilder.java | 21 +- .../handler/ssl/ocsp/OcspClientHandler.java | 65 +++ .../netty/handler/ssl/ocsp/package-info.java | 23 + .../io/netty/handler/ssl/ocsp/OcspTest.java | 501 ++++++++++++++++++ pom.xml | 12 + 22 files changed, 1636 insertions(+), 38 deletions(-) create mode 100644 example/src/main/java/io/netty/example/ocsp/Digester.java create mode 100644 example/src/main/java/io/netty/example/ocsp/OcspClientExample.java create mode 100644 example/src/main/java/io/netty/example/ocsp/OcspRequestBuilder.java create mode 100644 example/src/main/java/io/netty/example/ocsp/OcspServerExample.java create mode 100644 example/src/main/java/io/netty/example/ocsp/OcspUtils.java create mode 100644 example/src/main/resources/io/netty/example/ocsp/README.txt create mode 100644 example/src/main/resources/io/netty/example/ocsp/netty_io_chain.pem create mode 100644 handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ssl/ocsp/package-info.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java diff --git a/example/pom.xml b/example/pom.xml index 2028390060..e483832c54 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -129,6 +129,16 @@ logback-classic runtime + + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + diff --git a/example/src/main/java/io/netty/example/ocsp/Digester.java b/example/src/main/java/io/netty/example/ocsp/Digester.java new file mode 100644 index 0000000000..fb1c6aa645 --- /dev/null +++ b/example/src/main/java/io/netty/example/ocsp/Digester.java @@ -0,0 +1,81 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version + * 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.netty.example.ocsp; + +import java.io.OutputStream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.cert.ocsp.OCSPReqBuilder; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.io.DigestOutputStream; +import org.bouncycastle.operator.DigestCalculator; + +/** + * BC's {@link OCSPReqBuilder} needs a {@link DigestCalculator} but BC doesn't + * provide any public implementations of that interface. That's why we need to + * write our own. There's a default SHA-1 implementation and one for SHA-256. + * Which one to use will depend on the Certificate Authority (CA). + */ +public final class Digester implements DigestCalculator { + + public static DigestCalculator sha1() { + Digest digest = new SHA1Digest(); + AlgorithmIdentifier algId = new AlgorithmIdentifier( + OIWObjectIdentifiers.idSHA1); + + return new Digester(digest, algId); + } + + public static DigestCalculator sha256() { + Digest digest = new SHA256Digest(); + + // The OID for SHA-256: http://www.oid-info.com/get/2.16.840.1.101.3.4.2.1 + ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier( + "2.16.840.1.101.3.4.2.1").intern(); + AlgorithmIdentifier algId = new AlgorithmIdentifier(oid); + + return new Digester(digest, algId); + } + + private final DigestOutputStream dos; + + private final AlgorithmIdentifier algId; + + private Digester(Digest digest, AlgorithmIdentifier algId) { + this.dos = new DigestOutputStream(digest); + this.algId = algId; + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return algId; + } + + @Override + public OutputStream getOutputStream() { + return dos; + } + + @Override + public byte[] getDigest() { + return dos.getDigest(); + } +} diff --git a/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java new file mode 100644 index 0000000000..f36001cab4 --- /dev/null +++ b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java @@ -0,0 +1,246 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version + * 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.netty.example.ocsp; + +import java.math.BigInteger; + +import javax.net.ssl.SSLSession; +import javax.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.SingleResp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.ReferenceCountedOpenSslContext; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.ocsp.OcspClientHandler; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; + +/** + * This is a very simple example for a HTTPS client that uses OCSP stapling. + * The client connects to a HTTPS server that has OCSP stapling enabled and + * then uses BC to parse and validate it. + */ +public class OcspClientExample { + public static void main(String[] args) throws Exception { + if (!OpenSsl.isAvailable()) { + throw new IllegalStateException("OpenSSL is not available!"); + } + + if (!OpenSsl.isOcspSupported()) { + throw new IllegalStateException("OCSP is not supported!"); + } + + // Using Wikipedia as an example. I'd rather use Netty's own website + // but the server (Cloudflare) doesn't support OCSP stapling. A few + // other examples could be Microsoft or Squarespace. Use OpenSSL's + // CLI client to assess if a server supports OCSP stapling. E.g.: + // + // openssl s_client -tlsextdebug -status -connect www.squarespace.com:443 + // + String host = "www.wikipedia.org"; + + ReferenceCountedOpenSslContext context + = (ReferenceCountedOpenSslContext) SslContextBuilder.forClient() + .sslProvider(SslProvider.OPENSSL) + .enableOcsp(true) + .build(); + + try { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Promise promise = group.next().newPromise(); + + Bootstrap bootstrap = new Bootstrap() + .channel(NioSocketChannel.class) + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000) + .handler(newClientHandler(context, host, promise)); + + Channel channel = bootstrap.connect(host, 443) + .syncUninterruptibly() + .channel(); + + try { + FullHttpResponse response = promise.get(); + ReferenceCountUtil.release(response); + } finally { + channel.close(); + } + } finally { + group.shutdownGracefully(); + } + } finally { + context.release(); + } + } + + private static ChannelInitializer newClientHandler(final ReferenceCountedOpenSslContext context, + final String host, final Promise promise) { + + return new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + SslHandler sslHandler = context.newHandler(ch.alloc()); + ReferenceCountedOpenSslEngine engine + = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(sslHandler); + pipeline.addLast(new ExampleOcspClientHandler(engine)); + + pipeline.addLast(new HttpClientCodec()); + pipeline.addLast(new HttpObjectAggregator(1024 * 1024)); + pipeline.addLast(new HttpClientHandler(host, promise)); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (!promise.isDone()) { + promise.tryFailure(new IllegalStateException("Connection closed and Promise was not done.")); + } + ctx.fireChannelInactive(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (!promise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + }; + } + + private static class HttpClientHandler extends ChannelInboundHandlerAdapter { + + private final String host; + + private final Promise promise; + + public HttpClientHandler(String host, Promise promise) { + this.host = host; + this.promise = promise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + request.headers().set(HttpHeaderNames.HOST, host); + request.headers().set(HttpHeaderNames.USER_AGENT, "netty-ocsp-example/1.0"); + + ctx.writeAndFlush(request).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (!promise.isDone()) { + promise.tryFailure(new IllegalStateException("Connection closed and Promise was not done.")); + } + ctx.fireChannelInactive(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpResponse) { + if (!promise.trySuccess((FullHttpResponse) msg)) { + ReferenceCountUtil.release(msg); + } + return; + } + + ctx.fireChannelRead(msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (!promise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + } + + private static class ExampleOcspClientHandler extends OcspClientHandler { + + public ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) { + super(engine); + } + + @Override + protected boolean verify(ChannelHandlerContext ctx, ReferenceCountedOpenSslEngine engine) throws Exception { + byte[] staple = engine.getOcspResponse(); + if (staple == null) { + throw new IllegalStateException("Server didn't provide an OCSP staple!"); + } + + OCSPResp response = new OCSPResp(staple); + if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { + return false; + } + + SSLSession session = engine.getSession(); + X509Certificate[] chain = session.getPeerCertificateChain(); + BigInteger certSerial = chain[0].getSerialNumber(); + + BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); + SingleResp first = basicResponse.getResponses()[0]; + + // ATTENTION: CertificateStatus.GOOD is actually a null value! Do not use + // equals() or you'll NPE! + CertificateStatus status = first.getCertStatus(); + BigInteger ocspSerial = first.getCertID().getSerialNumber(); + String message = new StringBuilder() + .append("OCSP status of ").append(ctx.channel().remoteAddress()) + .append("\n Status: ").append(status == CertificateStatus.GOOD ? "Good" : status) + .append("\n This Update: ").append(first.getThisUpdate()) + .append("\n Next Update: ").append(first.getNextUpdate()) + .append("\n Cert Serial: ").append(certSerial) + .append("\n OCSP Serial: ").append(ocspSerial) + .toString(); + System.out.println(message); + + return status == CertificateStatus.GOOD && certSerial.equals(ocspSerial); + } + } +} diff --git a/example/src/main/java/io/netty/example/ocsp/OcspRequestBuilder.java b/example/src/main/java/io/netty/example/ocsp/OcspRequestBuilder.java new file mode 100644 index 0000000000..c63c5c7048 --- /dev/null +++ b/example/src/main/java/io/netty/example/ocsp/OcspRequestBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version + * 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.netty.example.ocsp; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPReqBuilder; +import org.bouncycastle.operator.DigestCalculator; + +/** + * This is a simplified version of BC's own {@link OCSPReqBuilder}. + * + * @see OCSPReqBuilder + */ +public class OcspRequestBuilder { + + private static final SecureRandom GENERATOR = new SecureRandom(); + + private SecureRandom generator = GENERATOR; + + private DigestCalculator calculator = Digester.sha1(); + + private X509Certificate certificate; + + private X509Certificate issuer; + + public OcspRequestBuilder generator(SecureRandom generator) { + this.generator = generator; + return this; + } + + public OcspRequestBuilder calculator(DigestCalculator calculator) { + this.calculator = calculator; + return this; + } + + public OcspRequestBuilder certificate(X509Certificate certificate) { + this.certificate = certificate; + return this; + } + + public OcspRequestBuilder issuer(X509Certificate issuer) { + this.issuer = issuer; + return this; + } + + /** + * ATTENTION: The returned {@link OCSPReq} is not re-usable/cacheable! It contains a one-time nonce + * and CA's will (should) reject subsequent requests that have the same nonce value. + */ + public OCSPReq build() throws OCSPException, IOException, CertificateEncodingException { + SecureRandom generator = checkNotNull(this.generator, "generator"); + DigestCalculator calculator = checkNotNull(this.calculator, "calculator"); + X509Certificate certificate = checkNotNull(this.certificate, "certificate"); + X509Certificate issuer = checkNotNull(this.issuer, "issuer"); + + BigInteger serial = certificate.getSerialNumber(); + + CertificateID certId = new CertificateID(calculator, + new X509CertificateHolder(issuer.getEncoded()), serial); + + OCSPReqBuilder builder = new OCSPReqBuilder(); + builder.addRequest(certId); + + byte[] nonce = new byte[8]; + generator.nextBytes(nonce); + + Extension[] extensions = new Extension[] { + new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, + new DEROctetString(nonce)) }; + + builder.setRequestExtensions(new Extensions(extensions)); + + return builder.build(); + } +} diff --git a/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java new file mode 100644 index 0000000000..5ff2210a0f --- /dev/null +++ b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java @@ -0,0 +1,209 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version + * 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.netty.example.ocsp; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.math.BigInteger; +import java.net.URI; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.ReferenceCountedOpenSslContext; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.util.CharsetUtil; + +/** + * ATTENTION: This is an incomplete example! In order to provide a fully functional + * end-to-end example we'd need a X.509 certificate and the matching PrivateKey. + */ +@SuppressWarnings("unused") +public class OcspServerExample { + public static void main(String[] args) throws Exception { + // We assume there's a private key. + PrivateKey privateKey = null; + + // Step 1: Load the certificate chain for netty.io. We'll need the certificate + // and the issuer's certificate and we don't need any of the intermediate certs. + // The array is assumed to be a certain order to keep things simple. + X509Certificate[] keyCertChain = parseCertificates(OcspServerExample.class, "netty_io_chain.pem"); + + X509Certificate certificate = keyCertChain[0]; + X509Certificate issuer = keyCertChain[keyCertChain.length - 1]; + + // Step 2: We need the URL of the CA's OCSP responder server. It's somewhere encoded + // into the certificate! Notice that it's a HTTP URL. + URI uri = OcspUtils.ocspUri(certificate); + System.out.println("OCSP Responder URI: " + uri); + + if (uri == null) { + throw new IllegalStateException("The CA/certificate doesn't have an OCSP responder"); + } + + // Step 3: Construct the OCSP request + OCSPReq request = new OcspRequestBuilder() + .certificate(certificate) + .issuer(issuer) + .build(); + + // Step 4: Do the request to the CA's OCSP responder + OCSPResp response = OcspUtils.request(uri, request, 5L, TimeUnit.SECONDS); + if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { + throw new IllegalStateException("response-status=" + response.getStatus()); + } + + // Step 5: Is my certificate any good or has the CA revoked it? + BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); + SingleResp first = basicResponse.getResponses()[0]; + + CertificateStatus status = first.getCertStatus(); + System.out.println("Status: " + (status == CertificateStatus.GOOD ? "Good" : status)); + System.out.println("This Update: " + first.getThisUpdate()); + System.out.println("Next Update: " + first.getNextUpdate()); + + if (status != null) { + throw new IllegalStateException("certificate-status=" + status); + } + + BigInteger certSerial = certificate.getSerialNumber(); + BigInteger ocspSerial = first.getCertID().getSerialNumber(); + if (!certSerial.equals(ocspSerial)) { + throw new IllegalStateException("Bad Serials=" + certSerial + " vs. " + ocspSerial); + } + + // Step 6: Cache the OCSP response and use it as long as it's not + // expired. The exact semantics are beyond the scope of this example. + + if (!OpenSsl.isAvailable()) { + throw new IllegalStateException("OpenSSL is not available!"); + } + + if (!OpenSsl.isOcspSupported()) { + throw new IllegalStateException("OCSP is not supported!"); + } + + if (privateKey == null) { + throw new IllegalStateException("Because we don't have a PrivateKey we can't continue past this point."); + } + + ReferenceCountedOpenSslContext context + = (ReferenceCountedOpenSslContext) SslContextBuilder.forServer(privateKey, keyCertChain) + .sslProvider(SslProvider.OPENSSL) + .enableOcsp(true) + .build(); + + try { + ServerBootstrap bootstrap = new ServerBootstrap() + .childHandler(newServerHandler(context, response)); + + // so on and so forth... + } finally { + context.release(); + } + } + + private static ChannelInitializer newServerHandler(final ReferenceCountedOpenSslContext context, + final OCSPResp response) { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + SslHandler sslHandler = context.newHandler(ch.alloc()); + + if (response != null) { + ReferenceCountedOpenSslEngine engine + = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + + engine.setOcspResponse(response.getEncoded()); + } + + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(sslHandler); + + // so on and so forth... + } + }; + } + + private static X509Certificate[] parseCertificates(Class clazz, String name) throws Exception { + InputStream in = clazz.getResourceAsStream(name); + if (in == null) { + throw new FileNotFoundException("clazz=" + clazz + ", name=" + name); + } + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(in, CharsetUtil.US_ASCII)); + try { + return parseCertificates(reader); + } finally { + reader.close(); + } + } finally { + in.close(); + } + } + + private static X509Certificate[] parseCertificates(Reader reader) throws Exception { + + JcaX509CertificateConverter converter = new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()); + + List dst = new ArrayList(); + + PEMParser parser = new PEMParser(reader); + try { + X509CertificateHolder holder = null; + + while ((holder = (X509CertificateHolder) parser.readObject()) != null) { + X509Certificate certificate = converter.getCertificate(holder); + if (certificate == null) { + continue; + } + + dst.add(certificate); + } + } finally { + parser.close(); + } + + return dst.toArray(new X509Certificate[0]); + } +} diff --git a/example/src/main/java/io/netty/example/ocsp/OcspUtils.java b/example/src/main/java/io/netty/example/ocsp/OcspUtils.java new file mode 100644 index 0000000000..09ef17f39e --- /dev/null +++ b/example/src/main/java/io/netty/example/ocsp/OcspUtils.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, version + * 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.netty.example.ocsp; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.HttpsURLConnection; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.BERTags; +import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.x509.extension.X509ExtensionUtil; + +import io.netty.util.CharsetUtil; + +public final class OcspUtils { + /** + * The OID for OCSP responder URLs. + * + * http://www.alvestrand.no/objectid/1.3.6.1.5.5.7.48.1.html + */ + private static final ASN1ObjectIdentifier OCSP_RESPONDER_OID + = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.48.1").intern(); + + private static final String OCSP_REQUEST_TYPE = "application/ocsp-request"; + + private static final String OCSP_RESPONSE_TYPE = "application/ocsp-response"; + + private OcspUtils() { + } + + /** + * Returns the OCSP responder {@link URI} or {@code null} if it doesn't have one. + */ + public static URI ocspUri(X509Certificate certificate) throws IOException { + byte[] value = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); + if (value == null) { + return null; + } + + ASN1Primitive authorityInfoAccess = X509ExtensionUtil.fromExtensionValue(value); + if (!(authorityInfoAccess instanceof DLSequence)) { + return null; + } + + DLSequence aiaSequence = (DLSequence) authorityInfoAccess; + DERTaggedObject taggedObject = findObject(aiaSequence, OCSP_RESPONDER_OID, DERTaggedObject.class); + if (taggedObject == null) { + return null; + } + + if (taggedObject.getTagNo() != BERTags.OBJECT_IDENTIFIER) { + return null; + } + + byte[] encoded = taggedObject.getEncoded(); + int length = (int) encoded[1] & 0xFF; + String uri = new String(encoded, 2, length, CharsetUtil.UTF_8); + return URI.create(uri); + } + + private static T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class type) { + for (ASN1Encodable element : sequence) { + if (!(element instanceof DLSequence)) { + continue; + } + + DLSequence subSequence = (DLSequence) element; + if (subSequence.size() != 2) { + continue; + } + + ASN1Encodable key = subSequence.getObjectAt(0); + ASN1Encodable value = subSequence.getObjectAt(1); + + if (key.equals(oid) && type.isInstance(value)) { + return type.cast(value); + } + } + + return null; + } + + /** + * TODO: This is a very crude and non-scalable HTTP client to fetch the OCSP response from the + * CA's OCSP responder server. It's meant to demonstrate the basic building blocks on how to + * interact with the responder server and you should consider using Netty's HTTP client instead. + */ + public static OCSPResp request(URI uri, OCSPReq request, long timeout, TimeUnit unit) throws IOException { + byte[] encoded = request.getEncoded(); + + URL url = uri.toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setConnectTimeout((int) unit.toMillis(timeout)); + connection.setReadTimeout((int) unit.toMillis(timeout)); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("host", uri.getHost()); + connection.setRequestProperty("content-type", OCSP_REQUEST_TYPE); + connection.setRequestProperty("accept", OCSP_RESPONSE_TYPE); + connection.setRequestProperty("content-length", String.valueOf(encoded.length)); + + OutputStream out = connection.getOutputStream(); + try { + out.write(encoded); + out.flush(); + + InputStream in = connection.getInputStream(); + try { + int code = connection.getResponseCode(); + if (code != HttpsURLConnection.HTTP_OK) { + throw new IOException("Unexpected status-code=" + code); + } + + String contentType = connection.getContentType(); + if (!contentType.equalsIgnoreCase(OCSP_RESPONSE_TYPE)) { + throw new IOException("Unexpected content-type=" + contentType); + } + + int contentLength = connection.getContentLength(); + if (contentLength == -1) { + // Probably a terrible idea! + contentLength = Integer.MAX_VALUE; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[8192]; + int length = -1; + + while ((length = in.read(buffer)) != -1) { + baos.write(buffer, 0, length); + + if (baos.size() >= contentLength) { + break; + } + } + } finally { + baos.close(); + } + return new OCSPResp(baos.toByteArray()); + } finally { + in.close(); + } + } finally { + out.close(); + } + } finally { + connection.disconnect(); + } + } +} diff --git a/example/src/main/resources/io/netty/example/ocsp/README.txt b/example/src/main/resources/io/netty/example/ocsp/README.txt new file mode 100644 index 0000000000..2139ceed7b --- /dev/null +++ b/example/src/main/resources/io/netty/example/ocsp/README.txt @@ -0,0 +1,8 @@ +The netty_io_chain.pem file is the cert chain of . The file +was created using the browser's export functionality for certs. The cert was +issued by COMODO (via Cloudflare) and its purpose is to demonstrate how to +extract the CA's OCSP responder server URL from the certificate and then +interact with the responder server to check the revocation status of the +certificate. The cert will at some point expire or get revoked. It's probably +a good idea to save it once that happens as it's an excellent example to +demonstrate negative responses from the CA. \ No newline at end of file diff --git a/example/src/main/resources/io/netty/example/ocsp/netty_io_chain.pem b/example/src/main/resources/io/netty/example/ocsp/netty_io_chain.pem new file mode 100644 index 0000000000..9471a3d95c --- /dev/null +++ b/example/src/main/resources/io/netty/example/ocsp/netty_io_chain.pem @@ -0,0 +1,63 @@ +-----BEGIN CERTIFICATE----- +MIIHIDCCBsagAwIBAgIQWBZ3c+IkpJfV6xSQfc79MzAKBggqhkjOPQQDAjCBkjEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxODA2BgNVBAMT +L0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQSAy +MB4XDTE3MDIyMTAwMDAwMFoXDTE3MDgwNjIzNTk1OVowazEhMB8GA1UECxMYRG9t +YWluIENvbnRyb2wgVmFsaWRhdGVkMSEwHwYDVQQLExhQb3NpdGl2ZVNTTCBNdWx0 +aS1Eb21haW4xIzAhBgNVBAMTGnNuaTQ5NjI5LmNsb3VkZmxhcmVzc2wuY29tMFkw +EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbBzbKRFsB8VD5/T1ruGNByqTP3b+7zRN +TJ0uaaHb5BzDd7+bdfYeQLfxbIe9yIX/InRWxcPrxZxNFe0pe26zWaOCBSIwggUe +MB8GA1UdIwQYMBaAFEAJYWfwvINxT94SCCxv1NQrdj2WMB0GA1UdDgQWBBQB5zit +yT/cG3KZA9q/xPDNgwVu4DAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAd +BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEE +AbIxAQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29t +L0NQUzAIBgZngQwBAgEwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5jb21v +ZG9jYTQuY29tL0NPTU9ET0VDQ0RvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJD +QTIuY3JsMIGIBggrBgEFBQcBAQR8MHowUQYIKwYBBQUHMAKGRWh0dHA6Ly9jcnQu +Y29tb2RvY2E0LmNvbS9DT01PRE9FQ0NEb21haW5WYWxpZGF0aW9uU2VjdXJlU2Vy +dmVyQ0EyLmNydDAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AuY29tb2RvY2E0LmNv +bTCCA2kGA1UdEQSCA2AwggNcghpzbmk0OTYyOS5jbG91ZGZsYXJlc3NsLmNvbYIQ +Ki4wOTMzMTk0NDIzLmNvbYISKi5hbGlhbW90ZWwuY29tLnR3ghMqLmFuaW1hbGNv +bGxlZ2Uub3JnghEqLmFyZG9yLWRjLmNvbS50d4IOKi5hc2lhbmlhZ2EubXmCDCou +YmlsbG93cy50d4ISKi5jYW5keXNvdXJjZXMuY29tgiIqLmNvbmNyZXRlY29tbWFu +ZG9zY29uc3RydWN0aW9uLmNhghYqLmZsYXRsaW5lc2VjdXJpdHkuY29tgggqLmcy +Yy50d4IOKi5nbGluZS5jb20udHeCEioubGVtaXJhYmVhdXR5LmNvbYIWKi5saWJl +cnR5ZGVzaWduLmNvbS50d4ISKi5saWJlcnR5ZGVzaWduLnR3ghIqLmxpYmVydHlv +Y2Vhbi5jb22CEioubGl1aC1qaWFuLmNvbS50d4IQKi5samhzYWx1bW5pLm9yZ4Ia +Ki5sb21hbmthbS13aW5nY2h1bi5vcmcudHeCCioubmV0dHkuaW+CDCoub3BsaWZ0 +LmNvbYITKi5zb3V0aGVybmJlbGxzLm5ldIIZKi50ZW5yeW8tY2xlYW5yb29tLmNv +bS50d4IOMDkzMzE5NDQyMy5jb22CEGFsaWFtb3RlbC5jb20udHeCEWFuaW1hbGNv +bGxlZ2Uub3Jngg9hcmRvci1kYy5jb20udHeCDGFzaWFuaWFnYS5teYIKYmlsbG93 +cy50d4IQY2FuZHlzb3VyY2VzLmNvbYIgY29uY3JldGVjb21tYW5kb3Njb25zdHJ1 +Y3Rpb24uY2GCFGZsYXRsaW5lc2VjdXJpdHkuY29tggZnMmMudHeCDGdsaW5lLmNv +bS50d4IQbGVtaXJhYmVhdXR5LmNvbYIUbGliZXJ0eWRlc2lnbi5jb20udHeCEGxp +YmVydHlkZXNpZ24udHeCEGxpYmVydHlvY2Vhbi5jb22CEGxpdWgtamlhbi5jb20u +dHeCDmxqaHNhbHVtbmkub3Jnghhsb21hbmthbS13aW5nY2h1bi5vcmcudHeCCG5l +dHR5LmlvggpvcGxpZnQuY29tghFzb3V0aGVybmJlbGxzLm5ldIIXdGVucnlvLWNs +ZWFucm9vbS5jb20udHcwCgYIKoZIzj0EAwIDSAAwRQIgCqAcYW//4ucQu4WiCM/l +bsaz7ZIJb6vKxyMoWRoCuJkCIQDEIK02bfsKicBK9DJGCq7j6TkmIwwvnRuuRA7W +08APyA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDnzCCAyWgAwIBAgIQWyXOaQfEJlVm0zkMmalUrTAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwOTI1MDAw +MDAwWhcNMjkwOTI0MjM1OTU5WjCBkjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxODA2BgNVBAMTL0NPTU9ETyBFQ0MgRG9tYWluIFZhbGlk +YXRpb24gU2VjdXJlIFNlcnZlciBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEAjgZgTrJaYRwWQKOqIofMN+83gP8eR06JSxrQSEYgur5PkrkM8wSzypD/A7y +ZADA4SVQgiTNtkk4DyVHkUikraOCAWYwggFiMB8GA1UdIwQYMBaAFHVxpxlIGbyd +nepBR9+UxEh3mdN5MB0GA1UdDgQWBBRACWFn8LyDcU/eEggsb9TUK3Y9ljAOBgNV +HQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAECATBMBgNV +HR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9FQ0ND +ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDByBggrBgEFBQcBAQRmMGQwOwYIKwYB +BQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET0VDQ0FkZFRydXN0 +Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQuY29tMAoG +CCqGSM49BAMDA2gAMGUCMQCsaEclgBNPE1bAojcJl1pQxOfttGHLKIoKETKm4nHf +EQGJbwd6IGZrGNC5LkP3Um8CMBKFfI4TZpIEuppFCZRKMGHRSdxv6+ctyYnPHmp8 +7IXOMCVZuoFwNLg0f+cB0eLLUg== +-----END CERTIFICATE----- diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java index 2ffcf96255..29526536d5 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java @@ -259,6 +259,13 @@ public final class OpenSsl { return version() >= 0x10002000L; } + /** + * Returns {@code true} if the used version of OpenSSL supports OCSP stapling. + */ + public static boolean isOcspSupported() { + return version() >= 0x10002000L; + } + /** * Returns the version of the used available OpenSSL library or {@code -1} if {@link #isAvailable()} * returns {@code false}. diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java index 0e1902d8a3..46412e9f52 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java @@ -175,17 +175,18 @@ public final class OpenSslClientContext extends OpenSslContext { throws SSLException { this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), - keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, null, sessionCacheSize, sessionTimeout); + keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, null, sessionCacheSize, + sessionTimeout, false); } OpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout) + long sessionCacheSize, long sessionTimeout, boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, - ClientAuth.NONE, protocols, false); + ClientAuth.NONE, protocols, false, enableOcsp); boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java index 4523b330a5..c4ca6b5197 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java @@ -29,18 +29,19 @@ import javax.net.ssl.SSLException; public abstract class OpenSslContext extends ReferenceCountedOpenSslContext { OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, - ClientAuth clientAuth, String[] protocols, boolean startTls) + ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apnCfg, sessionCacheSize, sessionTimeout, mode, keyCertChain, - clientAuth, protocols, startTls, false); + clientAuth, protocols, startTls, enableOcsp, false); } OpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, - ClientAuth clientAuth, String[] protocols, boolean startTls) throws SSLException { + ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, mode, keyCertChain, clientAuth, protocols, - startTls, false); + startTls, enableOcsp, false); } @Override diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java index b8b8a0f822..f57434b133 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -323,17 +323,18 @@ public final class OpenSslServerContext extends OpenSslContext { this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, - apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false); + apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false); } OpenSslServerContext( X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls) - throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, - cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls); + cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, + enableOcsp); } @SuppressWarnings("deprecation") @@ -341,10 +342,10 @@ public final class OpenSslServerContext extends OpenSslContext { X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls) - throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, - clientAuth, protocols, startTls); + clientAuth, protocols, startTls, enableOcsp); // Create a new SSL_CTX and configure it. boolean success = false; try { diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java index 960b4e25cb..f41c38b65a 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java @@ -54,10 +54,10 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - String[] protocols, long sessionCacheSize, long sessionTimeout) - throws SSLException { + String[] protocols, long sessionCacheSize, long sessionTimeout, + boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain, - ClientAuth.NONE, protocols, false, true); + ClientAuth.NONE, protocols, false, enableOcsp, true); boolean success = false; try { sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java index b35bc17c11..a4b2e73193 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; + import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; @@ -141,6 +142,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen final Certificate[] keyCertChain; final ClientAuth clientAuth; final String[] protocols; + final boolean enableOcsp; final OpenSslEngineMap engineMap = new DefaultOpenSslEngineMap(); private volatile boolean rejectRemoteInitiatedRenegotiation; private volatile int bioNonApplicationBufferSize = DEFAULT_BIO_NON_APPLICATION_BUFFER_SIZE; @@ -213,20 +215,24 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen ReferenceCountedOpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, ClientAuth clientAuth, String[] protocols, - boolean startTls, boolean leakDetection) throws SSLException { + boolean startTls, boolean enableOcsp, boolean leakDetection) throws SSLException { this(ciphers, cipherFilter, toNegotiator(apnCfg), sessionCacheSize, sessionTimeout, mode, keyCertChain, - clientAuth, protocols, startTls, leakDetection); + clientAuth, protocols, startTls, enableOcsp, leakDetection); } ReferenceCountedOpenSslContext(Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain, - ClientAuth clientAuth, String[] protocols, boolean startTls, boolean leakDetection) - throws SSLException { + ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp, + boolean leakDetection) throws SSLException { super(startTls); OpenSsl.ensureAvailability(); + if (enableOcsp && !OpenSsl.isOcspSupported()) { + throw new IllegalStateException("OCSP is not supported."); + } + if (mode != SSL.SSL_MODE_SERVER && mode != SSL.SSL_MODE_CLIENT) { throw new IllegalArgumentException("mode most be either SSL.SSL_MODE_SERVER or SSL.SSL_MODE_CLIENT"); } @@ -234,6 +240,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen this.mode = mode; this.clientAuth = isServer() ? checkNotNull(clientAuth, "clientAuth") : ClientAuth.NONE; this.protocols = protocols; + this.enableOcsp = enableOcsp; if (mode == SSL.SSL_MODE_SERVER) { rejectRemoteInitiatedRenegotiation = @@ -348,6 +355,10 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen // Revert the session timeout to the default value. SSLContext.setSessionCacheTimeout(ctx, sessionTimeout); } + + if (enableOcsp) { + SSLContext.enableOcsp(ctx, isClient()); + } } success = true; } finally { @@ -493,6 +504,10 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen final void destroy() { synchronized (ReferenceCountedOpenSslContext.class) { if (ctx != 0) { + if (enableOcsp) { + SSLContext.disableOcsp(ctx); + } + SSLContext.free(ctx); ctx = 0; } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java index 9db63c9b0a..90647c33b3 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + import javax.net.ssl.SNIMatcher; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -203,6 +204,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1]; private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1]; private final OpenSslKeyMaterialManager keyMaterialManager; + private final boolean enableOcsp; // This is package-private as we set it from OpenSslContext if an exception is thrown during // the verification step. @@ -229,6 +231,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc rejectRemoteInitiatedRenegation = context.getRejectRemoteInitiatedRenegotiation(); localCerts = context.keyCertChain; keyMaterialManager = context.keyMaterialManager(); + enableOcsp = context.enableOcsp; ssl = SSL.newSSL(context.ctx, !context.isClient()); try { networkBIO = SSL.bioNewByteBuffer(ssl, context.getBioNonApplicationBufferSize()); @@ -246,12 +249,52 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc if (clientMode && peerHost != null) { SSL.setTlsExtHostName(ssl, peerHost); } + + if (enableOcsp) { + SSL.enableOcsp(ssl); + } } catch (Throwable cause) { SSL.freeSSL(ssl); PlatformDependent.throwException(cause); } } + /** + * Sets the OCSP response. + */ + @UnstableApi + public void setOcspResponse(byte[] response) { + if (!enableOcsp) { + throw new IllegalStateException("OCSP stapling is not enabled"); + } + + if (clientMode) { + throw new IllegalStateException("Not a server SSLEngine"); + } + + synchronized (this) { + SSL.setOcspResponse(ssl, response); + } + } + + /** + * Returns the OCSP response or {@code null} if the server didn't provide a stapled OCSP response. + */ + @UnstableApi + public byte[] getOcspResponse() { + if (!enableOcsp) { + throw new IllegalStateException("OCSP stapling is not enabled"); + } + + if (!clientMode) { + throw new IllegalStateException("Not a client SSLEngine"); + } + + synchronized (this) { + return SSL.getOcspResponse(ssl); + } + } + @Override public final int refCnt() { return refCnt.refCnt(); diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java index 4761ddf50b..1cef8bc991 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java @@ -48,20 +48,21 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls) - throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, - cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls); + cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, + enableOcsp); } private ReferenceCountedOpenSslServerContext( X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls) - throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain, - clientAuth, protocols, startTls, true); + clientAuth, protocols, startTls, enableOcsp, true); // Create a new SSL_CTX and configure it. boolean success = false; try { diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index 77d407f6f1..ba6cb68f81 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -385,7 +385,7 @@ public abstract class SslContext { trustManagerFactory, toX509Certificates(keyCertChainFile), toPrivateKey(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, - sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false); + sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false); } catch (Exception e) { if (e instanceof SSLException) { throw (SSLException) e; @@ -400,8 +400,8 @@ public abstract class SslContext { X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls) - throws SSLException { + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp) throws SSLException { if (provider == null) { provider = defaultServerProvider(); @@ -409,6 +409,9 @@ public abstract class SslContext { switch (provider) { case JDK: + if (enableOcsp) { + throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); + } return new JdkSslServerContext(sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, @@ -418,13 +421,13 @@ public abstract class SslContext { return new OpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls); + clientAuth, protocols, startTls, enableOcsp); case OPENSSL_REFCNT: verifyNullSslContextProvider(provider, sslContextProvider); return new ReferenceCountedOpenSslServerContext( trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls); + clientAuth, protocols, startTls, enableOcsp); default: throw new Error(provider.toString()); } @@ -740,7 +743,7 @@ public abstract class SslContext { toX509Certificates(trustCertCollectionFile), trustManagerFactory, toX509Certificates(keyCertChainFile), toPrivateKey(keyFile, keyPassword), keyPassword, keyManagerFactory, ciphers, cipherFilter, - apn, null, sessionCacheSize, sessionTimeout); + apn, null, sessionCacheSize, sessionTimeout, false); } catch (Exception e) { if (e instanceof SSLException) { throw (SSLException) e; @@ -755,12 +758,15 @@ public abstract class SslContext { X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout) throws SSLException { + long sessionCacheSize, long sessionTimeout, boolean enableOcsp) throws SSLException { if (provider == null) { provider = defaultClientProvider(); } switch (provider) { case JDK: + if (enableOcsp) { + throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); + } return new JdkSslClientContext(sslContextProvider, trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout); @@ -768,12 +774,14 @@ public abstract class SslContext { verifyNullSslContextProvider(provider, sslContextProvider); return new OpenSslClientContext( trustCert, trustManagerFactory, keyCertChain, key, keyPassword, - keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout); + keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, + enableOcsp); case OPENSSL_REFCNT: verifyNullSslContextProvider(provider, sslContextProvider); return new ReferenceCountedOpenSslClientContext( trustCert, trustManagerFactory, keyCertChain, key, keyPassword, - keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout); + keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, + enableOcsp); default: throw new Error(provider.toString()); } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java index 8b85851ae8..2e3c55b9cb 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -18,10 +18,13 @@ package io.netty.handler.ssl; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import io.netty.util.internal.UnstableApi; + import java.security.Provider; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManagerFactory; + import java.io.File; import java.io.InputStream; import java.security.PrivateKey; @@ -142,6 +145,7 @@ public final class SslContextBuilder { private ClientAuth clientAuth = ClientAuth.NONE; private String[] protocols; private boolean startTls; + private boolean enableOcsp; private SslContextBuilder(boolean forServer) { this.forServer = forServer; @@ -415,6 +419,18 @@ public final class SslContextBuilder { return this; } + /** + * Enables OCSP stapling. Please note that not all {@link SslProvider} implementations support OCSP + * stapling and an exception will be thrown upon {@link #build()}. + * + * @see OpenSsl#isOcspSupported() + */ + @UnstableApi + public SslContextBuilder enableOcsp(boolean enableOcsp) { + this.enableOcsp = enableOcsp; + return this; + } + /** * Create new {@code SslContext} instance with configured settings. *

If {@link #sslProvider(SslProvider)} is set to {@link SslProvider#OPENSSL_REFCNT} then the caller is @@ -424,11 +440,12 @@ public final class SslContextBuilder { if (forServer) { return SslContext.newServerContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, - ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls); + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, + enableOcsp); } else { return SslContext.newClientContextInternal(provider, sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, - ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout); + ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp); } } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java new file mode 100644 index 0000000000..aff09495ca --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.ssl.ocsp; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.ssl.ReferenceCountedOpenSslContext; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.UnstableApi; + +import javax.net.ssl.SSLHandshakeException; + +/** + * A handler for SSL clients to handle and act upon stapled OCSP responses. + * + * @see ReferenceCountedOpenSslContext#enableOcsp() + * @see ReferenceCountedOpenSslEngine#getOcspResponse() + */ +@UnstableApi +public abstract class OcspClientHandler extends ChannelInboundHandlerAdapter { + + private static final SSLHandshakeException OCSP_VERIFICATION_EXCEPTION = ThrowableUtil.unknownStackTrace( + new SSLHandshakeException("Bad OCSP response"), OcspClientHandler.class, "verify(...)"); + + private final ReferenceCountedOpenSslEngine engine; + + protected OcspClientHandler(ReferenceCountedOpenSslEngine engine) { + this.engine = ObjectUtil.checkNotNull(engine, "engine"); + } + + /** + * @see ReferenceCountedOpenSslEngine#getOcspResponse() + */ + protected abstract boolean verify(ChannelHandlerContext ctx, ReferenceCountedOpenSslEngine engine) throws Exception; + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + ctx.pipeline().remove(this); + + SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt; + if (event.isSuccess() && !verify(ctx, engine)) { + throw OCSP_VERIFICATION_EXCEPTION; + } + } + + ctx.fireUserEventTriggered(evt); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ocsp/package-info.java b/handler/src/main/java/io/netty/handler/ssl/ocsp/package-info.java new file mode 100644 index 0000000000..2883ff48cf --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/ocsp/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * OCSP stapling, + * formally known as the TLS Certificate Status Request extension, is an + * alternative approach to the Online Certificate Status Protocol (OCSP) + * for checking the revocation status of X.509 digital certificates. + */ +package io.netty.handler.ssl.ocsp; diff --git a/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java new file mode 100644 index 0000000000..4aecc74800 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java @@ -0,0 +1,501 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.handler.ssl.ocsp; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; + +import java.net.SocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +public class OcspTest { + + @BeforeClass + public static void checkOcspSupported() { + assumeTrue(OpenSsl.isOcspSupported()); + } + + @Test(expected = IllegalArgumentException.class) + public void testJdkClientEnableOcsp() throws Exception { + SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .enableOcsp(true) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testJdkServerEnableOcsp() throws Exception { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + try { + SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(SslProvider.JDK) + .enableOcsp(true) + .build(); + } finally { + ssc.delete(); + } + } + + @Test(expected = IllegalStateException.class) + public void testClientOcspNotEnabledOpenSsl() throws Exception { + testClientOcspNotEnabled(SslProvider.OPENSSL); + } + + @Test(expected = IllegalStateException.class) + public void testClientOcspNotEnabledOpenSslRefCnt() throws Exception { + testClientOcspNotEnabled(SslProvider.OPENSSL_REFCNT); + } + + private void testClientOcspNotEnabled(SslProvider sslProvider) throws Exception { + SslContext context = SslContextBuilder.forClient() + .sslProvider(sslProvider) + .build(); + try { + SslHandler sslHandler = context.newHandler(ByteBufAllocator.DEFAULT); + ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + try { + engine.getOcspResponse(); + } finally { + engine.release(); + } + } finally { + ReferenceCountUtil.release(context); + } + } + + @Test(expected = IllegalStateException.class) + public void testServerOcspNotEnabledOpenSsl() throws Exception { + testServerOcspNotEnabled(SslProvider.OPENSSL); + } + + @Test(expected = IllegalStateException.class) + public void testServerOcspNotEnabledOpenSslRefCnt() throws Exception { + testServerOcspNotEnabled(SslProvider.OPENSSL_REFCNT); + } + + private void testServerOcspNotEnabled(SslProvider sslProvider) throws Exception { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + try { + SslContext context = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslProvider) + .build(); + try { + SslHandler sslHandler = context.newHandler(ByteBufAllocator.DEFAULT); + ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + try { + engine.setOcspResponse(new byte[] { 1, 2, 3 }); + } finally { + engine.release(); + } + } finally { + ReferenceCountUtil.release(context); + } + } finally { + ssc.delete(); + } + } + + @Test(timeout = 10000L) + public void testClientAcceptingOcspStapleOpenSsl() throws Exception { + testClientAcceptingOcspStaple(SslProvider.OPENSSL); + } + + @Test(timeout = 10000L) + public void testClientAcceptingOcspStapleOpenSslRefCnt() throws Exception { + testClientAcceptingOcspStaple(SslProvider.OPENSSL_REFCNT); + } + + /** + * The Server provides an OCSP staple and the Client accepts it. + */ + private void testClientAcceptingOcspStaple(SslProvider sslProvider) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + ChannelInboundHandlerAdapter serverHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.writeAndFlush(Unpooled.wrappedBuffer("Hello, World!".getBytes())); + ctx.fireChannelActive(); + } + }; + + ChannelInboundHandlerAdapter clientHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + ReferenceCountUtil.release(msg); + } finally { + latch.countDown(); + } + } + }; + + byte[] response = newOcspResponse(); + TestClientOcspContext callback = new TestClientOcspContext(true); + + handshake(sslProvider, latch, serverHandler, response, clientHandler, callback); + + byte[] actual = callback.response(); + + assertNotNull(actual); + assertNotSame(response, actual); + assertArrayEquals(response, actual); + } + + @Test(timeout = 10000L) + public void testClientRejectingOcspStapleOpenSsl() throws Exception { + testClientRejectingOcspStaple(SslProvider.OPENSSL); + } + + @Test(timeout = 10000L) + public void testClientRejectingOcspStapleOpenSslRefCnt() throws Exception { + testClientRejectingOcspStaple(SslProvider.OPENSSL_REFCNT); + } + + /** + * The Server provides an OCSP staple and the Client rejects it. + */ + private void testClientRejectingOcspStaple(SslProvider sslProvider) throws Exception { + final AtomicReference causeRef = new AtomicReference(); + final CountDownLatch latch = new CountDownLatch(1); + + ChannelInboundHandlerAdapter clientHandler = new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + try { + causeRef.set(cause); + } finally { + latch.countDown(); + } + } + }; + + byte[] response = newOcspResponse(); + TestClientOcspContext callback = new TestClientOcspContext(false); + + handshake(sslProvider, latch, null, response, clientHandler, callback); + + byte[] actual = callback.response(); + + assertNotNull(actual); + assertNotSame(response, actual); + assertArrayEquals(response, actual); + + Throwable cause = causeRef.get(); + assertTrue("" + cause, cause instanceof SSLHandshakeException); + } + + @Test(timeout = 10000L) + public void testServerHasNoStapleOpenSsl() throws Exception { + testServerHasNoStaple(SslProvider.OPENSSL); + } + + @Test(timeout = 10000L) + public void testServerHasNoStapleOpenSslRefCnt() throws Exception { + testServerHasNoStaple(SslProvider.OPENSSL_REFCNT); + } + + /** + * The server has OCSP stapling enabled but doesn't provide a staple. + */ + private void testServerHasNoStaple(SslProvider sslProvider) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + ChannelInboundHandlerAdapter serverHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.writeAndFlush(Unpooled.wrappedBuffer("Hello, World!".getBytes())); + ctx.fireChannelActive(); + } + }; + + ChannelInboundHandlerAdapter clientHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + ReferenceCountUtil.release(msg); + } finally { + latch.countDown(); + } + } + }; + + byte[] response = null; + TestClientOcspContext callback = new TestClientOcspContext(true); + + handshake(sslProvider, latch, serverHandler, response, clientHandler, callback); + + byte[] actual = callback.response(); + + assertNull(response); + assertNull(actual); + } + + @Test(timeout = 10000L) + public void testClientExceptionOpenSsl() throws Exception { + testClientException(SslProvider.OPENSSL); + } + + @Test(timeout = 10000L) + public void testClientExceptionOpenSslRefCnt() throws Exception { + testClientException(SslProvider.OPENSSL_REFCNT); + } + + /** + * Testing what happens if the {@link OcspClientCallback} throws an {@link Exception}. + * + * The exception should bubble up on the client side and the connection should get closed. + */ + private void testClientException(SslProvider sslProvider) throws Exception { + final AtomicReference causeRef = new AtomicReference(); + final CountDownLatch latch = new CountDownLatch(1); + + ChannelInboundHandlerAdapter clientHandler = new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + try { + causeRef.set(cause); + } finally { + latch.countDown(); + } + } + }; + + final OcspTestException clientException = new OcspTestException("testClientException"); + byte[] response = newOcspResponse(); + OcspClientCallback callback = new OcspClientCallback() { + @Override + public boolean verify(byte[] response) throws Exception { + throw clientException; + } + }; + + handshake(sslProvider, latch, null, response, clientHandler, callback); + + assertSame(clientException, causeRef.get()); + } + + private static void handshake(SslProvider sslProvider, CountDownLatch latch, ChannelHandler serverHandler, + byte[] response, ChannelHandler clientHandler, OcspClientCallback callback) throws Exception { + + SelfSignedCertificate ssc = new SelfSignedCertificate(); + try { + SslContext serverSslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) + .sslProvider(sslProvider) + .enableOcsp(true) + .build(); + + try { + SslContext clientSslContext = SslContextBuilder.forClient() + .sslProvider(sslProvider) + .enableOcsp(true) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + + try { + EventLoopGroup group = new DefaultEventLoopGroup(); + try { + LocalAddress address = new LocalAddress("handshake-" + Math.random()); + Channel server = newServer(group, address, serverSslContext, response, serverHandler); + Channel client = newClient(group, address, clientSslContext, callback, clientHandler); + try { + assertTrue("Something went wrong.", latch.await(10L, TimeUnit.SECONDS)); + } finally { + client.close().syncUninterruptibly(); + server.close().syncUninterruptibly(); + } + } finally { + group.shutdownGracefully(1L, 1L, TimeUnit.SECONDS); + } + } finally { + ReferenceCountUtil.release(clientSslContext); + } + } finally { + ReferenceCountUtil.release(serverSslContext); + } + } finally { + ssc.delete(); + } + } + + private static Channel newServer(EventLoopGroup group, SocketAddress address, + SslContext context, byte[] response, ChannelHandler handler) { + + ServerBootstrap bootstrap = new ServerBootstrap() + .channel(LocalServerChannel.class) + .group(group) + .childHandler(newServerHandler(context, response, handler)); + + return bootstrap.bind(address) + .syncUninterruptibly() + .channel(); + } + + private static Channel newClient(EventLoopGroup group, SocketAddress address, + SslContext context, OcspClientCallback callback, ChannelHandler handler) { + + Bootstrap bootstrap = new Bootstrap() + .channel(LocalChannel.class) + .group(group) + .handler(newClientHandler(context, callback, handler)); + + return bootstrap.connect(address) + .syncUninterruptibly() + .channel(); + } + + private static ChannelHandler newServerHandler(final SslContext context, + final byte[] response, final ChannelHandler handler) { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + SslHandler sslHandler = context.newHandler(ch.alloc()); + + if (response != null) { + ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + engine.setOcspResponse(response); + } + + pipeline.addLast(sslHandler); + + if (handler != null) { + pipeline.addLast(handler); + } + } + }; + } + + private static ChannelHandler newClientHandler(final SslContext context, + final OcspClientCallback callback, final ChannelHandler handler) { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + SslHandler sslHandler = context.newHandler(ch.alloc()); + ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + + pipeline.addLast(sslHandler); + pipeline.addLast(new OcspClientCallbackHandler(engine, callback)); + + if (handler != null) { + pipeline.addLast(handler); + } + } + }; + } + + private static byte[] newOcspResponse() { + // Assume we got the OCSP staple from somewhere. Using a bogus byte[] + // in the test because getting a true staple from the CA is quite involved. + // It requires HttpCodec and Bouncycastle and the test may be very unreliable + // because the OCSP responder servers are basically being DDoS'd by the + // Internet. + + return "I am a bogus OCSP staple. OpenSSL does not care about the format of the byte[]!" + .getBytes(CharsetUtil.US_ASCII); + } + + private interface OcspClientCallback { + boolean verify(byte[] staple) throws Exception; + } + + private static final class TestClientOcspContext implements OcspClientCallback { + + private final CountDownLatch latch = new CountDownLatch(1); + private final boolean valid; + + private volatile byte[] response; + + public TestClientOcspContext(boolean valid) { + this.valid = valid; + } + + public byte[] response() throws InterruptedException, TimeoutException { + assertTrue(latch.await(10L, TimeUnit.SECONDS)); + return response; + } + + @Override + public boolean verify(byte[] response) throws Exception { + this.response = response; + latch.countDown(); + + return valid; + } + } + + private static final class OcspClientCallbackHandler extends OcspClientHandler { + + private final OcspClientCallback callback; + + public OcspClientCallbackHandler(ReferenceCountedOpenSslEngine engine, OcspClientCallback callback) { + super(engine); + this.callback = callback; + } + + @Override + protected boolean verify(ChannelHandlerContext ctx, ReferenceCountedOpenSslEngine engine) throws Exception { + byte[] response = engine.getOcspResponse(); + return callback.verify(response); + } + } + + private static final class OcspTestException extends IllegalStateException { + public OcspTestException(String message) { + super(message); + } + } +} diff --git a/pom.xml b/pom.xml index 55183c41d2..a2ebfb0ce6 100644 --- a/pom.xml +++ b/pom.xml @@ -370,6 +370,18 @@ true + + + org.bouncycastle + bcprov-jdk15on + 1.54 + compile + true + + com.fasterxml aalto-xml