Skip to content

Conversation

@alexw91
Copy link
Contributor

@alexw91 alexw91 commented Nov 22, 2025

Goal

Resolves #5602

Adds a new strongly preferred groups field to s2n_security_policy. This preference list represents the set of SupportedGroups that s2n's TLS server is willing to perform a 2-RTT handshake for if that SupportedGroup was sent in the SupportedGroups extension, but not the KeyShare extension.

If an s2n security policy contains strongly preferred groups, then s2n's usual negotiation logic to prefer KeyShares that are negotiable in 1-RTT when possible is bypassed, and the first mutually supported strongly-preferred SupportedGroup will be selected regardless if it is negotiable in 1-RTT or 2-RTT. If no mutually supported strongly-preferred SupportedGroup exists, the s2n's default logic is skipped.

After this change, s2n's new negotiation logic will be:
1. Any Strongly-Preferred Group (using server preference order)
2. Any PQ SupportedGroup in 1-RTT (using server preference order)
3. Any PQ SupportedGroup in 2-RTT (using server preference order)
4. Any ECDHE Group in 1-RTT (using server preference order)
5. Any mutually supported group in 2-RTT (using server preference order)

Why

Needed to strongly prefer p384 for CNSA policies to force a 2-RTT handshake to select p384 even if p256 is negotiable in 1-RTT. With this logic, TLS Clients that do support p384 will be always be upgraded to p384, and clients that only support p256 will continue negotiating p256 without causing breakages. Then once this CNSA transition policy is deployed we can measure how many TLS connections do not support p384 and we can work on tracking down and fixing those clients.

How

Adds a new field to s2n_security_policy that is checked in s2n_extensions_server_key_share_select() that overrides s2n's default KeyShare negotiation logic as per the description in #5602.

Callouts

PR is based on top of #5615

Testing

Unit tests confirm new negotiation behavior for CNSA transition policies

Related

Resolves #5602

release summary: Added support for Security Policies to have "strongly preferred" SupportedGroups

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

@alexw91 alexw91 force-pushed the strongly_preferred branch 2 times, most recently from 1ff3582 to 906ffeb Compare November 24, 2025 18:42
@alexw91 alexw91 changed the title [WIP] Ability to set "strongly preferred" groups Ability to set "strongly preferred" groups Nov 24, 2025
@alexw91 alexw91 marked this pull request as ready for review November 24, 2025 18:44
@alexw91 alexw91 requested review from a team and jouho November 24, 2025 18:44
@alexw91 alexw91 force-pushed the strongly_preferred branch 4 times, most recently from c2b318a to 8746fc1 Compare November 25, 2025 23:24
@alexw91 alexw91 changed the title Ability to set "strongly preferred" groups feat: Ability to set "strongly preferred" groups Nov 26, 2025
@jouho jouho requested a review from maddeleine November 26, 2025 20:39
* without sending HRR. (The case of both being non-NULL should never occur, and
* is an error.) */
POSIX_ENSURE((server_curve == NULL) != (server_kem_group == NULL), S2N_ERR_ECDHE_UNSUPPORTED_CURVE);
POSIX_ENSURE((server_curve == NULL) != (server_kem_group == NULL), S2N_ERR_INVALID_SUPPORTED_GROUP_STATE);
Copy link
Contributor

Choose a reason for hiding this comment

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

The PR description for this feature is really light. Can you explain how you've implemented "strongly preferred groups" there? How does this feature interact with our existing behavior of doing HRRs when clients support PQ but don't send a PQ keyshare? Is it the same mechanism used here or not?

Copy link
Contributor Author

@alexw91 alexw91 Nov 27, 2025

Choose a reason for hiding this comment

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

Can you explain how you've implemented "strongly preferred groups" there? ç

Added more detailed description to the PR. Overall the mechanism is described in #5602.

Can you explain how you've implemented "strongly preferred groups" there? How does this feature interact with our existing behavior of doing HRRs when clients support PQ but don't send a PQ keyshare?

I added one new check for a mutually supported strongly-preferred group before the rest of s2n's existing SupportedGroup negotiation logic.

After this change, s2n's new negotiation logic will be:

  1. Strongly-Preferred Groups (using server preference order)
  2. Any PQ SupportedGroup in 1-RTT (using server preference order)
  3. Any PQ SupportedGroup in 2-RTT (using server preference order)
  4. Any ECDHE Group in 1-RTT (using server preference order)
  5. Any mutually supported group in 2-RTT (using server preference order)

Is it the same mechanism used here or not?

