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

Add HRR compliance comments and tests for TLS RFC section 4.2.8 #3362

Merged
merged 14 commits into from
Jul 6, 2022

Conversation

goatgoose
Copy link
Contributor

@goatgoose goatgoose commented Jun 14, 2022

Resolved issues:

None

Description of changes:

This is a followup PR to #3337. This PR adds hello retry request related compliance comments and tests from TLS RFC section 4.2.8, Key Share.

Call-outs:

There are additional hello retry request related comments in the RFC that are not in section 4.2.8 which will be added in #3363.

Testing:

How is this change tested (unit tests, fuzz tests, etc.)? Are there any testing steps to be verified by the reviewer?

Is this a refactor change? If so, how have you proved that the intended behavior hasn't changed?

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

@github-actions github-actions bot added the s2n-core team label Jun 14, 2022
@goatgoose goatgoose marked this pull request as ready for review June 14, 2022 04:06
Comment on lines 124 to 129
*= https://tools.ietf.org/rfc/rfc8446#4.2.8
*= type=exception
*= reason=Permit HRRs to leave the selected_group field unmodified if sent due to rejecting early data
*# If either of these checks fails, then
*# the client MUST abort the handshake with an "illegal_parameter"
*# alert.
Copy link
Contributor

Choose a reason for hiding this comment

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

Even with early data, don't you need to offer a correct selected group? Why is there an exception for early data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, a correct selected group still needs to be offered. This exception is for this line:

POSIX_ENSURE((conn->early_data_state == S2N_EARLY_DATA_REJECTED) || new_key_share_requested,
S2N_ERR_INVALID_HELLO_RETRY);

Without the exception, I think there should not be a check for conn->early_data_state == S2N_EARLY_DATA_REJECTED. It seems like this condition is here to allow for HRRs to not modify the selected_group field if the HRR was sent only for the purpose of rejecting early data. I think this makes sense, because if you're only rejecting early data it wouldn't make sense to also need to update the selected_group. However, this condition does not seem to be present in the RFC.

Here is the relevant part:

   Upon receipt of this extension in a HelloRetryRequest, the client
   MUST verify that (1) the selected_group field corresponds to a group
   which was provided in the "supported_groups" extension in the
   original ClientHello and (2) the selected_group field does not
   correspond to a group which was provided in the "key_share" extension
   in the original ClientHello.  If either of these checks fails, then
   the client MUST abort the handshake with an "illegal_parameter"
   alert.

Checks (1) and (2) must both succeed. And, according to check (2), new_key_share_requested should always be true. So, the exception I added is for this statement:

   If either of these checks fails, then
   the client MUST abort the handshake with an "illegal_parameter"
   alert.

Since we allow check (2) to be false if early data was rejected.

Let me know if I'm misunderstanding something or if we actually should not be making this exception!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the misunderstanding comes from the fact that the HelloRetryRequest doesn't have to include a keyshare extension. A server who is using a HRR to reject early data would not include a keyshare extension in that request. So the client doesn't need to make an exception in this case. Basically, if the server includes a keyshare extension in its HRR, it has to be asking for a keyshare change, otherwise the server can just leave that extension out.

So what I'm saying is, at minimum, there is no exception to the RFC. However, the existing code might need to be rewritten for the case where the server uses HRR to reject early data. 😪 @lrstewart do you know if we have a test for this?

This paragraph shows that the keyshare extension is optional in a HRR:

  • If a "key_share" extension was supplied in the HelloRetryRequest,
    replacing the list of shares with a list containing a single
    KeyShareEntry from the indicated group.

Copy link
Contributor Author

@goatgoose goatgoose Jun 22, 2022

Choose a reason for hiding this comment

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

Ah, got it okay. That makes sense. I was thinking that the extension was required for some reason.

So would the change be to check to see if a key share extension was received in s2n_server_hello_retry_recv, and if it was, ensure that check (2) succeeds here, and if it wasn't received, only check to see if conn->early_data_state == S2N_EARLY_DATA_REJECTED?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what I would do is gate line 71-107 with the check that the client actually received a keyshare extension. And then you can remove the early data rejected clause from the posix_ensure statement.

It seems like we have a test for this situation: https://github.com/aws/s2n-tls/blob/main/tests/unit/s2n_client_early_data_indication_test.c#L412.
But I think this test is flawed, because it's not appropriately mocking how a server would send a HRR if it wanted to reject early data. The server in this case should write the HRR without the keyshare ext.
Basically, additionally, I think we need to change the test so that the server excludes the keyshare ext.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After looking at bouncy castle and openssl, it seems like implementations require a key share extension and don't provide an exception for early data. I therefor removed the exception, and fixed two failing tests that relied on not needing to update the selected_group: 36cf6a7

