Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ jobs:
run: make check-licenses

test:
name: Test
runs-on: windows-2022
name: Test (${{ matrix.runner }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner: [windows-2022, windows-2025]
steps:
- name: Check out repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down
45 changes: 45 additions & 0 deletions src/kx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/)
// Copyright 2026 Datadog, Inc.

use once_cell::sync::Lazy;
use rustls::crypto::{ActiveKeyExchange, SharedSecret, SupportedKxGroup};
use rustls::{Error, NamedGroup};
use windows::core::Owned;
Expand All @@ -26,6 +27,13 @@ const MAX_SECRET_SIZE: usize = 48;
/// * [SECP256R1]
///
pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[X25519, SECP256R1, SECP384R1];
static DEFAULT_KX_GROUPS: Lazy<Vec<&'static dyn SupportedKxGroup>> = Lazy::new(|| {
ALL_KX_GROUPS
.iter()
.copied()
.filter(|kx_group| usable_kx_group(*kx_group))
.collect()
});

#[derive(Debug, Copy, Clone)]
enum KxGroup {
Expand Down Expand Up @@ -67,6 +75,26 @@ impl KxGroup {
}
}

fn usable_kx_group(kx_group: &dyn SupportedKxGroup) -> bool {
kx_group.name() != NamedGroup::X25519 || cng_supports_x25519()
}

fn cng_supports_x25519() -> bool {
// Windows CNG's Curve25519 public-key import behavior differs by OS version. Windows Server
// 2022 accepts the X25519 Wycheproof `u = 4` vector, but Windows Server 2025 rejects it with
// STATUS_INVALID_PARAMETER even when the import blob includes a valid Montgomery `v`
// coordinate. That vector is a valid X25519 input, so a CNG backend that rejects it should not
// advertise X25519 for TLS negotiation. Probe the same public key shape used by the provider and
// leave X25519 available only on hosts whose CNG implementation can import it.
let u = [
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,
0, 0, 0,
];
let y = [0; 32];

import_ecdh_public_key(KxGroup::X25519.alg_handle(), &u, &y).is_ok()
}

struct EcKeyExchange {
kx_group: KxGroup,
key_handle: Owned<BCRYPT_KEY_HANDLE>,
Expand All @@ -83,6 +111,11 @@ pub const SECP256R1: &dyn SupportedKxGroup = &KxGroup::SECP256R1;
/// secp384r1 key exchange group as registered with [IANA](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8)
pub const SECP384R1: &dyn SupportedKxGroup = &KxGroup::SECP384R1;

/// Returns key exchange groups usable by the host CNG implementation.
pub fn default_kx_groups() -> Vec<&'static dyn SupportedKxGroup> {
DEFAULT_KX_GROUPS.clone()
}

impl SupportedKxGroup for KxGroup {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
let mut key_handle = Owned::default();
Expand Down Expand Up @@ -249,6 +282,14 @@ mod test {

use crate::{keys::import_ecdh_private_key, kx::EcKeyExchange};

#[test]
fn default_kx_groups_match_cng_x25519_support() {
let advertises_x25519 = super::default_kx_groups()
.iter()
.any(|kx_group| kx_group.name() == rustls::NamedGroup::X25519);
assert_eq!(advertises_x25519, super::cng_supports_x25519());
}

#[test]
fn secp256r1() {
let test_set = wycheproof::ecdh::TestSet::load(TestName::EcdhSecp256r1Ecpoint).unwrap();
Expand Down Expand Up @@ -286,6 +327,10 @@ mod test {

#[test]
fn x25519() {
if !super::cng_supports_x25519() {
return;
}

let test_set = wycheproof::xdh::TestSet::load(wycheproof::xdh::TestName::X25519).unwrap();

let mut counter = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub use verify::SUPPORTED_SIG_ALGS;
pub fn default_provider() -> CryptoProvider {
CryptoProvider {
cipher_suites: ALL_CIPHER_SUITES.to_vec(),
kx_groups: ALL_KX_GROUPS.to_vec(),
kx_groups: kx::default_kx_groups(),
signature_verification_algorithms: SUPPORTED_SIG_ALGS,
secure_random: &SecureRandom,
key_provider: &KeyProvider,
Expand Down
16 changes: 12 additions & 4 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,13 +437,21 @@ mod tests {
for test_group in test_set.test_groups {
for test in test_group.tests {
let res = alg.verify_signature(&test_group.key.key, &test.msg, &test.sig);
let expected_failure = test.flags.contains(&TestFlag::EdgeCaseShamirMultiplication);

match (&test.result, expected_failure) {
(TestResult::Acceptable | TestResult::Valid, false) => {
if test.result == TestResult::Valid
&& test.flags.contains(&TestFlag::EdgeCaseShamirMultiplication)
{
// Windows CNG versions differ on these valid arithmetic edge cases:
// Windows Server 2022 rejects them, while Windows Server 2025 accepts them.
// Invalid signatures below must still be rejected.
continue;
}

match test.result {
TestResult::Acceptable | TestResult::Valid => {
assert!(res.is_ok(), "Failed test: {test:?}");
}
_ => {
TestResult::Invalid => {
assert!(res.is_err(), "Failed test: {test:?}");
}
}
Expand Down
11 changes: 11 additions & 0 deletions tests/it.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ use webpki::EndEntityCert;

pub mod server;

fn cng_supports_group(group: &'static dyn SupportedKxGroup) -> bool {
default_provider()
.kx_groups
.iter()
.any(|supported| supported.name() == group.name())
}

fn test_with_provider(
provider: CryptoProvider,
port: u16,
Expand Down Expand Up @@ -157,6 +164,10 @@ fn test_client_and_server(
#[case] alg: &'static rcgen::SignatureAlgorithm,
#[case] expected: CipherSuite,
) {
if !cng_supports_group(group) {
return;
}

// Run against a server using our default provider
let (port, certificate) = start_server(alg);
let provider = custom_provider(vec![suite], vec![group]);
Expand Down
Loading