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:
InitializeSecurityContext
on the client and get SEC_I_CONTINUE_NEEDED
with some data to send to the server (264 bytes for example). This would be the client hello, cipher suites, and key share.AcceptSecurityContext
on the server and pass in the received data, getting SEC_I_CONTINUE_NEEDED
with some data to send to the client (785 bytes for example). This would be the server hello, key agreement protocol, key share, and an indication that the server has finished.InitializeSecurityContext
on the client, pass in the received data, and get SEC_E_OK
with some data to send to the server (80 bytes for example). This would be the client finished indication.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:
AcceptSecurityContext
on the server and pass in the received data, getting SEC_E_OK
with some data to send to the client (103 bytes for example). I don't know what this message could be.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.
InitializeSecurityContext
on the client with the extra data and get SEC_E_OK
with no more data to send to the server. Negotiation is finally actually complete.Can anyone explain what this additional message is?
We have finally identified this additional message and the answer has an interesting ramification.
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
.
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.