I also removed a test that checked for this specific exception that no longer exists: 75009a9

Copy link
Contributor

@lrstewart lrstewart left a comment

Choose a reason for hiding this comment

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

Should this test still exist? I'm guessing it didn't fail because the relevant check isn't actually performed by the extension itself. This supports my concerns in this comment about the validation being scattered :\

Comment on lines 1513 to 1527
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, HELLO_RETRY_MSG));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, SERVER_HELLO));

/* Server sends ServerHello 2 */
EXPECT_SUCCESS(s2n_server_hello_send(server_conn));

EXPECT_SUCCESS(s2n_stuffer_wipe(&client_conn->handshake.io));
EXPECT_SUCCESS(s2n_stuffer_copy(&server_conn->handshake.io, &client_conn->handshake.io,
s2n_stuffer_data_available(&server_conn->handshake.io)));
int server_hello = 3;
client_conn->handshake.message_number = server_hello;
Copy link
Contributor

@lrstewart lrstewart Jun 28, 2022

Choose a reason for hiding this comment

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

Well this is an unfortunate side effect of messages appearing twice in retry handshakes :/

You might still be able to simplify a little though:

Suggested change
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, HELLO_RETRY_MSG));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, SERVER_HELLO));
/* Server sends ServerHello 2 */
EXPECT_SUCCESS(s2n_server_hello_send(server_conn));
EXPECT_SUCCESS(s2n_stuffer_wipe(&client_conn->handshake.io));
EXPECT_SUCCESS(s2n_stuffer_copy(&server_conn->handshake.io, &client_conn->handshake.io,
s2n_stuffer_data_available(&server_conn->handshake.io)));
int server_hello = 3;
client_conn->handshake.message_number = server_hello;
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, HELLO_RETRY_MSG));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, ENCRYPTED_EXTENSIONS));

You don't need to send the SERVER_HELLO manually.

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 may be missing something with the negotiate functions, but similar to #3363 (comment), I think the last send needs to be sent manually so that the stuffer isn't wiped at the end of the negotiate call. If I use negotiate here s2n_server_hello_recv errors because the handshake stuffer is empty, but when calling it manually the stuffer is preserved after it writes so it works.

Copy link
Contributor

@lrstewart lrstewart Jul 5, 2022

Choose a reason for hiding this comment

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

You might consider stuffers for io instead of an io pair. Then you can read the actual output without relying on handshake.io.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! This fixed #3363 (comment). For this test, I found the EXPECT_ERROR_WITH_ERRNO macro which helped simplify it: 386e42e

@goatgoose
Copy link
Contributor Author

Should this test still exist? I'm guessing it didn't fail because the relevant check isn't actually performed by the extension itself. This supports my concerns in this comment about the validation being scattered :\

This check is for the client key share sends, rather than any server key share send/receive. So, it seems to just be checking to make sure there wasn't anything in the send that would have prevented a duplicate key share send. However, now that no longer have an exception for early data, I added a sanity check to the client key share send in 88dc849 to make sure we don't send the same share twice.

@goatgoose goatgoose requested a review from lrstewart July 1, 2022 03:11
Comment on lines 98 to 102
*= https://tools.ietf.org/rfc/rfc8446#section-4.1.4
*# Clients MUST abort the handshake with an
*# "illegal_parameter" alert if the HelloRetryRequest would not result
*# in any change in the ClientHello.
**/
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be down on line 120? This chunk of code doesn't abort anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that makes sense. I moved it up thinking it might be confusing which of the two checks at 120 this comment applied to, but it makes sense to put it where the actual abort happens. I moved it back in 9373fc8.

Comment on lines 1513 to 1527
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, HELLO_RETRY_MSG));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, CLIENT_HELLO));
EXPECT_OK(s2n_negotiate_until_message(client_conn, &blocked, SERVER_HELLO));
EXPECT_OK(s2n_negotiate_until_message(server_conn, &blocked, SERVER_HELLO));

/* Server sends ServerHello 2 */
EXPECT_SUCCESS(s2n_server_hello_send(server_conn));

EXPECT_SUCCESS(s2n_stuffer_wipe(&client_conn->handshake.io));
EXPECT_SUCCESS(s2n_stuffer_copy(&server_conn->handshake.io, &client_conn->handshake.io,
s2n_stuffer_data_available(&server_conn->handshake.io)));
int server_hello = 3;
client_conn->handshake.message_number = server_hello;
Copy link
Contributor

@lrstewart lrstewart Jul 5, 2022

Choose a reason for hiding this comment

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

You might consider stuffers for io instead of an io pair. Then you can read the actual output without relying on handshake.io.

@goatgoose goatgoose requested a review from lrstewart July 6, 2022 16:17
@goatgoose goatgoose merged commit dea5dba into aws:main Jul 6, 2022
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