-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Defragment incoming TLS handshake messages #9872
Changes from 2 commits
ac2cf1f
5f7c2c2
cad11ad
3dfe75e
aaa152e
b70e76a
afa11db
eb77e5b
cf4e6a1
dd14c0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Changes | ||
* Defragment incoming TLS handshake messages. | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1808,6 +1808,8 @@ struct mbedtls_ssl_context { | |
|
||
size_t MBEDTLS_PRIVATE(in_hslen); /*!< current handshake message length, | ||
including the handshake header */ | ||
unsigned char *MBEDTLS_PRIVATE(in_hshdr); /*!< original handshake header start */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more comment that was posted on the tests's pr 9928 @gilles-peskine-arm commented: Introducing a new buffer pointer creates two risks: buffer overflow, and use-after-free. I would prefer to have more clarity as to the size of the (sub-)buffer accessible from in_hshdr (it isn't in_hsfraglen), and also the impact on the in_hdr field (which has become less straightforward). I don't have any bad feeling about the current code here, but I'd prefer to have better documentation to help future maintainers (I do have a bad feeling about someone later fixing a bug that's unrelated to fragmentation, and missing some interaction between their bug and fragmentation). I am a little worried about a use-after-free. handle_buffer_resizing takes care of this pointer, and it's the only risky place I can think of, but I am not very confident. Please reset in_hshdr in mbedtls_ssl_update_in_pointers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
the overview of the approach to defragmentation is as follows:
i hope this makes it clear. any suggestions wrt comments or documentation are welcome. as a more general note, i think that an extensive comment or perhaps even a readme explaining the ways in/out buffers and pointers are managed would be helpful. it took me a while to wrap my head around it when i started. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the explanation, that's very useful! Would you mind adding comments? Both on the context fields, and also a comment in
Absolutely, yes! The more we add features like defragmentation, the more complicated it is, and it doesn't help that there's so little documentation. Whenever we identify new wisdom about how things work, we should write them down inside the code, even if it's incomplete. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually, upon reflection i realized that we don't need to store this pointer: it will always be the first message in the buffer. so, instead in dd14c0a i'm removing it. i tested manually with facebook as well as #9928 with this change - defrag tests are passing. i also expanded |
||
size_t MBEDTLS_PRIVATE(in_hsfraglen); /*!< accumulated hs fragments length */ | ||
int MBEDTLS_PRIVATE(nb_zero); /*!< # of 0-length encrypted messages */ | ||
|
||
int MBEDTLS_PRIVATE(keep_current_message); /*!< drop or reuse current message | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3225,7 +3225,11 @@ int mbedtls_ssl_prepare_handshake_record(mbedtls_ssl_context *ssl) | |
return MBEDTLS_ERR_SSL_INVALID_RECORD; | ||
} | ||
|
||
ssl->in_hslen = mbedtls_ssl_hs_hdr_len(ssl) + ssl_get_hs_total_len(ssl); | ||
if (ssl->in_hslen == 0) { | ||
ssl->in_hslen = mbedtls_ssl_hs_hdr_len(ssl) + ssl_get_hs_total_len(ssl); | ||
ssl->in_hsfraglen = 0; | ||
ssl->in_hshdr = ssl->in_hdr; | ||
} | ||
|
||
MBEDTLS_SSL_DEBUG_MSG(3, ("handshake message: msglen =" | ||
" %" MBEDTLS_PRINTF_SIZET ", type = %u, hslen = %" | ||
|
@@ -3291,10 +3295,59 @@ int mbedtls_ssl_prepare_handshake_record(mbedtls_ssl_context *ssl) | |
} | ||
} else | ||
#endif /* MBEDTLS_SSL_PROTO_DTLS */ | ||
/* With TLS we don't handle fragmentation (for now) */ | ||
if (ssl->in_msglen < ssl->in_hslen) { | ||
MBEDTLS_SSL_DEBUG_MSG(1, ("TLS handshake fragmentation not supported")); | ||
return MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE; | ||
{ | ||
int ret; | ||
const size_t hs_remain = ssl->in_hslen - ssl->in_hsfraglen; | ||
rojer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const size_t msg_hslen = (hs_remain <= ssl->in_msglen ? hs_remain : ssl->in_msglen); | ||
rojer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
MBEDTLS_SSL_DEBUG_MSG(3, | ||
("handshake fragment: %" MBEDTLS_PRINTF_SIZET " .. %" | ||
MBEDTLS_PRINTF_SIZET " of %" | ||
MBEDTLS_PRINTF_SIZET " msglen %" MBEDTLS_PRINTF_SIZET, | ||
ssl->in_hsfraglen, ssl->in_hsfraglen + msg_hslen, | ||
ssl->in_hslen, ssl->in_msglen)); | ||
(void) msg_hslen; | ||
if (ssl->in_msglen < hs_remain) { | ||
ssl->in_hsfraglen += ssl->in_msglen; | ||
ssl->in_hdr = ssl->in_msg + ssl->in_msglen; | ||
ssl->in_msglen = 0; | ||
mbedtls_ssl_update_in_pointers(ssl); | ||
Comment on lines
+3311
to
+3313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing in #9989 reveals a bug: defragmentation of encrypted records does not work correctly in TLS 1.2 when the symmetric encryption is CBC (EtM or not), CCM or GCM. It works with any TLS 1.3 cipher suite, and with ChachaPoly and null encryption in TLS 1.2. The problematic cases are exactly the ones where the record includes an explicit IV (always 8 bytes). In the initial handshake, only the Finished message is likely to be affected, and it's only 16 bytes, so this happens when the fragment size is less than 16 bytes. This can happen with larger fragment sizes during renegotiation. In problematic scenarios, the fragment reassembly loop looks for the second fragment 8 bytes too early in the buffer. There is an 8-byte gap between the fragments, but the reassembly expects them to be consecutive. It's not 100% clear to me yet, but I think the offset update here isn't correct. As far as I can tell from looking at an example in a debugger, at the end of the first incomplete fragment, |
||
return MBEDTLS_ERR_SSL_CONTINUE_PROCESSING; | ||
} | ||
if (ssl->in_hshdr != ssl->in_hdr) { | ||
/* | ||
* At ssl->in_hshdr we have a sequence of records that cover the next handshake | ||
* record, each with its own record header that we need to remove. | ||
* Note that the reassembled record size may not equal the size of the message, | ||
* there maybe bytes from the next message following it. | ||
*/ | ||
size_t merged_rec_len = 0; | ||
unsigned char *p = ssl->in_hshdr, *q = NULL; | ||
do { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I was trying to analyze and fix https://github.com/Mbed-TLS/mbedtls/pull/9872/files#r1967669302, I tried to make sense of what the various pointers and offsets should be at any given point ( One of the difficulties in understanding the code is that fragment accumulation is completely disconnected from fragment reassembly, so reassembly has to re-parse data. I would find it easier to understand if the structure of the code was: when we have finished parsing a fragment, if it wasn't the initial fragment, then merge it with the initial fragment. With this structure, there'd be fewer moving parts. Fragment reassembly would have access to the offsets and record data from the latest fragment, and wouldn't need to do any parsing, so it would be easier to figure out offsets. There would also be fewer opportunities for parsing errors or integer/buffer overflows. In addition, the input buffer would fill out less quickly — the current structure adds a 5- or 13-byte overhead per fragment. @rojer Did you try doing fragment reassembly incrementally? Are there any difficulties in doing it that way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, i don't see any reason why it couldn't be done that way. this just happened to be the way i went at it at the time. |
||
mbedtls_record rec; | ||
ret = ssl_parse_record_header(ssl, p, mbedtls_ssl_in_hdr_len(ssl), &rec); | ||
if (ret != 0) { | ||
return ret; | ||
} | ||
merged_rec_len += rec.data_len; | ||
p = rec.buf + rec.buf_len; | ||
if (q != NULL) { | ||
memmove(q, rec.buf + rec.data_offset, rec.data_len); | ||
q += rec.data_len; | ||
} else { | ||
q = p; | ||
} | ||
} while (merged_rec_len < ssl->in_hslen); | ||
ssl->in_hdr = ssl->in_hshdr; | ||
mbedtls_ssl_update_in_pointers(ssl); | ||
ssl->in_msglen = merged_rec_len; | ||
/* Adjust message length. */ | ||
MBEDTLS_PUT_UINT16_BE(merged_rec_len, ssl->in_len, 0); | ||
ssl->in_hsfraglen = 0; | ||
ssl->in_hshdr = NULL; | ||
MBEDTLS_SSL_DEBUG_BUF(4, "reassembled record", | ||
ssl->in_hdr, mbedtls_ssl_in_hdr_len(ssl) + merged_rec_len); | ||
} | ||
} | ||
|
||
return 0; | ||
|
@@ -4639,6 +4692,16 @@ static int ssl_consume_current_message(mbedtls_ssl_context *ssl) | |
return MBEDTLS_ERR_SSL_INTERNAL_ERROR; | ||
} | ||
|
||
if (ssl->in_hsfraglen != 0) { | ||
/* Not all handshake fragments have arrived, do not consume. */ | ||
MBEDTLS_SSL_DEBUG_MSG(3, | ||
("waiting for more fragments (%" MBEDTLS_PRINTF_SIZET " of %" | ||
MBEDTLS_PRINTF_SIZET ", %" MBEDTLS_PRINTF_SIZET " left)", | ||
ssl->in_hsfraglen, ssl->in_hslen, | ||
ssl->in_hslen - ssl->in_hsfraglen)); | ||
return 0; | ||
} | ||
|
||
/* | ||
* Get next Handshake message in the current record | ||
*/ | ||
|
@@ -4664,6 +4727,7 @@ static int ssl_consume_current_message(mbedtls_ssl_context *ssl) | |
ssl->in_msglen -= ssl->in_hslen; | ||
memmove(ssl->in_msg, ssl->in_msg + ssl->in_hslen, | ||
ssl->in_msglen); | ||
MBEDTLS_PUT_UINT16_BE(ssl->in_msglen, ssl->in_len, 0); | ||
|
||
MBEDTLS_SSL_DEBUG_BUF(4, "remaining content in record", | ||
ssl->in_msg, ssl->in_msglen); | ||
|
@@ -5338,7 +5402,7 @@ void mbedtls_ssl_update_in_pointers(mbedtls_ssl_context *ssl) | |
} else | ||
#endif | ||
{ | ||
ssl->in_ctr = ssl->in_hdr - MBEDTLS_SSL_SEQUENCE_NUMBER_LEN; | ||
ssl->in_ctr = ssl->in_buf; | ||
ssl->in_len = ssl->in_hdr + 3; | ||
#if defined(MBEDTLS_SSL_DTLS_CONNECTION_ID) | ||
ssl->in_cid = ssl->in_len; | ||
|
@@ -5354,24 +5418,35 @@ void mbedtls_ssl_update_in_pointers(mbedtls_ssl_context *ssl) | |
* Setup an SSL context | ||
*/ | ||
|
||
void mbedtls_ssl_reset_in_out_pointers(mbedtls_ssl_context *ssl) | ||
void mbedtls_ssl_reset_in_pointers(mbedtls_ssl_context *ssl) | ||
{ | ||
#if defined(MBEDTLS_SSL_PROTO_DTLS) | ||
if (ssl->conf->transport == MBEDTLS_SSL_TRANSPORT_DATAGRAM) { | ||
ssl->in_hdr = ssl->in_buf; | ||
} else | ||
#endif | ||
rojer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
ssl->in_hdr = ssl->in_buf + MBEDTLS_SSL_SEQUENCE_NUMBER_LEN; | ||
} | ||
|
||
/* Derive other internal pointers. */ | ||
mbedtls_ssl_update_in_pointers(ssl); | ||
} | ||
|
||
void mbedtls_ssl_reset_out_pointers(mbedtls_ssl_context *ssl) | ||
{ | ||
/* Set the incoming and outgoing record pointers. */ | ||
#if defined(MBEDTLS_SSL_PROTO_DTLS) | ||
if (ssl->conf->transport == MBEDTLS_SSL_TRANSPORT_DATAGRAM) { | ||
ssl->out_hdr = ssl->out_buf; | ||
ssl->in_hdr = ssl->in_buf; | ||
} else | ||
#endif /* MBEDTLS_SSL_PROTO_DTLS */ | ||
{ | ||
ssl->out_ctr = ssl->out_buf; | ||
ssl->out_hdr = ssl->out_buf + 8; | ||
ssl->in_hdr = ssl->in_buf + 8; | ||
ssl->out_hdr = ssl->out_buf + MBEDTLS_SSL_SEQUENCE_NUMBER_LEN; | ||
rojer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/* Derive other internal pointers. */ | ||
mbedtls_ssl_update_out_pointers(ssl, NULL /* no transform enabled */); | ||
mbedtls_ssl_update_in_pointers(ssl); | ||
} | ||
|
||
/* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can eve make classify that as a bugfix: (1) there is nothing in the spec that says support for receiving fragemented messages is optional (quite the opposite: it's the first point in the implementation pitfalls section), and (2) it was causing interop failures in practice.
Also, I think we should expand a bit so that users who are not aware of TLS internals may make more sense of the entry, perhaps:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done