c++tls1.3schannel

SChannel TLS 1.3 mystery additional message


A while ago I implemented a client and server using SChannel to encrypt communication. Recently I made the required switch from the SCHANNEL_CRED struct to the SCH_CREDENTIALS one so that TLS 1.3 support is provided in Windows 11. However, I encountered a situation that my code didn't originally account for and that I've resolved but can't explain.

The negotiation flow is as follows:

At this point I call AcceptSecurityContext on the server and pass in the received data and I would expect to get SEC_E_OK and no data to pass back to the client. Both sides have indicated that they've finished and, by all accounts that I've read, the negotiation is complete. However what actually happens is:

My original implementation would fail at this point because once a given side returned SEC_E_OK I didn't expect the peer to provide it with any more messages for the negotiation. The client already returned that, and yet the server has more data to send it.

Can anyone explain what this additional message is?


Solution

  • We have finally identified this additional message and the answer has an interesting ramification.

    Message explanation

    When the message is passed to DecryptMessage it returns SEC_I_RENEGOTIATE and passes back a modified version of the message in the extra data buffer that we can easily understand. The modified data turns out to be a NewSessionTicket message.

    Example data in:

    17 03 03 00 62 14 2F 12 1E 2C 50 AB B4 54 60 9B
    ED 9B E3 0C 87 8F 94 FE BD 56 9D 65 AE 80 62 65
    B9 FF D9 A1 28 FA 6F 84 12 10 3B 45 F2 1C 84 A2
    5C 53 76 42 63 25 AE 27 E7 C5 D0 D0 A5 0C BA 81
    19 D7 FF 3F A3 3D 26 05 0D 99 9F 5F 84 99 2F 7B
    BA 0D C8 87 9D D0 85 40 B3 50 9D 9C 07 0C F0 4C
    D8 17 28 9E 4F E0 BD
    

    And data out:

    16 03 03 00 62 04 00 00 4D 00 00 8C A0 FC 6F BC
    2A 20 0E 56 42 D7 51 47 D7 21 25 0F C7 84 10 90
    F3 41 A0 A9 C4 15 F0 2E 01 75 96 F9 F4 4E 9B 00
    8E 92 00 20 F5 1E 00 00 83 E1 D8 D6 E3 FB B3 53
    0D 3E BB 7C 15 4F DD 34 1F BF 63 3D 2E F7 29 E8
    3E 20 BB A5 00 00 16 40 B3 50 9D 9C 07 0C F0 4C
    D8 17 28 9E 4F 51 00
    

    Fields:

    See RFC 8446 section 4.6.1 for remainder

    There is a note in RFC8446 section 4.6.1 which I think explains why SChannel sends this message immediately after the handshake completes:

    Note: Although the resumption master secret depends on the client's second flight, a server which does not request client authentication MAY compute the remainder of the transcript independently and then send a NewSessionTicket immediately upon sending its Finished rather than waiting for the client Finished. This might be appropriate in cases where the client is expected to open multiple TLS connections in parallel and would benefit from the reduced overhead of a resumption handshake.

    Figure 3 in Section 2.2 shows the NewSessionTicket message being sent.

    The same is true for KeyUpdate messages - DecryptMessage will return SEC_I_RENEGOTIATE plus the data to pass to InitializeSecurityContext.

    Rammification

    Now... the interesting thing here is that when using SChannel you can get an SEC_I_RENEGOTIATE status code back from DecryptMessage even though TLS 1.3 doesn't support renegotiation.

    We, in fact, saw this and is what eventually led to the explanation of that first mystery message.

    This is because Microsoft saw this as the easiest way to allow users of SChannel to handle this situation (and thus TLS 1.3) without adding additional handling for new status codes. What happens when you are returned the renegotiate status in TLS 1.2? You feed the subsequent messages into InitializeSecurityContext until it returns a success code. What is the intended behaviour when a NewSessionTicket or KeyUpdate message is received? Feed the subsequent messages into InitializeSecurityContext until it returns a success code. You essentially get the handling of this for free.

    Now... what happens if you have complete control of both ends of the connection and know that a renegotiation will not be triggered? You don't implement handling of renegotiation. Then TLS 1.3 comes along and suddenly your connections start failing because you're receiving "renegotiation" requests that were never sent. Something to be aware of.