JEP 114: TLS SNI Extension - Virtual Servers Dispatcher


The implementation of JEP 114 (TLS Server Name Indication (SNI) Extension) had integrated into JDK 8 at October, 2012. In the previous blog entries, we talked about the behavior changes in JSSE, and a few typical user cases. Let's look at a special user case, how to design a virtual server dispatcher in Java. Please refer to javax.net.ssl package of JDK 8 APIs for the detailed specification.


Prepare the ClientHello Parser


Applications need to implementation their own APIs to parser the client hello message from a plaintext socket. Suppose that an application design the following API to do the work, SSLCapabilities and SSLExplorer.

SSLCapabilities is defined to show the SSL/TLS security capabilities during handshaking, SSLCapabilities can be retrieved by exploring the network data of an SSL/TLS connection via SSLExplorer.explore(ByteBuffer).

/**
 * Encapsulates the security capabilities
 * of an SSL/TLS connection.
 * 
 * The security capabilities are the list
 * ciphersuites to be accepted in an SSL/TLS
 * handshake, the record version, the hello
 * version, and server name indication, etc.,
 * of an SSL/TLS connection.
 * 
 * {@code SSLCapabilities} can be retrieved by exploring the network
 * data of an SSL/TLS connection via {@link SSLExplorer#explore(ByteBuffer)}
 * or {@link SSLExplorer#explore(byte[], int, int)}.
 *
 * @see SSLExplorer
 */
public abstract class SSLCapabilities {

    /**
     * Returns the record version of an SSL/TLS connection
     *
     * @return a non-null record version
     */
    public abstract String getRecordVersion();

    /**
     * Returns the hello version of an SSL/TLS connection
     *
     * @return a non-null hello version
     */
    public abstract String getHelloVersion();

    /**
     * Returns a {@code List} containing all
     * {@link SNIServerName}s of the server name indication.
     *
     * @return a non-null immutable list of {@link SNIServerName}s
     *         of the server name indication parameter, may be empty
     *         if no server name indication.
     *
     * @see SNIServerName
     */
    public abstract List<SNIServerName> getServerNames();
}
SSLExplorer is defined to explore the initial ClientHello message from TLS Client. But it does not kick off handshaking, or consume network data. The SSLExplorer.explore() method parses the ClientHello message, and retrieve the security parameters from ClientHello message into SSLCapabilities.

This method must be called before handshaking occurs on any TLS connections.
/**
 * Instances of this class acts as an explorer of the network data of an
 * SSL/TLS connection.
 */
public final class SSLExplorer {

    /**
     * The header size of TLS/SSL records.
     * <P>
     * The value of this constant is {@value}.
     */
    public final static int RECORD_HEADER_SIZE = 0x05;

    /**
     * Returns the required size in byte from byte buffer to explore an
     * SSL/TLS connection.
     * <P>
     * This method tries to parse as few bytes as possible from
     * {@code source} byte buffer to get the length of an
     * SSL/TLS record.
     *
     * @param  source
     *         a {@code ByteBuffer} containing
     *         inbound or outbound network data for an SSL/TLS connection.
     *
     * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE}
     *         bytes remaining in {@code source}
     */
    public final static int getRequiredSize(ByteBuffer source);

    /**
     * Returns the required size in byte from byte array to explore an
     * SSL/TLS connection.
     * <P>
     * This method tries to parse as few bytes as possible from
     * {@code source} byte array to get the length of an
     * SSL/TLS record.
     *
     * @param  source
     *         a byte array containing inbound or outbound network data for
     *         an SSL/TLS connection.
     * @param  offset
     *         the start offset in array {@code source} at which the
     *         network data is read from.
     * @param  length
     *         the maximum number of bytes to read.
     *
     * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE}
     *         bytes remaining in {@code source}
     */
    public final static int getRequiredSize(byte[] source,
            int offset, int lenght) throws IOException;

