blob: c092fb0c212ad522ca6678b25f997cb13205a21d [file] [log] [blame]
/*
* Copyright (c) 2015, 2020, 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
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.ssl;
import java.io.IOException;
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 sun.security.ssl.SSLExtension.ExtensionConsumer;
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 SSLStringizer certStatusReqStringizer =
new CertStatusRequestStringizer();
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 SSLStringizer certStatusReqV2Stringizer =
new CertStatusRequestsStringizer();
static final SSLStringizer certStatusRespStringizer =
new CertStatusRespStringizer();
/**
* 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;
* select (status_type) {
* case ocsp: OCSPStatusRequest;
* } request;
* } CertificateStatusRequest;
*
* enum { ocsp(1), (255) } CertificateStatusType;
*
* struct {
* ResponderID responder_id_list<0..2^16-1>;
* Extensions request_extensions;
* } 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 CertStatusRequest statusRequest;
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;
}
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();
}
}
/**
* 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.
*
* struct {
* CertificateStatusType status_type;
* select (status_type) {
* case ocsp: OCSPResponse;
* } response;
* } CertificateStatus;
*/
static final class CertStatusResponseSpec implements SSLExtensionSpec {
final CertStatusResponse statusResponse;
private CertStatusResponseSpec(CertStatusResponse resp) {
this.statusResponse = resp;
}
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 CertStatusRequestStringizer implements SSLStringizer {
@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 CertStatusRespStringizer implements SSLStringizer {
@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;
}
/**
* 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.
*
* The RFC defines an OCSPStatusRequest structure:
*
* struct {
* ResponderID responder_id_list<0..2^16-1>;
* Extensions request_extensions;
* } OCSPStatusRequest;
*/
static final class OCSPStatusRequest extends CertStatusRequest {
static final OCSPStatusRequest EMPTY_OCSP;
static final OCSPStatusRequest EMPTY_OCSP_MULTI;
final List<ResponderId> responderIds;
final List<Extension> extensions;
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");
}
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 {
extBuilder.append(",\n");
}
extBuilder.append("{\n").
append(Utilities.indent(ext.toString())).
append("}");
}
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);
}
}
/**
* 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
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(SSLExtension.CH_STATUS_REQUEST)) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine(
"Ignore unavailable extension: " +
SSLExtension.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(SSLExtension.CH_STATUS_REQUEST,
CertStatusRequestSpec.DEFAULT);
return extData;
}
}
/**
* 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
public void consume(ConnectionContext context,
HandshakeMessage message, ByteBuffer buffer) throws IOException {
// The consuming happens in server side only.
ServerHandshakeContext shc = (ServerHandshakeContext)context;
if (!shc.sslConfig.isAvailable(SSLExtension.CH_STATUS_REQUEST)) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine("Ignore unavailable extension: " +
SSLExtension.CH_STATUS_REQUEST.name);
}
return; // ignore the extension
}
// Parse the extension.
CertStatusRequestSpec spec;
try {
spec = new CertStatusRequestSpec(buffer);
} catch (IOException ioe) {
throw shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
}
// Update the context.
shc.handshakeExtensions.put(SSLExtension.CH_STATUS_REQUEST, spec);
if (!shc.isResumption &&
!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.
}
}
/**
* 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(SSLExtension.CH_STATUS_REQUEST);
if (spec == null) {
// Ignore, no status_request extension requested.
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.finest("Ignore unavailable extension: " +
SSLExtension.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(SSLExtension.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(SSLExtension.CH_STATUS_REQUEST);
if (requestedCsr == null) {
throw chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
"Unexpected status_request extension in ServerHello");
}
// Parse the extension.
if (buffer.hasRemaining()) {
throw 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(SSLExtension.SH_STATUS_REQUEST,
CertStatusRequestSpec.DEFAULT);
// 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);
if (chc.staplingActive) {
chc.handshakeConsumers.put(SSLHandshake.CERTIFICATE_STATUS.id,
SSLHandshake.CERTIFICATE_STATUS);
}
// 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;
*
* 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() {
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();
}
}
}
private static final
class CertStatusRequestsStringizer implements SSLStringizer {
@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();
}
}
}
/**
* 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(SSLExtension.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(SSLExtension.CH_STATUS_REQUEST_V2,
CertStatusRequestV2Spec.DEFAULT);
return extData;
}
}
/**
* Network data consumer of a "status_request_v2" extension in the
* ClientHello handshake message.
*/
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 consuming happens in server side only.
ServerHandshakeContext shc = (ServerHandshakeContext)context;
if (!shc.sslConfig.isAvailable(SSLExtension.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) {
throw shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe);
}
// Update the context.
shc.handshakeExtensions.put(SSLExtension.CH_STATUS_REQUEST_V2,
spec);
if (!shc.isResumption) {
shc.handshakeProducers.putIfAbsent(
SSLHandshake.CERTIFICATE_STATUS.id,
SSLHandshake.CERTIFICATE_STATUS);
}
// No impact on session resumption.
}
}
/**
* 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(SSLExtension.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(SSLExtension.SH_STATUS_REQUEST_V2,
CertStatusRequestV2Spec.DEFAULT);
return extData;
}
}
/**
* Network data consumer of a "status_request_v2" extension in the
* ServerHello handshake message.
*/
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(SSLExtension.CH_STATUS_REQUEST_V2);
if (requestedCsr == null) {
throw chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE,
"Unexpected status_request_v2 extension in ServerHello");
}
// Parse the extension.
if (buffer.hasRemaining()) {
throw 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(SSLExtension.SH_STATUS_REQUEST_V2,
CertStatusRequestV2Spec.DEFAULT);
// Since we've received a legitimate status_request in the
// ServerHello, stapling is active if it's been enabled. If it
// is active, make sure we add the CertificateStatus message
// consumer.
chc.staplingActive = chc.sslContext.isStaplingEnabled(true);
if (chc.staplingActive) {
chc.handshakeConsumers.put(SSLHandshake.CERTIFICATE_STATUS.id,
SSLHandshake.CERTIFICATE_STATUS);
}
// 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) {
throw shc.conContext.fatal(Alert.BAD_CERTIFICATE,
"Failed to parse server certificates", ce);
} catch (IOException ioe) {
throw 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) {
throw chc.conContext.fatal(Alert.DECODE_ERROR, ioe);
}
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");
}
}
}
}
}