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