mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 06:45:07 +02:00

Co-authored-by: Adam Petcher <adam.petcher@oracle.com> Co-authored-by: Amanda Jiang <amanda.jiang@oracle.com> Co-authored-by: Anthony Scarpino <anthony.scarpino@oracle.com> Co-authored-by: Bradford Wetmore <bradford.wetmore@oracle.com> Co-authored-by: Jamil Nimeh <jamil.j.nimeh@oracle.com> Co-authored-by: John Jiang <sha.jiang@oracle.com> Co-authored-by: Rajan Halade <rajan.halade@oracle.com> Co-authored-by: Sibabrata Sahoo <sibabrata.sahoo@oracle.com> Co-authored-by: Valerie Peng <valerie.peng@oracle.com> Co-authored-by: Weijun Wang <weijun.wang@oracle.com> Reviewed-by: ascarpino, coffeys, dfuchs, jjiang, jnimeh, mullan, rhalade, ssahoo, valeriep, weijun, wetmore, xuelei
1219 lines
46 KiB
Java
1219 lines
46 KiB
Java
/*
|
|
* 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
|
|
* 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 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 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" + 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(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.
|
|
}
|
|
|
|
// Update the context.
|
|
shc.handshakeExtensions.put(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(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;
|
|
*
|
|
* 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(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.
|
|
*/
|
|
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(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);
|
|
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(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.
|
|
*/
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|