Skip to content
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

fix: tainted handshake.io and add large client hello test #5208

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

boquan-fang
Copy link
Contributor

@boquan-fang boquan-fang commented Mar 19, 2025

Release Summary:

Resolved issues:

related to aws/s2n-quic#332.

Description of changes:

  • Replace s2n_stuffer_raw_write() in s2n_ecc_evp_write_params(). Use a stuffer copy to perform raw write, so that the original stuffer won't get tainted.

    • We need to do a s2n_stuffer_reserve_space() before creating a copy, so that the copy stuffer won't realloc memory during the raw write operation.
    • We need to update the original stuffer with the copy stuffer's write_cursor and high_water_mark since they might get changed.
    • Same operation in s2n_ecc_evpo_write_params(), but we don't reserve any additional space or updating any other stuffer's meta data, because the data_len is zero.
  • Change cipher preferences count from uint8_t to uint16_t. The RFC specifies that the total cipher suites size can be (2^16 - 2).

  • Add two tests in s2n_client_hello_test.c. One to generate a Client Hello that's close the maximum handshake length and another to generate a Client Hello that's larger than the maximum length.

    • We set up two custom security policies with ciphersuites preferences of lots of repeating cipher suites. That can enlarge the Client Hello size.
    • In the test, I have verify that handshake.io is not tainted, and the size of Client Hello is either less than 300 bytes from the maximum handshake length or exceeds the maximum handshake length.

Call-outs:

  • This PR is a step to verify that server will drop large (capped at 64KB) handshake message. The next step, according to the issue, is to merge this PR and fix: handshake message length integer overflow in s2n_handshake_finish_header #5206. And then do a S2N-TLS rust bindings release. Specific steps can be track in Provide an interface for TLS to communicate buffering limits s2n-quic#332 (comment).
  • We shouldn't use s2n_raw_write or s2n_raw_read unless we have no other ways. There are many other places in S2N-TLS where we used those two functions. We should probably do a check for all of them in the future to see if we can change all of them.
  • If a user appends counts of UINT16_MAX cipher suites, then the total cipher suites length would be (2^17 - 2) bytes. Our Client Hello send function will error on that, since it only reserve 16 bytes to write all cipher suites information:
    struct s2n_stuffer_reservation available_cipher_suites_size;
    POSIX_GUARD(s2n_stuffer_reserve_uint16(out, &available_cipher_suites_size));
  • I defined some macros in tests/unit/s2n_client_hello_test.c. I would like some feedbacks on naming and how to make the math computations better.
  • In tls/extensions/s2n_extension_type.c, I checked every time that we call s2n_extension_list_send in our codebase (except testing), the out stuffer is always handshake.io in s2n_connection struct. handshake.io will always be wiped at the end of each handshake message. That means the data in handshake.io is temporary. Therefore, it is safe to mark the out stuffer as not tainted after the extension is sent.
    • I left a comment in tls/extensions/s2n_extension_type.c when I am reseting tainted to false. Please let me know if the comment is not clear.

Testing:

When the unit tests in the CI are testing with openssl-1.1.1 and openssl-3.0, EVP_APIS_SUPPORTED will be enabled. When the unit tests in CI are testing with openssl-1.0.2, EVP_APIS_SUPPORTED will be disabled. Hence, the CI will test for changes for both situations.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Comment on lines 437 to 441

POSIX_GUARD(s2n_alloc(&point_blob, point_len));
POSIX_GUARD(s2n_ecc_evp_write_point_data_snug(point, group, &point_blob));
POSIX_GUARD(s2n_stuffer_write(out, &point_blob));
POSIX_GUARD(s2n_free(&point_blob));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allocating and then immediately freeing is pretty inefficient. Can you figure out a way to do this without an allocation?

@boquan-fang boquan-fang force-pushed the fix-raw-write branch 2 times, most recently from 42dfff6 to 4aee908 Compare March 25, 2025 19:46
@boquan-fang boquan-fang changed the title refactor: remove s2n_raw_write usage in s2n_ecc_evp_write_params_point refactor: replace s2n_raw_write and add large client hello test Mar 25, 2025
* Fix typos for temporary
* use s2n_stuffer_data_available
* add GUARD to the s2n_negotiate to write Client Hello
@boquan-fang boquan-fang changed the title refactor: replace s2n_raw_write and add large client hello test refactor: replace s2n_stuffer_raw_write and add large client hello test Mar 26, 2025
* Set out->tainted to false in s2n_extension_send.
@boquan-fang boquan-fang requested a review from maddeleine March 31, 2025 22:35
@boquan-fang boquan-fang requested a review from lrstewart March 31, 2025 22:35
@boquan-fang boquan-fang changed the title refactor: replace s2n_stuffer_raw_write and add large client hello test fix: tainted handshake.io and add large client hello test Mar 31, 2025
Comment on lines +470 to +472
POSIX_GUARD(s2n_stuffer_rewrite(out));
POSIX_GUARD(s2n_stuffer_skip_write(out, written_data_location));
POSIX_GUARD(s2n_stuffer_skip_read(out, read_cursor));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the potential not to work. You can't read past where you've written, so this only works if read_cursor < written_data_location. I imagine it works bc read_cursor should be 0 here, but it's not technically correct.

It seems non-ideal to have to deal with the read_cursor at all. Can you figure out a less destructive way to handle this?

Comment on lines +1962 to +1963
/* Test: Client Hello is slightly less than 64KB */
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I see this test 6mo from now, I'm going to question why this needed to be tested :) I'd bundle these two tests together under the name "Test: very large client hellos" so that it's clear why you're doing <64KB / >64KB.

Comment on lines +2061 to +2062
/* The size of Client Hello exceeds S2N_MAXIMUM_HANDSHAKE_MESSAGE_LENGTH */
EXPECT_TRUE(s2n_stuffer_data_available(&io_pair.server_in) > S2N_MAXIMUM_HANDSHAKE_MESSAGE_LENGTH);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You check s2n_client_hello_get_raw_message_length above, but server_in here. If these two tests are supposed to be analogous, we should make the same size assertion.

Also note that server_in isn't just the CH-- it's also the record, so an extra 5 bytes for the record header. If that matters, I'd probably note that in a comment rather than doing math to strip off the header.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these two tests are supposed to be analogous, we should make the same size assertion.

I will check server_in for both for them to be analogous. The Client Hello will not be parsed into the server connection if the message itself is a bad message, so we can't get use s2n_client_hello_get_raw_mesage_length for the second case.

I will put a comment mentioning the extra 5 bytes for record header. It shouldn't matter, since the handshake message from the first case is about 100 bytes less than S2N_MAXIMUM_HANDSHAKE_MESSAGE_LENGTH , and the second case is about 300 bytes more than S2N_MAXIMUM_HANDSHAKE_MESSAGE_LENGTH.

Comment on lines +118 to +123
* Reset the tainted flag on the out stuffer (typically handshake.io).
* The handshake.io stuffer is wiped after processing each handshake message,
* which resets its state and marks it as not tainted.
* No raw data from handshake.io is permanently stored in the connection during extension send.
* Because of that, we can mark the out stuffer as not tainted which allows the stuffer to
* continue growing after each extension send.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment assumes handshake.io, but you note only "typically handshake.io". That makes me question whether this is safe / correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will say reset the tainted flag on the out stuffer (handshake.io stuffer). It's always the handshake.io stuffer since I have checked the entire codebase for this function call.

Comment on lines +1962 to +1963
/* Test: Client Hello is slightly less than 64KB */
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests require a lot of setup that should be the same. Can they share any setup, to better highlight the differences?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants