33// This product includes software developed at Datadog (https://www.datadoghq.com/)
44// Copyright 2026 Datadog, Inc.
55
6+ use once_cell:: sync:: Lazy ;
67use rustls:: crypto:: { ActiveKeyExchange , SharedSecret , SupportedKxGroup } ;
78use rustls:: { Error , NamedGroup } ;
89use windows:: core:: Owned ;
@@ -26,6 +27,13 @@ const MAX_SECRET_SIZE: usize = 48;
2627/// * [SECP256R1]
2728///
2829pub const ALL_KX_GROUPS : & [ & dyn SupportedKxGroup ] = & [ X25519 , SECP256R1 , SECP384R1 ] ;
30+ static DEFAULT_KX_GROUPS : Lazy < Vec < & ' static dyn SupportedKxGroup > > = Lazy :: new ( || {
31+ ALL_KX_GROUPS
32+ . iter ( )
33+ . copied ( )
34+ . filter ( |kx_group| usable_kx_group ( * kx_group) )
35+ . collect ( )
36+ } ) ;
2937
3038#[ derive( Debug , Copy , Clone ) ]
3139enum KxGroup {
@@ -67,6 +75,26 @@ impl KxGroup {
6775 }
6876}
6977
78+ fn usable_kx_group ( kx_group : & dyn SupportedKxGroup ) -> bool {
79+ kx_group. name ( ) != NamedGroup :: X25519 || cng_supports_x25519 ( )
80+ }
81+
82+ fn cng_supports_x25519 ( ) -> bool {
83+ // Windows CNG's Curve25519 public-key import behavior differs by OS version. Windows Server
84+ // 2022 accepts the X25519 Wycheproof `u = 4` vector, but Windows Server 2025 rejects it with
85+ // STATUS_INVALID_PARAMETER even when the import blob includes a valid Montgomery `v`
86+ // coordinate. That vector is a valid X25519 input, so a CNG backend that rejects it should not
87+ // advertise X25519 for TLS negotiation. Probe the same public key shape used by the provider and
88+ // leave X25519 available only on hosts whose CNG implementation can import it.
89+ let u = [
90+ 0x04 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
91+ 0 , 0 , 0 ,
92+ ] ;
93+ let y = [ 0 ; 32 ] ;
94+
95+ import_ecdh_public_key ( KxGroup :: X25519 . alg_handle ( ) , & u, & y) . is_ok ( )
96+ }
97+
7098struct EcKeyExchange {
7199 kx_group : KxGroup ,
72100 key_handle : Owned < BCRYPT_KEY_HANDLE > ,
@@ -83,6 +111,11 @@ pub const SECP256R1: &dyn SupportedKxGroup = &KxGroup::SECP256R1;
83111/// secp384r1 key exchange group as registered with [IANA](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8)
84112pub const SECP384R1 : & dyn SupportedKxGroup = & KxGroup :: SECP384R1 ;
85113
114+ /// Returns key exchange groups usable by the host CNG implementation.
115+ pub fn default_kx_groups ( ) -> Vec < & ' static dyn SupportedKxGroup > {
116+ DEFAULT_KX_GROUPS . clone ( )
117+ }
118+
86119impl SupportedKxGroup for KxGroup {
87120 fn start ( & self ) -> Result < Box < dyn ActiveKeyExchange > , Error > {
88121 let mut key_handle = Owned :: default ( ) ;
@@ -249,6 +282,14 @@ mod test {
249282
250283 use crate :: { keys:: import_ecdh_private_key, kx:: EcKeyExchange } ;
251284
285+ #[ test]
286+ fn default_kx_groups_match_cng_x25519_support ( ) {
287+ let advertises_x25519 = super :: default_kx_groups ( )
288+ . iter ( )
289+ . any ( |kx_group| kx_group. name ( ) == rustls:: NamedGroup :: X25519 ) ;
290+ assert_eq ! ( advertises_x25519, super :: cng_supports_x25519( ) ) ;
291+ }
292+
252293 #[ test]
253294 fn secp256r1 ( ) {
254295 let test_set = wycheproof:: ecdh:: TestSet :: load ( TestName :: EcdhSecp256r1Ecpoint ) . unwrap ( ) ;
@@ -286,6 +327,10 @@ mod test {
286327
287328 #[ test]
288329 fn x25519 ( ) {
330+ if !super :: cng_supports_x25519 ( ) {
331+ return ;
332+ }
333+
289334 let test_set = wycheproof:: xdh:: TestSet :: load ( wycheproof:: xdh:: TestName :: X25519 ) . unwrap ( ) ;
290335
291336 let mut counter = 0 ;
0 commit comments