    /**
     * Launch and explore the security capabilities from byte buffer.
     * <P>
     * This method tries to parse as few records as possible from
     * {@code source} byte buffer to get the {@code SSLCapabilities}
     * of an SSL/TLS connection.
     * <P>
     * Please NOTE that this method must be called before any handshaking
     * occurs.  The behavior of this method is not defined in this release
     * if the handshake has begun, or has completed.
     * <P>
     * This method accesses the {@code source} parameter in read-only
     * mode, and does not update the buffer's properties such as capacity,
     * limit, position, and mark values.
     *
     * @param  source
     *         a {@code ByteBuffer} containing
     *         inbound or outbound network data for an SSL/TLS connection.
     *
     * @throws IOException on network data error
     * @throws BufferUnderflowException if no enough source bytes available
     *         to make a complete exploration.
     *
     * @return the explored {@code SSLCapabilities} of the SSL/TLS
     *         connection
     */
    public final static SSLCapabilities explore(ByteBuffer source)
            throws IOException;

    /**
     * Launch and explore the security capabilities from byte array.
     * <P>
     * Please NOTE that this method must be called before any handshaking
     * occurs.  The behavior of this method is not defined in this release
     * if the handshake has begun, or has completed.
     * <P>
     * Please NOTE that this method must be called before any handshaking
     * occurs.  Once handshake has begun, or has completed, the security
     * capabilities can not and should not be launched with this method.
     *
     * @param  source
     *         a byte array containing inbound or outbound network data for
     *         an SSL/TLS connection.
     * @param  offset
     *         the start offset in array {@code source} at which the
     *         network data is read from.
     * @param  length
     *         the maximum number of bytes to read.
     *
     * @throws IOException on network data error
     * @throws BufferUnderflowException if no enough source bytes available
     *         to make a complete exploration.
     * @return the explored {@code SSLCapabilities} of the SSL/TLS
     *         connection
     *
     * @see #explore(ByteBuffer)
     */
    public final static SSLCapabilities explore(byte[] source,
            int offset, int length) throws IOException;
}

Please note that the above two classes is not part of JDK. It is used to illustrate how to use JSSE specification of server name indication.

Socket Based Scenarios of a virtual servers dispatcher

Step 1: register server name handler in the dispatcher server
At the step, the application may create different SSLContext for different server name indication, or link a certain server name indication to a specified virtual machine or distributed system.

For example, for server name of "www.example.com", the registered server name handler may be for a local virtual hosting web service. The local virtual hosting web service will use a specified SSL context. For server name of "www.invalid.com", the registered server name handler may be for a virtual machine hosting on "10.0.0.36". The handler may map proxy this connection to the virtual machine.

Step 2: create a server socket, and accept a new connection
    ServerSocket serverSocket = new ServerSocket(serverPort);
    Socket socket = serverSocket.accept();
Step 3: read and buffer bytes from the socket input stream, and explore the buffered bytes
    InputStream ins = socket.getInputStream();

    byte[] buffer = new byte[0xFF];
    int position = 0;
    SSLCapabilities capabilities = null;

    // Read the header of TLS record
    while (position < SSLExplorer.RECORD_HEADER_SIZE) {
        int count = SSLExplorer.RECORD_HEADER_SIZE - position;
        int n = ins.read(buffer, position, count);
        if (n < 0) {
            throw new Exception("unexpected end of stream!");
        }
        position += n;
    }

    // Get the required size to exlpore the SSL capabilities
    int recordLength = SSLExplorer.getRequiredSize(buffer, 0, position);
    if (buffer.length < recordLength) {
        buffer = Arrays.copyOf(buffer, recordLength);
    }

    while (position < recordLength) {
        int count = recordLength - position;
        int n = ins.read(buffer, position, count);
        if (n < 0) {
            throw new Exception("unexpected end of stream!");
        }
        position += n;
    }

    // Explore
    capabilities = SSLExplorer.explore(buffer, 0, recordLength);;
    if (capabilities != null) {
        System.out.println("Record version: " +
                capabilities.getRecordVersion());
        System.out.println("Hello version: " +
                capabilities.getHelloVersion());
    }
Step 4: get the requested server name from the SSLCapabilities
    List<SNIServerName> serverNames = capabilities.getServerNames();
Step 5: looking for the registered server name handler for this server name indication
Typically, there are two types of handler. The first one is that the service of the hostname is resident in a virtual machine or another distributed separated box. In this case, the application need to forward the connection to the destination. The application requires to read and write the raw internet data, rather than the SSL application from the socket stream.
    Socket destinationSocket = new Socket(serverName, 443);

    // forward buffered bytes and network data from the current socket to
    // destinationSocket, proxy-like coding

