< prev index next >
src/java.base/share/classes/sun/security/ssl/CertStatusExtension.java
Print this page
@@ -1,7 +1,7 @@
/*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
@@ -24,16 +24,79 @@
*/
package sun.security.ssl;
import java.io.IOException;
-import java.util.Objects;
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.security.cert.Extension;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import javax.net.ssl.SSLProtocolException;
+import sun.security.provider.certpath.OCSPResponse;
+import sun.security.provider.certpath.ResponderId;
+import static sun.security.ssl.SSLExtension.CH_STATUS_REQUEST;
+import static sun.security.ssl.SSLExtension.CH_STATUS_REQUEST_V2;
+import sun.security.ssl.SSLExtension.ExtensionConsumer;
+import static sun.security.ssl.SSLExtension.SH_STATUS_REQUEST;
+import static sun.security.ssl.SSLExtension.SH_STATUS_REQUEST_V2;
+import sun.security.ssl.SSLExtension.SSLExtensionSpec;
+import sun.security.ssl.SSLHandshake.HandshakeMessage;
+import sun.security.util.DerInputStream;
+import sun.security.util.DerValue;
+import sun.security.util.HexDumpEncoder;
-/*
+/**
+ * Pack of "status_request" and "status_request_v2" extensions.
+ */
+final class CertStatusExtension {
+ static final HandshakeProducer chNetworkProducer =
+ new CHCertStatusReqProducer();
+ static final ExtensionConsumer chOnLoadConsumer =
+ new CHCertStatusReqConsumer();
+
+ static final HandshakeProducer shNetworkProducer =
+ new SHCertStatusReqProducer();
+ static final ExtensionConsumer shOnLoadConsumer =
+ new SHCertStatusReqConsumer();
+
+ static final HandshakeProducer ctNetworkProducer =
+ new CTCertStatusResponseProducer();
+ static final ExtensionConsumer ctOnLoadConsumer =
+ new CTCertStatusResponseConsumer();
+
+ static final SSLStringize certStatusReqStringize =
+ new CertStatusRequestStringize();
+
+ static final HandshakeProducer chV2NetworkProducer =
+ new CHCertStatusReqV2Producer();
+ static final ExtensionConsumer chV2OnLoadConsumer =
+ new CHCertStatusReqV2Consumer();
+
+ static final HandshakeProducer shV2NetworkProducer =
+ new SHCertStatusReqV2Producer();
+ static final ExtensionConsumer shV2OnLoadConsumer =
+ new SHCertStatusReqV2Consumer();
+
+ static final SSLStringize certStatusReqV2Stringize =
+ new CertStatusRequestsStringize();
+
+ static final SSLStringize certStatusRespStringize =
+ new CertStatusRespStringize();
+
+ /**
+ * The "status_request" extension.
+ *
* RFC6066 defines the TLS extension,"status_request" (type 0x5),
* which allows the client to request that the server perform OCSP
* on the client's behalf.
+ *
* The "extension data" field of this extension contains a
* "CertificateStatusRequest" structure:
*
* struct {
* CertificateStatusType status_type;
@@ -50,156 +113,1106 @@
* } OCSPStatusRequest;
*
* opaque ResponderID<1..2^16-1>;
* opaque Extensions<0..2^16-1>;
*/
+ static final class CertStatusRequestSpec implements SSLExtensionSpec {
+ static final CertStatusRequestSpec DEFAULT =
+ new CertStatusRequestSpec(OCSPStatusRequest.EMPTY_OCSP);
-final class CertStatusReqExtension extends HelloExtension {
+ final CertStatusRequest statusRequest;
- private final StatusRequestType statReqType;
- private final StatusRequest request;
+ private CertStatusRequestSpec(CertStatusRequest statusRequest) {
+ this.statusRequest = statusRequest;
+ }
+ private CertStatusRequestSpec(ByteBuffer buffer) throws IOException {
+ // Is it a empty extension_data?
+ if (buffer.remaining() == 0) {
+ // server response
+ this.statusRequest = null;
+ return;
+ }
- /**
- * Construct the default status request extension object. The default
- * object results in a status_request extension where the extension
- * data segment is zero-length. This is used primarily in ServerHello
- * messages where the server asserts it can do RFC 6066 status stapling.
- */
- CertStatusReqExtension() {
- super(ExtensionType.EXT_STATUS_REQUEST);
- statReqType = null;
- request = null;
+ if (buffer.remaining() < 1) {
+ throw new SSLProtocolException(
+ "Invalid status_request extension: insufficient data");
+ }
+
+ byte statusType = (byte)Record.getInt8(buffer);
+ byte[] encoded = new byte[buffer.remaining()];
+ if (encoded.length != 0) {
+ buffer.get(encoded);
+ }
+ if (statusType == CertStatusRequestType.OCSP.id) {
+ this.statusRequest = new OCSPStatusRequest(statusType, encoded);
+ } else {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.info(
+ "Unknown certificate status request " +
+ "(status type: " + statusType + ")");
+ }
+
+ this.statusRequest = new CertStatusRequest(statusType, encoded);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return statusRequest == null ?
+ "<empty>" : statusRequest.toString();
+ }
}
/**
- * Construct the status request extension object given a request type
- * and {@code StatusRequest} object.
+ * Defines the CertificateStatus response structure as outlined in
+ * RFC 6066. This will contain a status response type, plus a single,
+ * non-empty OCSP response in DER-encoded form.
*
- * @param reqType a {@code StatusRequestExtType object correspoding
- * to the underlying {@code StatusRequest} object. A value of
- * {@code null} is not allowed.
- * @param statReq the {@code StatusRequest} object used to provide the
- * encoding for the TLS extension. A value of {@code null} is not
- * allowed.
- *
- * @throws IllegalArgumentException if the provided {@code StatusRequest}
- * does not match the type.
- * @throws NullPointerException if either the {@code reqType} or
- * {@code statReq} arguments are {@code null}.
+ * struct {
+ * CertificateStatusType status_type;
+ * select (status_type) {
+ * case ocsp: OCSPResponse;
+ * } response;
+ * } CertificateStatus;
*/
- CertStatusReqExtension(StatusRequestType reqType, StatusRequest statReq) {
- super(ExtensionType.EXT_STATUS_REQUEST);
+ static final class CertStatusResponseSpec implements SSLExtensionSpec {
+ final CertStatusResponse statusResponse;
- statReqType = Objects.requireNonNull(reqType,
- "Unallowed null value for status_type");
- request = Objects.requireNonNull(statReq,
- "Unallowed null value for request");
+ private CertStatusResponseSpec(CertStatusResponse resp) {
+ this.statusResponse = resp;
+ }
- // There is currently only one known status type (OCSP)
- // We can add more clauses to cover other types in the future
- if (statReqType == StatusRequestType.OCSP) {
- if (!(statReq instanceof OCSPStatusRequest)) {
- throw new IllegalArgumentException("StatusRequest not " +
- "of type OCSPStatusRequest");
+ private CertStatusResponseSpec(ByteBuffer buffer) throws IOException {
+ if (buffer.remaining() < 2) {
+ throw new SSLProtocolException(
+ "Invalid status_request extension: insufficient data");
}
+
+ // Get the status type (1 byte) and response data (vector)
+ byte type = (byte)Record.getInt8(buffer);
+ byte[] respData = Record.getBytes24(buffer);
+
+ // Create the CertStatusResponse based on the type
+ if (type == CertStatusRequestType.OCSP.id) {
+ this.statusResponse = new OCSPStatusResponse(type, respData);
+ } else {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.info(
+ "Unknown certificate status response " +
+ "(status type: " + type + ")");
+ }
+
+ this.statusResponse = new CertStatusResponse(type, respData);
}
}
+ @Override
+ public String toString() {
+ return statusResponse == null ?
+ "<empty>" : statusResponse.toString();
+ }
+ }
+
+ private static final
+ class CertStatusRequestStringize implements SSLStringize {
+ @Override
+ public String toString(ByteBuffer buffer) {
+ try {
+ return (new CertStatusRequestSpec(buffer)).toString();
+ } catch (IOException ioe) {
+ // For debug logging only, so please swallow exceptions.
+ return ioe.getMessage();
+ }
+ }
+ }
+
+ private static final
+ class CertStatusRespStringize implements SSLStringize {
+ @Override
+ public String toString(ByteBuffer buffer) {
+ try {
+ return (new CertStatusResponseSpec(buffer)).toString();
+ } catch (IOException ioe) {
+ // For debug logging only, so please swallow exceptions.
+ return ioe.getMessage();
+ }
+ }
+ }
+
+ static enum CertStatusRequestType {
+ OCSP ((byte)0x01, "ocsp"), // RFC 6066/6961
+ OCSP_MULTI ((byte)0x02, "ocsp_multi"); // RFC 6961
+
+ final byte id;
+ final String name;
+
+ private CertStatusRequestType(byte id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
/**
- * Construct the {@code CertStatusReqExtension} object from data read from
- * a {@code HandshakeInputStream}
+ * Returns the enum constant of the specified id (see RFC 6066).
+ */
+ static CertStatusRequestType valueOf(byte id) {
+ for (CertStatusRequestType srt : CertStatusRequestType.values()) {
+ if (srt.id == id) {
+ return srt;
+ }
+ }
+
+ return null;
+ }
+
+ static String nameOf(byte id) {
+ for (CertStatusRequestType srt : CertStatusRequestType.values()) {
+ if (srt.id == id) {
+ return srt.name;
+ }
+ }
+
+ return "UNDEFINED-CERT-STATUS-TYPE(" + id + ")";
+ }
+ }
+
+ static class CertStatusRequest {
+ final byte statusType;
+ final byte[] encodedRequest;
+
+ protected CertStatusRequest(byte statusType, byte[] encodedRequest) {
+ this.statusType = statusType;
+ this.encodedRequest = encodedRequest;
+ }
+
+ @Override
+ public String toString() {
+ MessageFormat messageFormat = new MessageFormat(
+ "\"certificate status type\": {0}\n" +
+ "\"encoded certificate status\": '{'\n" +
+ "{1}\n" +
+ "'}'",
+ Locale.ENGLISH);
+
+ HexDumpEncoder hexEncoder = new HexDumpEncoder();
+ String encoded = hexEncoder.encodeBuffer(encodedRequest);
+
+ Object[] messageFields = {
+ CertStatusRequestType.nameOf(statusType),
+ Utilities.indent(encoded)
+ };
+
+ return messageFormat.format(messageFields);
+ }
+ }
+
+ /*
+ * RFC6066 defines the TLS extension,"status_request" (type 0x5),
+ * which allows the client to request that the server perform OCSP
+ * on the client's behalf.
*
- * @param s the {@code HandshakeInputStream} providing the encoded data
- * @param len the length of the extension data
+ * The RFC defines an OCSPStatusRequest structure:
*
- * @throws IOException if any decoding errors happen during object
- * construction.
+ * struct {
+ * ResponderID responder_id_list<0..2^16-1>;
+ * Extensions request_extensions;
+ * } OCSPStatusRequest;
*/
- CertStatusReqExtension(HandshakeInStream s, int len) throws IOException {
- super(ExtensionType.EXT_STATUS_REQUEST);
+ static final class OCSPStatusRequest extends CertStatusRequest {
+ static final OCSPStatusRequest EMPTY_OCSP;
+ static final OCSPStatusRequest EMPTY_OCSP_MULTI;
- if (len > 0) {
- // Obtain the status type (first byte)
- statReqType = StatusRequestType.get(s.getInt8());
- if (statReqType == StatusRequestType.OCSP) {
- request = new OCSPStatusRequest(s);
- } else {
- // This is a status_type we don't understand. Create
- // an UnknownStatusRequest in order to preserve the data
- request = new UnknownStatusRequest(s, len - 1);
+ final List<ResponderId> responderIds;
+ final List<Extension> extensions;
+ private final int encodedLen;
+ private final int ridListLen;
+ private final int extListLen;
+
+ static {
+ OCSPStatusRequest ocspReq = null;
+ OCSPStatusRequest multiReq = null;
+
+ try {
+ ocspReq = new OCSPStatusRequest(
+ CertStatusRequestType.OCSP.id,
+ new byte[] {0x00, 0x00, 0x00, 0x00});
+ multiReq = new OCSPStatusRequest(
+ CertStatusRequestType.OCSP_MULTI.id,
+ new byte[] {0x00, 0x00, 0x00, 0x00});
+ } catch (IOException ioe) {
+ // unlikely
}
+
+ EMPTY_OCSP = ocspReq;
+ EMPTY_OCSP_MULTI = multiReq;
+ }
+
+ private OCSPStatusRequest(byte statusType,
+ byte[] encoded) throws IOException {
+ super(statusType, encoded);
+
+ if (encoded == null || encoded.length < 4) {
+ // 2: length of responder_id_list
+ // +2: length of request_extensions
+ throw new SSLProtocolException(
+ "Invalid OCSP status request: insufficient data");
+ }
+ this.encodedLen = encoded.length;
+
+ List<ResponderId> rids = new ArrayList<>();
+ List<Extension> exts = new ArrayList<>();
+ ByteBuffer m = ByteBuffer.wrap(encoded);
+
+ this.ridListLen = Record.getInt16(m);
+ if (m.remaining() < (ridListLen + 2)) {
+ throw new SSLProtocolException(
+ "Invalid OCSP status request: insufficient data");
+ }
+
+ int ridListBytesRemaining = ridListLen;
+ while (ridListBytesRemaining >= 2) { // 2: length of responder_id
+ byte[] ridBytes = Record.getBytes16(m);
+ try {
+ rids.add(new ResponderId(ridBytes));
+ } catch (IOException ioe) {
+ throw new SSLProtocolException(
+ "Invalid OCSP status request: invalid responder ID");
+ }
+ ridListBytesRemaining -= ridBytes.length + 2;
+ }
+
+ if (ridListBytesRemaining != 0) {
+ throw new SSLProtocolException(
+ "Invalid OCSP status request: incomplete data");
+ }
+
+ byte[] extListBytes = Record.getBytes16(m);
+ this.extListLen = extListBytes.length;
+ if (extListLen > 0) {
+ try {
+ DerInputStream dis = new DerInputStream(extListBytes);
+ DerValue[] extSeqContents =
+ dis.getSequence(extListBytes.length);
+ for (DerValue extDerVal : extSeqContents) {
+ exts.add(new sun.security.x509.Extension(extDerVal));
+ }
+ } catch (IOException ioe) {
+ throw new SSLProtocolException(
+ "Invalid OCSP status request: invalid extension");
+ }
+ }
+
+ this.responderIds = rids;
+ this.extensions = exts;
+ }
+
+ @Override
+ public String toString() {
+ MessageFormat messageFormat = new MessageFormat(
+ "\"certificate status type\": {0}\n" +
+ "\"OCSP status request\": '{'\n" +
+ "{1}\n" +
+ "'}'",
+ Locale.ENGLISH);
+
+ MessageFormat requestFormat = new MessageFormat(
+ "\"responder_id\": {0}\n" +
+ "\"request extensions\": '{'\n" +
+ "{1}\n" +
+ "'}'",
+ Locale.ENGLISH);
+
+ String ridStr = "<empty>";
+ if (!responderIds.isEmpty()) {
+ ridStr = responderIds.toString();
+
+ }
+
+ String extsStr = "<empty>";
+ if (!extensions.isEmpty()) {
+ StringBuilder extBuilder = new StringBuilder(512);
+ boolean isFirst = true;
+ for (Extension ext : this.extensions) {
+ if (isFirst) {
+ isFirst = false;
} else {
- // Treat this as a zero-length extension (i.e. from a ServerHello
- statReqType = null;
- request = null;
+ extBuilder.append(",\n");
+ }
+ extBuilder.append(
+ "{\n" + Utilities.indent(ext.toString()) + "}");
+ }
+
+ extsStr = extBuilder.toString();
+ }
+
+ Object[] requestFields = {
+ ridStr,
+ Utilities.indent(extsStr)
+ };
+ String ocspStatusRequest = requestFormat.format(requestFields);
+
+ Object[] messageFields = {
+ CertStatusRequestType.nameOf(statusType),
+ Utilities.indent(ocspStatusRequest)
+ };
+
+ return messageFormat.format(messageFields);
+ }
+ }
+
+ static class CertStatusResponse {
+ final byte statusType;
+ final byte[] encodedResponse;
+
+ protected CertStatusResponse(byte statusType, byte[] respDer) {
+ this.statusType = statusType;
+ this.encodedResponse = respDer;
+ }
+
+ byte[] toByteArray() throws IOException {
+ // Create a byte array large enough to handle the status_type
+ // field (1) + OCSP length (3) + OCSP data (variable)
+ byte[] outData = new byte[encodedResponse.length + 4];
+ ByteBuffer buf = ByteBuffer.wrap(outData);
+ Record.putInt8(buf, statusType);
+ Record.putBytes24(buf, encodedResponse);
+ return buf.array();
+ }
+
+ @Override
+ public String toString() {
+ MessageFormat messageFormat = new MessageFormat(
+ "\"certificate status response type\": {0}\n" +
+ "\"encoded certificate status\": '{'\n" +
+ "{1}\n" +
+ "'}'",
+ Locale.ENGLISH);
+
+ HexDumpEncoder hexEncoder = new HexDumpEncoder();
+ String encoded = hexEncoder.encodeBuffer(encodedResponse);
+
+ Object[] messageFields = {
+ CertStatusRequestType.nameOf(statusType),
+ Utilities.indent(encoded)
+ };
+
+ return messageFormat.format(messageFields);
+ }
+ }
+
+ static final class OCSPStatusResponse extends CertStatusResponse {
+ final OCSPResponse ocspResponse;
+
+ private OCSPStatusResponse(byte statusType,
+ byte[] encoded) throws IOException {
+ super(statusType, encoded);
+
+ // The DER-encoded OCSP response must not be zero length
+ if (encoded == null || encoded.length < 1) {
+ throw new SSLProtocolException(
+ "Invalid OCSP status response: insufficient data");
+ }
+
+ // Otherwise, make an OCSPResponse object from the data
+ ocspResponse = new OCSPResponse(encoded);
+ }
+
+ @Override
+ public String toString() {
+ MessageFormat messageFormat = new MessageFormat(
+ "\"certificate status response type\": {0}\n" +
+ "\"OCSP status response\": '{'\n" +
+ "{1}\n" +
+ "'}'",
+ Locale.ENGLISH);
+
+ Object[] messageFields = {
+ CertStatusRequestType.nameOf(statusType),
+ Utilities.indent(ocspResponse.toString())
+ };
+
+ return messageFormat.format(messageFields);
}
}
/**
- * Return the length of the encoded extension, including extension type,
- * extension length and status_type fields.
- *
- * @return the length in bytes, including the extension type and
- * length fields.
+ * Network data producer of a "status_request" extension in the
+ * ClientHello handshake message.
*/
+ private static final
+ class CHCertStatusReqProducer implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private CHCertStatusReqProducer() {
+ // blank
+ }
+
@Override
- int length() {
- return (statReqType != null ? 5 + request.length() : 4);
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ // The producing happens in client side only.
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ if (!chc.sslContext.isStaplingEnabled(true)) {
+ return null;
+ }
+
+ if (!chc.sslConfig.isAvailable(CH_STATUS_REQUEST)) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.fine(
+ "Ignore unavailable extension: " +
+ CH_STATUS_REQUEST.name);
+ }
+ return null;
+ }
+
+ // Produce the extension.
+ //
+ // We are using empty OCSPStatusRequest at present. May extend to
+ // support specific responder or extensions later.
+ byte[] extData = new byte[] {0x01, 0x00, 0x00, 0x00, 0x00};
+
+ // Update the context.
+ chc.handshakeExtensions.put(
+ CH_STATUS_REQUEST, CertStatusRequestSpec.DEFAULT);
+
+ return extData;
+ }
}
/**
- * Send the encoded TLS extension through a {@code HandshakeOutputStream}
- *
- * @param s the {@code HandshakeOutputStream} used to send the encoded data
- *
- * @throws IOException tf any errors occur during the encoding process
+ * Network data consumer of a "status_request" extension in the
+ * ClientHello handshake message.
*/
+ private static final
+ class CHCertStatusReqConsumer implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private CHCertStatusReqConsumer() {
+ // blank
+ }
+
@Override
- void send(HandshakeOutStream s) throws IOException {
- s.putInt16(type.id);
- s.putInt16(this.length() - 4);
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer) throws IOException {
+
+ // The comsuming happens in server side only.
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+ if (!shc.sslConfig.isAvailable(CH_STATUS_REQUEST)) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.fine("Ignore unavailable extension: " +
+ CH_STATUS_REQUEST.name);
+ }
+ return; // ignore the extension
+ }
+
+ // Parse the extension.
+ CertStatusRequestSpec spec;
+ try {
+ spec = new CertStatusRequestSpec(buffer);
+ } catch (IOException ioe) {
+ shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
+ return; // fatal() always throws, make the compiler happy.
+ }
- if (statReqType != null) {
- s.putInt8(statReqType.id);
- request.send(s);
+ // Update the context.
+ shc.handshakeExtensions.put(CH_STATUS_REQUEST, spec);
+ if (!shc.negotiatedProtocol.useTLS13PlusSpec()) {
+ shc.handshakeProducers.put(SSLHandshake.CERTIFICATE_STATUS.id,
+ SSLHandshake.CERTIFICATE_STATUS);
+ } // Otherwise, the certificate status presents in server cert.
+
+ // No impact on session resumption.
}
}
/**
- * Create a string representation of this {@code CertStatusReqExtension}
+ * Network data producer of a "status_request" extension in the
+ * ServerHello handshake message.
+ */
+ private static final
+ class SHCertStatusReqProducer implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private SHCertStatusReqProducer() {
+ // blank
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ // The producing happens in client side only.
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+ // The StaplingParameters in the ServerHandshakeContext will
+ // contain the info about what kind of stapling (if any) to
+ // perform and whether this status_request extension should be
+ // produced or the status_request_v2 (found in a different producer)
+ // No explicit check is required for isStaplingEnabled here. If
+ // it is false then stapleParams will be null. If it is true
+ // then stapleParams may or may not be false and the check below
+ // is sufficient.
+ if ((shc.stapleParams == null) ||
+ (shc.stapleParams.statusRespExt !=
+ SSLExtension.CH_STATUS_REQUEST)) {
+ return null; // Do not produce status_request in ServerHello
+ }
+
+ // In response to "status_request" extension request only.
+ CertStatusRequestSpec spec = (CertStatusRequestSpec)
+ shc.handshakeExtensions.get(CH_STATUS_REQUEST);
+ if (spec == null) {
+ // Ignore, no status_request extension requested.
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "Ignore unavailable extension: " +
+ CH_STATUS_REQUEST.name);
+ }
+
+ return null; // ignore the extension
+ }
+
+ // Is it a session resuming?
+ if (shc.isResumption) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "No status_request response for session resuming");
+ }
+
+ return null; // ignore the extension
+ }
+
+ // The "extension_data" in the extended ServerHello handshake
+ // message MUST be empty.
+ byte[] extData = new byte[0];
+
+ // Update the context.
+ shc.handshakeExtensions.put(
+ SH_STATUS_REQUEST, CertStatusRequestSpec.DEFAULT);
+
+ return extData;
+ }
+ }
+
+ /**
+ * Network data consumer of a "status_request" extension in the
+ * ServerHello handshake message.
+ */
+ private static final
+ class SHCertStatusReqConsumer implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private SHCertStatusReqConsumer() {
+ // blank
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer) throws IOException {
+
+ // The producing happens in client side only.
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ // In response to "status_request" extension request only.
+ CertStatusRequestSpec requestedCsr = (CertStatusRequestSpec)
+ chc.handshakeExtensions.get(CH_STATUS_REQUEST);
+ if (requestedCsr == null) {
+ chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+ "Unexpected status_request extension in ServerHello");
+ }
+
+ // Parse the extension.
+ if (buffer.hasRemaining()) {
+ chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+ "Invalid status_request extension in ServerHello message: " +
+ "the extension data must be empty");
+ }
+
+ // Update the context.
+ chc.handshakeExtensions.put(
+ SH_STATUS_REQUEST, CertStatusRequestSpec.DEFAULT);
+ chc.handshakeConsumers.put(SSLHandshake.CERTIFICATE_STATUS.id,
+ SSLHandshake.CERTIFICATE_STATUS);
+
+ // Since we've received a legitimate status_request in the
+ // ServerHello, stapling is active if it's been enabled.
+ chc.staplingActive = chc.sslContext.isStaplingEnabled(true);
+
+ // No impact on session resumption.
+ }
+ }
+
+ /**
+ * The "status_request_v2" extension.
+ *
+ * RFC6961 defines the TLS extension,"status_request_v2" (type 0x5),
+ * which allows the client to request that the server perform OCSP
+ * on the client's behalf.
+ *
+ * The RFC defines an CertStatusReqItemV2 structure:
+ *
+ * struct {
+ * CertificateStatusType status_type;
+ * uint16 request_length;
+ * select (status_type) {
+ * case ocsp: OCSPStatusRequest;
+ * case ocsp_multi: OCSPStatusRequest;
+ * } request;
+ * } CertificateStatusRequestItemV2;
+ *
+ * enum { ocsp(1), ocsp_multi(2), (255) } CertificateStatusType;
+ * struct {
+ * ResponderID responder_id_list<0..2^16-1>;
+ * Extensions request_extensions;
+ * } OCSPStatusRequest;
*
- * @return the string representation of this {@code CertStatusReqExtension}
+ * opaque ResponderID<1..2^16-1>;
+ * opaque Extensions<0..2^16-1>;
+ *
+ * struct {
+ * CertificateStatusRequestItemV2
+ * certificate_status_req_list<1..2^16-1>;
+ * } CertificateStatusRequestListV2;
*/
+ static final class CertStatusRequestV2Spec implements SSLExtensionSpec {
+ static final CertStatusRequestV2Spec DEFAULT =
+ new CertStatusRequestV2Spec(new CertStatusRequest[] {
+ OCSPStatusRequest.EMPTY_OCSP_MULTI});
+
+ final CertStatusRequest[] certStatusRequests;
+
+ private CertStatusRequestV2Spec(CertStatusRequest[] certStatusRequests) {
+ this.certStatusRequests = certStatusRequests;
+ }
+
+ private CertStatusRequestV2Spec(ByteBuffer message) throws IOException {
+ // Is it a empty extension_data?
+ if (message.remaining() == 0) {
+ // server response
+ this.certStatusRequests = new CertStatusRequest[0];
+ return;
+ }
+
+ if (message.remaining() < 5) { // 2: certificate_status_req_list
+ // +1: status_type
+ // +2: request_length
+ throw new SSLProtocolException(
+ "Invalid status_request_v2 extension: insufficient data");
+ }
+
+ int listLen = Record.getInt16(message);
+ if (listLen <= 0) {
+ throw new SSLProtocolException(
+ "certificate_status_req_list length must be positive " +
+ "(received length: " + listLen + ")");
+ }
+
+ int remaining = listLen;
+ List<CertStatusRequest> statusRequests = new ArrayList<>();
+ while (remaining > 0) {
+ byte statusType = (byte)Record.getInt8(message);
+ int requestLen = Record.getInt16(message);
+
+ if (message.remaining() < requestLen) {
+ throw new SSLProtocolException(
+ "Invalid status_request_v2 extension: " +
+ "insufficient data (request_length=" + requestLen +
+ ", remining=" + message.remaining() + ")");
+ }
+
+ byte[] encoded = new byte[requestLen];
+ if (encoded.length != 0) {
+ message.get(encoded);
+ }
+ remaining -= 3; // 1(status type) + 2(request_length) bytes
+ remaining -= requestLen;
+
+ if (statusType == CertStatusRequestType.OCSP.id ||
+ statusType == CertStatusRequestType.OCSP_MULTI.id) {
+ if (encoded.length < 4) {
+ // 2: length of responder_id_list
+ // +2: length of request_extensions
+ throw new SSLProtocolException(
+ "Invalid status_request_v2 extension: " +
+ "insufficient data");
+ }
+ statusRequests.add(
+ new OCSPStatusRequest(statusType, encoded));
+ } else {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.info(
+ "Unknown certificate status request " +
+ "(status type: " + statusType + ")");
+ }
+ statusRequests.add(
+ new CertStatusRequest(statusType, encoded));
+ }
+ }
+
+ certStatusRequests =
+ statusRequests.toArray(new CertStatusRequest[0]);
+ }
+
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("Extension ").append(type);
- if (statReqType != null) {
- sb.append(": ").append(statReqType).append(", ").append(request);
+ if (certStatusRequests == null || certStatusRequests.length == 0) {
+ return "<empty>";
+ } else {
+ MessageFormat messageFormat = new MessageFormat(
+ "\"cert status request\": '{'\n{0}\n'}'", Locale.ENGLISH);
+
+ StringBuilder builder = new StringBuilder(512);
+ boolean isFirst = true;
+ for (CertStatusRequest csr : certStatusRequests) {
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ builder.append(", ");
+ }
+ Object[] messageFields = {
+ Utilities.indent(csr.toString())
+ };
+ builder.append(messageFormat.format(messageFields));
+ }
+
+ return builder.toString();
+ }
+ }
}
- return sb.toString();
+ private static final
+ class CertStatusRequestsStringize implements SSLStringize {
+ @Override
+ public String toString(ByteBuffer buffer) {
+ try {
+ return (new CertStatusRequestV2Spec(buffer)).toString();
+ } catch (IOException ioe) {
+ // For debug logging only, so please swallow exceptions.
+ return ioe.getMessage();
+ }
+ }
}
/**
- * Return the type field for this {@code CertStatusReqExtension}
- *
- * @return the {@code StatusRequestType} for this extension. {@code null}
- * will be returned if the default constructor is used to create
- * a zero length status_request extension (found in ServerHello
- * messages)
+ * Network data producer of a "status_request_v2" extension in the
+ * ClientHello handshake message.
+ */
+ private static final
+ class CHCertStatusReqV2Producer implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private CHCertStatusReqV2Producer() {
+ // blank
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ // The producing happens in client side only.
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ if (!chc.sslContext.isStaplingEnabled(true)) {
+ return null;
+ }
+
+ if (!chc.sslConfig.isAvailable(CH_STATUS_REQUEST_V2)) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "Ignore unavailable status_request_v2 extension");
+ }
+
+ return null;
+ }
+
+ // Produce the extension.
+ //
+ // We are using empty OCSPStatusRequest at present. May extend to
+ // support specific responder or extensions later.
+ byte[] extData = new byte[] {
+ 0x00, 0x07, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00};
+
+ // Update the context.
+ chc.handshakeExtensions.put(
+ CH_STATUS_REQUEST_V2, CertStatusRequestV2Spec.DEFAULT);
+
+ return extData;
+ }
+ }
+
+ /**
+ * Network data consumer of a "status_request_v2" extension in the
+ * ClientHello handshake message.
*/
- StatusRequestType getType() {
- return statReqType;
+ private static final
+ class CHCertStatusReqV2Consumer implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private CHCertStatusReqV2Consumer() {
+ // blank
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer) throws IOException {
+
+ // The comsuming happens in server side only.
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+
+ if (!shc.sslConfig.isAvailable(CH_STATUS_REQUEST_V2)) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "Ignore unavailable status_request_v2 extension");
+ }
+
+ return; // ignore the extension
+ }
+
+ // Parse the extension.
+ CertStatusRequestV2Spec spec;
+ try {
+ spec = new CertStatusRequestV2Spec(buffer);
+ } catch (IOException ioe) {
+ shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
+ return; // fatal() always throws, make the compiler happy.
+ }
+
+ // Update the context.
+ shc.handshakeExtensions.put(CH_STATUS_REQUEST_V2, spec);
+ shc.handshakeProducers.putIfAbsent(
+ SSLHandshake.CERTIFICATE_STATUS.id,
+ SSLHandshake.CERTIFICATE_STATUS);
+ // No impact on session resumption.
+ }
}
/**
- * Get the underlying {@code StatusRequest} for this
- * {@code CertStatusReqExtension}
- *
- * @return the {@code StatusRequest} or {@code null} if the default
- * constructor was used to create this extension.
+ * Network data producer of a "status_request_v2" extension in the
+ * ServerHello handshake message.
+ */
+ private static final
+ class SHCertStatusReqV2Producer implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private SHCertStatusReqV2Producer() {
+ // blank
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ // The producing happens in client side only.
+
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+ // The StaplingParameters in the ServerHandshakeContext will
+ // contain the info about what kind of stapling (if any) to
+ // perform and whether this status_request extension should be
+ // produced or the status_request_v2 (found in a different producer)
+ // No explicit check is required for isStaplingEnabled here. If
+ // it is false then stapleParams will be null. If it is true
+ // then stapleParams may or may not be false and the check below
+ // is sufficient.
+ if ((shc.stapleParams == null) ||
+ (shc.stapleParams.statusRespExt !=
+ SSLExtension.CH_STATUS_REQUEST_V2)) {
+ return null; // Do not produce status_request_v2 in SH
+ }
+
+ // In response to "status_request_v2" extension request only
+ CertStatusRequestV2Spec spec = (CertStatusRequestV2Spec)
+ shc.handshakeExtensions.get(CH_STATUS_REQUEST_V2);
+ if (spec == null) {
+ // Ignore, no status_request_v2 extension requested.
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "Ignore unavailable status_request_v2 extension");
+ }
+
+ return null; // ignore the extension
+ }
+
+ // Is it a session resuming?
+ if (shc.isResumption) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "No status_request_v2 response for session resumption");
+ }
+ return null; // ignore the extension
+ }
+
+ // The "extension_data" in the extended ServerHello handshake
+ // message MUST be empty.
+ byte[] extData = new byte[0];
+
+ // Update the context.
+ shc.handshakeExtensions.put(
+ SH_STATUS_REQUEST_V2, CertStatusRequestV2Spec.DEFAULT);
+
+ return extData;
+ }
+ }
+
+ /**
+ * Network data consumer of a "status_request_v2" extension in the
+ * ServerHello handshake message.
*/
- StatusRequest getRequest() {
- return request;
+ private static final
+ class SHCertStatusReqV2Consumer implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private SHCertStatusReqV2Consumer() {
+ // blank
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer) throws IOException {
+
+ // The consumption happens in client side only.
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ // In response to "status_request" extension request only
+ CertStatusRequestV2Spec requestedCsr = (CertStatusRequestV2Spec)
+ chc.handshakeExtensions.get(CH_STATUS_REQUEST_V2);
+ if (requestedCsr == null) {
+ chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+ "Unexpected status_request_v2 extension in ServerHello");
+ }
+
+ // Parse the extension.
+ if (buffer.hasRemaining()) {
+ chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
+ "Invalid status_request_v2 extension in ServerHello: " +
+ "the extension data must be empty");
+ }
+
+ // Update the context.
+ chc.handshakeExtensions.put(
+ SH_STATUS_REQUEST_V2, CertStatusRequestV2Spec.DEFAULT);
+ chc.handshakeConsumers.put(SSLHandshake.CERTIFICATE_STATUS.id,
+ SSLHandshake.CERTIFICATE_STATUS);
+
+ // Since we've received a legitimate status_request in the
+ // ServerHello, stapling is active if it's been enabled.
+ chc.staplingActive = chc.sslContext.isStaplingEnabled(true);
+
+ // No impact on session resumption.
+ }
+ }
+
+ private static final
+ class CTCertStatusResponseProducer implements HandshakeProducer {
+ // Prevent instantiation of this class.
+ private CTCertStatusResponseProducer() {
+ // blank
+ }
+
+ @Override
+ public byte[] produce(ConnectionContext context,
+ HandshakeMessage message) throws IOException {
+ ServerHandshakeContext shc = (ServerHandshakeContext)context;
+ byte[] producedData = null;
+
+ // Stapling needs to be active and have valid data to proceed
+ if (shc.stapleParams == null) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest(
+ "Stapling is disabled for this connection");
+ }
+ return null;
+ }
+
+ // There needs to be a non-null CertificateEntry to proceed
+ if (shc.currentCertEntry == null) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
+ SSLLogger.finest("Found null CertificateEntry in context");
+ }
+ return null;
+ }
+
+ // Pull the certificate from the CertificateEntry and find
+ // a response from the response map. If one exists we will
+ // staple it.
+ try {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate x509Cert =
+ (X509Certificate)cf.generateCertificate(
+ new ByteArrayInputStream(
+ shc.currentCertEntry.encoded));
+ byte[] respBytes = shc.stapleParams.responseMap.get(x509Cert);
+ if (respBytes == null) {
+ // We're done with this entry. Clear it from the context
+ if (SSLLogger.isOn &&
+ SSLLogger.isOn("ssl,handshake,verbose")) {
+ SSLLogger.finest("No status response found for " +
+ x509Cert.getSubjectX500Principal());
+ }
+ shc.currentCertEntry = null;
+ return null;
+ }
+
+ // Build a proper response buffer from the stapling information
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake,verbose")) {
+ SSLLogger.finest("Found status response for " +
+ x509Cert.getSubjectX500Principal() +
+ ", response length: " + respBytes.length);
+ }
+ CertStatusResponse certResp = (shc.stapleParams.statReqType ==
+ CertStatusRequestType.OCSP) ?
+ new OCSPStatusResponse(shc.stapleParams.statReqType.id,
+ respBytes) :
+ new CertStatusResponse(shc.stapleParams.statReqType.id,
+ respBytes);
+ producedData = certResp.toByteArray();
+ } catch (CertificateException ce) {
+ shc.conContext.fatal(Alert.BAD_CERTIFICATE,
+ "Failed to parse server certificates", ce);
+ } catch (IOException ioe) {
+ shc.conContext.fatal(Alert.BAD_CERT_STATUS_RESPONSE,
+ "Failed to parse certificate status response", ioe);
+ }
+
+ // Clear the pinned CertificateEntry from the context
+ shc.currentCertEntry = null;
+ return producedData;
+ }
+ }
+
+ private static final
+ class CTCertStatusResponseConsumer implements ExtensionConsumer {
+ // Prevent instantiation of this class.
+ private CTCertStatusResponseConsumer() {
+ // blank
+ }
+
+ @Override
+ public void consume(ConnectionContext context,
+ HandshakeMessage message, ByteBuffer buffer) throws IOException {
+ // The consumption happens in client side only.
+ ClientHandshakeContext chc = (ClientHandshakeContext)context;
+
+ // Parse the extension.
+ CertStatusResponseSpec spec;
+ try {
+ spec = new CertStatusResponseSpec(buffer);
+ } catch (IOException ioe) {
+ chc.conContext.fatal(Alert.DECODE_ERROR, ioe);
+ return; // fatal() always throws, make the compiler happy.
+ }
+
+ if (chc.sslContext.isStaplingEnabled(true)) {
+ // Activate stapling
+ chc.staplingActive = true;
+ } else {
+ // Do no further processing of stapled responses
+ return;
+ }
+
+ // Get response list from the session. This is unmodifiable
+ // so we need to create a new list. Then add this new response
+ // to the end and submit it back to the session object.
+ if ((chc.handshakeSession != null) && (!chc.isResumption)) {
+ List<byte[]> respList = new ArrayList<>(
+ chc.handshakeSession.getStatusResponses());
+ respList.add(spec.statusResponse.encodedResponse);
+ chc.handshakeSession.setStatusResponses(respList);
+ } else {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake,verbose")) {
+ SSLLogger.finest(
+ "Ignoring stapled data on resumed session");
+ }
+ }
+ }
}
}
< prev index next >