Yes, it's a new check before all the same mechanisms are used. All of this KeyShare negotiation logic exists in the function int s2n_extensions_server_key_share_select(struct s2n_connection *conn). Each of these 5 cases is annotated with a code comment. s2n_extensions_server_key_share_select() is responsible for updating and setting exactly one of conn->kex_params.server_ecc_evp_params.negotiated_curve and conn->kex_params.server_kem_group_params.kem_group to the SupportedGroup that s2n would like to negotiate, and (optionally) calling s2n_set_hello_retry_required(conn) if selecting that SupportedGroup would require a 2-RTT handshake.

Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't you reuse the strongly-preferred group mechanism for the PQ groups? You would have to edit all existing PQ policies to include strongly-preferred groups(and probably include a unit test making sure future PQ policies add their PQ groups to the strongly-preferred groups mechanism), but it would be cleaner than having 5 distinct prioritizations.
Then I think it would look like:

  1. Any Strongly-Preferred Group 1 RTT or 2 RTT regardless (using server preference order)
  2. Any mutually supported group in 1-RTT (using server preference order)
  3. Any mutually supported group in 2-RTT (using server preference order)

I haven't quite thought through the consequences of this design, but I'm curious as to why you chose to go with a separate prioritization method rather than merging with the PQ prioritization design.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Merging the "Strongly Preferred" and "PQ" groups would result in different behavior that we wouldn't want. If we added x25519MLKEM768 to the strongly preferred list, but a client sent p256MLKEM768 (or any other PQ) KeyShare, we'd throw away the p256MLKEM768 KeyShare and perform a 2-RTT handshake with x25519MLKEM768.

The "strongly preferred" mechanism is a heavy-handed hammer for forcing a specific small subset of SupportedGroups to be negotiated for compliance-related security policies (like CNSA/FIPS) that we are willing to always take the 2-RTT performance impact for, while still offering backwards compatibility as a last resort to avoid breaking clients.

Our default negotiation mechanism for everything else without a strongly preferred preference list is that we will accept any PQ KeyShare in 1-RTT, and will prioritize a 2-RTT PQ KeyShare over a 1-RTT ECDHE KeyShare.

Copy link
Contributor Author

@alexw91 alexw91 Dec 2, 2025

Choose a reason for hiding this comment

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

Then I think it would look like:

  1. Any Strongly-Preferred Group 1 RTT or 2 RTT regardless (using server preference order)
  2. Any mutually supported group in 1-RTT (using server preference order)
  3. Any mutually supported group in 2-RTT (using server preference order)

This logic has a problem in the following case:

Server Preference: 
 - MLKEM1024, secp384r1, secp256r1
 - strongly_preferred: MLKEM1024, secp384r1

Client Hello: 
    key_share: p384
    supported_group: MLKEM1024, p384, p256

We'd negotiate p384 in 1-RTT instead of MLKEM1024 in 2-RTT

@alexw91 alexw91 force-pushed the strongly_preferred branch from 8d5d00a to f8670b5 Compare December 1, 2025 14:48
BEGIN_TEST();

/* Enforce minimum requirements on all security policies that have strongly preferred groups */
for (size_t policy_index = 0; security_policy_selection[policy_index].version != NULL; policy_index++) {
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 that (for now) we should enforce that the strongly_preferred_groups feature is only ever used on completely ECC policies.

Trying to reason through PQ preferences vs strongly preferred groups is a bit 😵

I agree with Madeleine that the path forward to have some unified way of enforcing the "compliance/alignment" preferences AND PQ preferences. I think this is pretty closely related (imo) of unifying ECC/KEM handling for key exchange. And I think this would also be the time to tackle generating multiple key shares for the client.

But I think that the approach in the current PR is sufficient to satisfying our goals for the ECC case.

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 that will also make some of the strongly preferred group logic a bit easier to handle, since we currently store PQ group and ECC groups in separate lists.

@alexw91 alexw91 force-pushed the strongly_preferred branch from f8670b5 to efd62de Compare December 3, 2025 16:27
Copy link
Contributor

@jmayclin jmayclin left a comment

Choose a reason for hiding this comment

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

A few nits.

I think generally I'd prefer to remove the PQ-only stuff since we aren't allowing that rn. Was the thought to keep it in in case it's used in the future?


POSIX_ENSURE_REF(security_policy->kem_preferences);

/* Temporarily require that policies either have PQ or strongly supported groups, but not both. */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/* Temporarily require that policies either have PQ or strongly supported groups, but not both. */
/* Temporarily require that policies don't allow both PQ and strongly supported groups. */

Comment on lines 50 to 54
for (size_t i = 0; i < security_policy->kem_preferences->tls13_kem_group_count; i++) {
const struct s2n_kem_group *kem_group = security_policy->kem_preferences->tls13_kem_groups[i];
POSIX_ENSURE_REF(kem_group);
ordered_supported_groups[ordered_supported_group_count++] = kem_group->iana_id;
}
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 we can remove this now that we've enforcing no PQ.

Suggested change
for (size_t i = 0; i < security_policy->kem_preferences->tls13_kem_group_count; i++) {
const struct s2n_kem_group *kem_group = security_policy->kem_preferences->tls13_kem_groups[i];
POSIX_ENSURE_REF(kem_group);
ordered_supported_groups[ordered_supported_group_count++] = kem_group->iana_id;
}

}

