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
This commit is contained in:
Roger Kapsi 2016-12-26 18:04:56 -05:00 committed by Scott Mitchell
parent 34e0007f07
commit 077a1988b9
22 changed files with 1636 additions and 38 deletions

View File

@ -129,6 +129,16 @@
<artifactId>logback-classic</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Needed for OCSP -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -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();
}
}

View File

@ -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<FullHttpResponse> 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<Channel> newClientHandler(final ReferenceCountedOpenSslContext context,
final String host, final Promise<FullHttpResponse> promise) {
return new ChannelInitializer<Channel>() {
@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<FullHttpResponse> promise;
public HttpClientHandler(String host, Promise<FullHttpResponse> 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);
}
}
}

View File

@ -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();
}
}

View File

@ -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<Channel> newServerHandler(final ReferenceCountedOpenSslContext context,
final OCSPResp response) {
return new ChannelInitializer<Channel>() {
@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<X509Certificate> dst = new ArrayList<X509Certificate>();
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]);
}
}

View File

@ -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> T findObject(DLSequence sequence, ASN1ObjectIdentifier oid, Class<T> 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();
}
}
}

View File

@ -0,0 +1,8 @@
The netty_io_chain.pem file is the cert chain of <https://netty.io>. 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.

View File

@ -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-----

View File

@ -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}.

View File

@ -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<String> 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,

View File

@ -29,18 +29,19 @@ import javax.net.ssl.SSLException;
public abstract class OpenSslContext extends ReferenceCountedOpenSslContext {
OpenSslContext(Iterable<String> 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<String> 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

View File

@ -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<String> 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<String> 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 {

View File

@ -54,10 +54,10 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
X509Certificate[] keyCertChain, PrivateKey key, String keyPassword,
KeyManagerFactory keyManagerFactory, Iterable<String> 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,

View File

@ -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<String> 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<String> 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;
}

View File

@ -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();

View File

@ -48,20 +48,21 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory,
X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
Iterable<String> 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<String> 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 {

View File

@ -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<String> 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<String> 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());
}

View File

@ -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.
* <p>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);
}
}
}

View File

@ -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);
}
}

View File

@ -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.
*/
/**
* <a href="https://en.wikipedia.org/wiki/OCSP_stapling">OCSP stapling</a>,
* 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;

View File

@ -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<Throwable> causeRef = new AtomicReference<Throwable>();
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<Throwable> causeRef = new AtomicReference<Throwable>();
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<Channel>() {
@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<Channel>() {
@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);
}
}
}

12
pom.xml
View File

@ -370,6 +370,18 @@
<optional>true</optional>
</dependency>
<!--
Completely optional and only needed for OCSP stapling to construct and
parse OCSP requests and responses.
-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml</groupId>
<artifactId>aalto-xml</artifactId>