Skip to content

Commit 8fdef8d

Browse files
committed
fix: advertise X25519 only when CNG supports it
1 parent cb55f2e commit 8fdef8d

4 files changed

Lines changed: 52 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ jobs:
4747
toolchain: stable
4848
- name: Cache build artifacts
4949
uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
50-
- name: X25519 CNG import diagnostics
51-
run: cargo test x25519_cng_import_diagnostics -- --nocapture
5250
- name: cargo test
5351
run: cargo test
5452
# https://github.com/rust-lang/cargo/issues/6669

src/kx.rs

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/)
44
// Copyright 2026 Datadog, Inc.
55

6+
use once_cell::sync::Lazy;
67
use rustls::crypto::{ActiveKeyExchange, SharedSecret, SupportedKxGroup};
78
use rustls::{Error, NamedGroup};
89
use windows::core::Owned;
@@ -26,6 +27,13 @@ const MAX_SECRET_SIZE: usize = 48;
2627
/// * [SECP256R1]
2728
///
2829
pub 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)]
3139
enum KxGroup {
@@ -67,6 +75,20 @@ 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+
let u = [
84+
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,
85+
0, 0, 0,
86+
];
87+
let y = [0; 32];
88+
89+
import_ecdh_public_key(KxGroup::X25519.alg_handle(), &u, &y).is_ok()
90+
}
91+
7092
struct EcKeyExchange {
7193
kx_group: KxGroup,
7294
key_handle: Owned<BCRYPT_KEY_HANDLE>,
@@ -83,6 +105,11 @@ pub const SECP256R1: &dyn SupportedKxGroup = &KxGroup::SECP256R1;
83105
/// secp384r1 key exchange group as registered with [IANA](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8)
84106
pub const SECP384R1: &dyn SupportedKxGroup = &KxGroup::SECP384R1;
85107

108+
/// Returns key exchange groups usable by the host CNG implementation.
109+
pub fn default_kx_groups() -> Vec<&'static dyn SupportedKxGroup> {
110+
DEFAULT_KX_GROUPS.clone()
111+
}
112+
86113
impl SupportedKxGroup for KxGroup {
87114
fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
88115
let mut key_handle = Owned::default();
@@ -247,10 +274,15 @@ mod test {
247274
use windows::core::Owned;
248275
use wycheproof::{ecdh::TestName, TestResult};
249276

250-
use crate::{
251-
keys::{import_ecdh_private_key, import_ecdh_public_key},
252-
kx::EcKeyExchange,
253-
};
277+
use crate::{keys::import_ecdh_private_key, kx::EcKeyExchange};
278+
279+
#[test]
280+
fn default_kx_groups_match_cng_x25519_support() {
281+
let advertises_x25519 = super::default_kx_groups()
282+
.iter()
283+
.any(|kx_group| kx_group.name() == rustls::NamedGroup::X25519);
284+
assert_eq!(advertises_x25519, super::cng_supports_x25519());
285+
}
254286

255287
#[test]
256288
fn secp256r1() {
@@ -288,53 +320,11 @@ mod test {
288320
}
289321

290322
#[test]
291-
fn x25519_cng_import_diagnostics() {
292-
let cases = [
293-
(
294-
"tc1 normal",
295-
"504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829",
296-
Some("98c969d09aeecfe56f44da8c143e7c739afc6d3ac26cd099a73436fec147a908"),
297-
Some("08a947c1fe3634a799d06cc23a6dfc9a737c3e148cda446fe5cfee9ad069c998"),
298-
),
299-
(
300-
"tc2 twist",
301-
"63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733",
302-
None,
303-
None,
304-
),
305-
(
306-
"tc34 special-valid-u4",
307-
"0400000000000000000000000000000000000000000000000000000000000000",
308-
Some("a0d42a061659386f4553ce7564094b15d73bc8a36340191b74c1e56f8a050469"),
309-
Some("6904058a6fe5c1741b194063a3c83bd7154b096475ce53456f385916062ad4a0"),
310-
),
311-
];
312-
let zero_y = [0u8; 32];
313-
314-
for (name, x_hex, y_le_hex, y_be_hex) in cases {
315-
let x = hex::decode(x_hex).unwrap();
316-
println!("x25519 import diagnostic: {name}");
317-
318-
let zero_res =
319-
import_ecdh_public_key(crate::kx::KxGroup::X25519.alg_handle(), &x, &zero_y);
320-
println!(" y=zero: {:?}", zero_res.as_ref().map(|_| ()));
321-
322-
if let Some(y_hex) = y_le_hex {
323-
let y = hex::decode(y_hex).unwrap();
324-
let res = import_ecdh_public_key(crate::kx::KxGroup::X25519.alg_handle(), &x, &y);
325-
println!(" y=sqrt-le: {:?}", res.as_ref().map(|_| ()));
326-
}
327-
328-
if let Some(y_hex) = y_be_hex {
329-
let y = hex::decode(y_hex).unwrap();
330-
let res = import_ecdh_public_key(crate::kx::KxGroup::X25519.alg_handle(), &x, &y);
331-
println!(" y=sqrt-be: {:?}", res.as_ref().map(|_| ()));
332-
}
323+
fn x25519() {
324+
if !super::cng_supports_x25519() {
325+
return;
333326
}
334-
}
335327

336-
#[test]
337-
fn x25519() {
338328
let test_set = wycheproof::xdh::TestSet::load(wycheproof::xdh::TestName::X25519).unwrap();
339329

340330
let mut counter = 0;

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub use verify::SUPPORTED_SIG_ALGS;
130130
pub fn default_provider() -> CryptoProvider {
131131
CryptoProvider {
132132
cipher_suites: ALL_CIPHER_SUITES.to_vec(),
133-
kx_groups: ALL_KX_GROUPS.to_vec(),
133+
kx_groups: kx::default_kx_groups(),
134134
signature_verification_algorithms: SUPPORTED_SIG_ALGS,
135135
secure_random: &SecureRandom,
136136
key_provider: &KeyProvider,

src/verify.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,13 +437,21 @@ mod tests {
437437
for test_group in test_set.test_groups {
438438
for test in test_group.tests {
439439
let res = alg.verify_signature(&test_group.key.key, &test.msg, &test.sig);
440-
let expected_failure = test.flags.contains(&TestFlag::EdgeCaseShamirMultiplication);
441440

442-
match (&test.result, expected_failure) {
443-
(TestResult::Acceptable | TestResult::Valid, false) => {
441+
if test.result == TestResult::Valid
442+
&& test.flags.contains(&TestFlag::EdgeCaseShamirMultiplication)
443+
{
444+
// Windows CNG versions differ on these valid arithmetic edge cases:
445+
// Windows Server 2022 rejects them, while Windows Server 2025 accepts them.
446+
// Invalid signatures below must still be rejected.
447+
continue;
448+
}
449+
450+
match test.result {
451+
TestResult::Acceptable | TestResult::Valid => {
444452
assert!(res.is_ok(), "Failed test: {test:?}");
445453
}
446-
_ => {
454+
TestResult::Invalid => {
447455
assert!(res.is_err(), "Failed test: {test:?}");
448456
}
449457
}

0 commit comments

Comments
 (0)