END_TEST();
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}
}

Comment on lines 24 to 31
const uint16_t cnsa_1_2_supported_group_iana_ids[] = {
TLS_PQ_KEM_GROUP_ID_MLKEM_1024,
TLS_EC_CURVE_SECP_384_R1
};

const uint16_t cnsa_2_supported_group_iana_ids[] = {
TLS_PQ_KEM_GROUP_ID_MLKEM_1024
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Since these can't be used for now, I'd vote to nuke em

Suggested change
const uint16_t cnsa_1_2_supported_group_iana_ids[] = {
TLS_PQ_KEM_GROUP_ID_MLKEM_1024,
TLS_EC_CURVE_SECP_384_R1
};
const uint16_t cnsa_2_supported_group_iana_ids[] = {
TLS_PQ_KEM_GROUP_ID_MLKEM_1024
};

Comment on lines 38 to 46
const struct s2n_supported_group_preferences cnsa_1_2_strong_preference = {
.count = s2n_array_len(cnsa_1_2_supported_group_iana_ids),
.iana_ids = cnsa_1_2_supported_group_iana_ids
};

const struct s2n_supported_group_preferences cnsa_2_strong_preference = {
.count = s2n_array_len(cnsa_2_supported_group_iana_ids),
.iana_ids = cnsa_2_supported_group_iana_ids
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const struct s2n_supported_group_preferences cnsa_1_2_strong_preference = {
.count = s2n_array_len(cnsa_1_2_supported_group_iana_ids),
.iana_ids = cnsa_1_2_supported_group_iana_ids
};
const struct s2n_supported_group_preferences cnsa_2_strong_preference = {
.count = s2n_array_len(cnsa_2_supported_group_iana_ids),
.iana_ids = cnsa_2_supported_group_iana_ids
};

Comment on lines 416 to 432
for (int j = 0; j < S2N_KEM_GROUPS_COUNT && !matched_strongly_preferred_iana; j++) {
const struct s2n_kem_group *mutually_supported_kem_group = conn->kex_params.mutually_supported_kem_groups[j];
if (mutually_supported_kem_group == NULL) {
break; /* Reached end of mutually supported KEM Groups */
}
if (strongly_preferred_iana == mutually_supported_kem_group->iana_id) {
matched_strongly_preferred_iana = true;
strongly_preferred_kem_group = mutually_supported_kem_group;

/* Check if we can negotiate our strongly preferred KEM Group in 1-RTT */
if (client_kem_group != NULL && (strongly_preferred_iana == client_kem_group->iana_id)) {
need_hrr_for_strongly_preferred_group = false;
} else {
need_hrr_for_strongly_preferred_group = true;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for (int j = 0; j < S2N_KEM_GROUPS_COUNT && !matched_strongly_preferred_iana; j++) {
const struct s2n_kem_group *mutually_supported_kem_group = conn->kex_params.mutually_supported_kem_groups[j];
if (mutually_supported_kem_group == NULL) {
break; /* Reached end of mutually supported KEM Groups */
}
if (strongly_preferred_iana == mutually_supported_kem_group->iana_id) {
matched_strongly_preferred_iana = true;
strongly_preferred_kem_group = mutually_supported_kem_group;
/* Check if we can negotiate our strongly preferred KEM Group in 1-RTT */
if (client_kem_group != NULL && (strongly_preferred_iana == client_kem_group->iana_id)) {
need_hrr_for_strongly_preferred_group = false;
} else {
need_hrr_for_strongly_preferred_group = true;
}
}
}
/* strongly preferred groups and not allowed on policies that support PQ, so we don't check KEMs */

@jmayclin jmayclin added this pull request to the merge queue Dec 4, 2025
Merged via the queue into aws:main with commit f1b90fe Dec 4, 2025
53 of 54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: ability to set "strongly preferred" groups

4 participants