The 2nd one is that the service of the server name is resident in the same process. And the service is able to use the socket directly. In this case, the application will simply set the SSLSocket instance to the server.
    SSLContext serviceContext = ...
                    // get service context from registered handler
                    // or create the context on the fly
    SSLSocketFactory serviceSocketFac =
                    serviceContext.getSSLSocketFactory();

    ByteArrayInputStream bais =
        new ByteArrayInputStream(buffer, 0, position);
                           // wrap the buffered bytes
    SSLSocket serviceSocket =
        (SSLSocket)serviceSocketFac.createSocket(socket, bais, true);

    // Now the service can use the serviceSocket as normal.

SSLEngine Based Scenarios of a virtual servers dispatcher


Similar to the socket based scenatios. There is a little different in the 2nd case in step 5.
The 2nd case is that the service of the hostname is resident in the same process. And hostname service is able to use the engine directly. In this case, the application will simply feed the net data to the new engine.
    SSLContext serviceContext =
                    // get service context from registered handler
                    // or create the context on the fly
    SSLEngine serviceEngine = serviceContext.createSSLEngine();

    // Now the service can use the buffered bytes and other byte
    // buffer as normal.
The scenarios for the cases that bridge SSLSocket source to SSLEngine destination, or SSLEngine source to SSLSocket destination are pretty similar to the above two scenarios.

No server name indication extension


It's often that there is no server name indication extension in a ClientHello message. There is no way to select a proper service according to server name indication.

For such cases, the application may want to specify a default service. When there is no server name indication extension, the connection will be delegated to the default service.

Failover


Please NOTE that the explore of the security capabilities should neither consume nor produce network or application data. So what about if the explore fails?

SSLExploree.explore() should not checking the validity of SSL/TLS contents. However, it requires that the record format complies to SSL/TLS specification, and handshaking has not started. SSLExploree.explore() may throw IOException for such bad cases.

SSLExploree.explore() cannot produce network data. And SSL/TLS protocols requires to reply with proper alert messages. It is not recommended to close the raw socket out of band of SSL/TLS protocols. Failover is recommended to handle exception thrown by SSLExplorer.explore().

The application need to define a failover SSLContext, it is not used to negotiate any real SSL/TLS connection. Instead, it is used to close the SSL/TLS connection with proper alert message. So the initialization of the context can be very basic.
    byte[] buffer = ...       // buffered network data
    boolean failed = true;    // SSLExploree.explore() throws an exception

    // off course, the above explore failed. Faile to failure handler
    SSLContext context = SSLContext.getInstance("TLS");
                                        // the failover SSLContext
    context.init(null, null, null);
    SSLSocketFactory sslsf = context.getSocketFactory();
    ByteArrayInputStream bais =
            new ByteArrayInputStream(buffer, 0, position);
    SSLSocket sslSocket = (SSLSocket)sslsf.createSocket(socket, bais, true);

    SNIMatcher matcher = new DenialSNIMatcher(); // see case 2.2.1
    Collection<SNIMatcher> matchers = new ArrayList<>(1);
    matchers.add(matcher);
    SSLParameters params = sslSocket.getSSLParameters();
    params.setSNIMatchers(matchers);    // no recognizable server name
    sslSocket.setSSLParameters(params);

    try {
        InputStream sslIS = sslSocket.getInputStream();
        sslIS.read();
    } catch (Exception e) {
        System.out.println("server exception " + e);
    } finally {
        sslSocket.close();
    }

To put it together, as SSLExplore.explore() may fail with exception, the application MUST handle the exception with a failover solution, such as failover SSLContext. An application may also need to handle ClientHello message without server name indication extension, the application MUST specify a default service for non-server-name-indication handshaking. And an application may be expected to handle server name indication properly, the application MUST specify the target service for a particular server name indication.

Hope it helps!

Popular posts from this blog

TLS Server Name Indication Extension and Unrecognized_name

JSSE Oracle Provider Preference of TLS Cipher Suites

JSSE Oracle Provider Default Disabled TLS Cipher Suites