Skip to content

Channel Binding Feature Summary

Terry edited this page Oct 4, 2022 · 1 revision

Branches

Here are two links to two different implementations of channel binding. The only difference between the two is how the TLS finished message's verify_data is fetched. One implementation uses reflection and the other uses a third party dependency called bouncy-castle.

microsoft/mssql-jdbc at channel-bindings (github.com) [Reflection]

microsoft/mssql-jdbc at channel-bindings-bc (github.com) [bouncy-castle]

The branches must be built with the following compile options:

--add-exports java.security.jgss/sun.security.jgss.krb5.internal=ALL-UNNAMED

--add-opens java.base/sun.security.ssl=ALL-UNNAMED

To run the built branch, you must supply the following environment variable and value:

JAVA_TOOL_OPTIONS="--add-exports=java.security.jgss/sun.security.jgss.krb5.internal=ALL-UNNAMED --add-opens=java.base/sun.security.ssl=ALL-UNNAMED"

How is Channel Binding is Facilitated Between SQL Server and the JDBC Driver

  1. Get the 12 byte verify_data byte array from the TLS finished message. This is possible through reflection or the bouncy-castle dependency as demonstrated in the branches linked above.

  2. Create a CBT to send to the server. The latest TLS version used for channel bindings as of this summary is TLS 1.2. TLS 1.2 uses a specific channel bindings type called "tls-unique" (this is the channel bindings type used between the driver and server.

    • The first step in creating a CBT is to prefix the verify_data from the first step with "tls-unique:" (note the colon at the end of tls-unique). Prefixing verify_data with "tls-unique:" will result in a new 23 byte byte array (11 bytes from the "tls-unique:" string plus the 12 byte verify_data byte array).

    • Depending on the authentication method, we will need to handle the CBT differently. Regardless, for both cases, the final data sent to the server is as follows:

      • The final data sent to the server is the MD5 hash of the 4 byte acceptorAddress, 4 byte acceptorAddressLength, 4 byte initiatorAddress, 4 byte initiatorAddressLength, 4 byte CbtLength and finally the CBT. Everything except the CbtLength and CBT is a zeroed 4 byte byte array.

      • For kerberos, we will only need to pass in the CBT to the security context. However, the ChannelBinding object provided doesn't zero the mentioned fields in step b). So, we need to explicitly import an internal JDK child ChannelBinding class (TlsChannelBindingImpl) to do so.

      • For NTLM, we will need to manually MD5 hash the 4 byte acceptorAddress, 4 byte acceptorAddressLength, 4 byte initiatorAddress, 4 byte initiatorAddressLength, 4 byte CbtLength and finally the CBT. Then the 16 bytes is passed to the NTLM authentication message in the channel binding field.

The exact data we are constructing and sending to the server is as follows:

// Address field byte arrays (type and length) are always initialized to zero
byte[] acceptorAddressType = {0, 0, 0, 0}; byte[] acceptorAddressLength = {0, 0, 0, 0}; byte[] initiatorAddressType = {0, 0, 0, 0}; byte[] initiatorAddressLength = {0, 0, 0, 0};

// Channel binding token (CBT) length is always 23 bytes or 0x17 in hex byte[] channelBindingTokenLenth = {0x17, 0, 0, 0};

// CBT is always 23 bytes comprised of 11 byte "tls-unique:" prefix plus 12 byte // TLS finished message byte array byte[] channelBindingToken = {tls-unique:12-byte-verify_data-from-tls-finished-message}; byte[] channelBindingHash = MD5.hash(<the-above-byte-arrays-altogether);

Caveats of Current Channel Bindings Implementation

Currently, as of writing this feature summary, there are 2 things blocking this implementation going live.

  1. The JDK does not offer an API to expose the verify_data byte array in the TLS finished message. This 12 byte byte array is key in creating the necessary channel binding information to pass to the server.

  2. We need to explicitly import an internal JDK child ChannelBinding class in order to zero the byte array address fields in the ChannelBinding object (this is for Kerberos only).

In both cases above, they both require special compile options and a special JAVA_TOOL_OPTIONS environment variable value (e.g. Look at the "Branches" section for these values).

In the first case, because the JDK does not offer an API that exposes the verify_data byte array, we need to use reflection in order to get the byte array. However, using reflection is fragile and can easily break if the code paths were to ever changed on the JDK side or if they differ across JDK implementations. In addition, since reflection relies on specific codes paths, we would need to verify the feature across all driver supported JDKs. And, in the case where it isn't supported, we would need to document the case. Doing so means we would need to track and document specific JDKs where channel binding does and doesn't function. This means extra overhead just to support channel bindings through reflection, which isn't ideal.

In the second case, as mentioned before, we need to zero the address byte array. However, the java GSSAPI's channel binding code does not allow us to do so manually. Instead, it does its own processing of the address field value before MD5 hashing it. In the following code block, when the address fields are set to null in the ChannelBinding object and if the channel binding instance is not of TlsChannelBindingImpl, the CHANNEL-BINDING_AF_NULL_ADDR constant which has the value 255 is used in the MD5 hash instead (we need it to be 0 not 255). So it creates the wrong 16 byte hash of the ChannelBinding object.

To get around this and to be able to zero the address fields, we explicitly import the TlsChannelBindingImpl class and instantiate this version of the ChannelBinding object. The reason for this is that the CHANNEL_BINDING_AF_UNSPEC constant has a value of 0 and to set the address field to 0 the instance needs to be of TlsChannelBindingImpl. In the ternary in the image below, if our ChannelBinding object is an instance of TlsChannelBindingImpl we will use CHANNEL_BINDING_AF_UNSPEC in the address field for hashing instead. However, as mentioned before, to import the TlsChannelBindingImpl class we need special compile and environment variable options, which isn't ideal, and would be poor practice to put into a library and impose on consuming applications.

private byte[] computeChannelBinding(ChannelBinding channelBinding)
        throws GSSException {

        InetAddress initiatorAddress = channelBinding.getInitiatorAddress();
        InetAddress acceptorAddress = channelBinding.getAcceptorAddress();
        int size = 5*4;

        // LDAP TLS Channel Binding requires CHANNEL_BINDING_AF_UNSPEC address type
        // for unspecified initiator and acceptor addresses.
        // CHANNEL_BINDING_AF_NULL_ADDR value should be used for unspecified address
        // in all other cases.
        int initiatorAddressType = getAddrType(initiatorAddress,
                (channelBinding instanceof TlsChannelBindingImpl) ?
                        CHANNEL_BINDING_AF_UNSPEC : CHANNEL_BINDING_AF_NULL_ADDR);
        int acceptorAddressType = getAddrType(acceptorAddress,
                (channelBinding instanceof TlsChannelBindingImpl) ?
                        CHANNEL_BINDING_AF_UNSPEC : CHANNEL_BINDING_AF_NULL_ADDR);

As a more concise summary of caveat #2, what we want to hash is the following:

int acceptorAddressType = 0;

int initiatorAddressType = 0;

But the GSSAPI channel binding code instead processes the address field information to the following if we don’t intervene:

int acceptorAddressType = 255;    // This produces the wrong hash, needs 0 not 255 

int initiatorAddressType = 255;

TL;DR/cliff notes of the caveats for the impatient

  1. Reflection is used to get verify_data byte array which is used to calculate channel binding data to send to server

    • Reflection is fragile and breaks easily if JDK code changes

    • Increased overhead on our part in maintaining and testing channel binding code

    • We will need to explicitly test and document support of channel binding in select JDK versions

  2. Need to explicitly import internal JDK ChannelBinding class TlsChannelBindingImpl to zero address field byte arrays

    • To explicitly import the class is another hack. It needs special compile and environment variable options at runtime.
  3. Need to use special compile and environment variable options to enable the hacks mentioned above which isn't ideal (mostly just a problem with having to provide an environment variable, compile options are ok)

    • When using channel binding, the user would need to provide an explicit environment variable option. Otherwise, a runtime exception is thrown.
Clone this wiki locally