diff --git a/Cargo.lock b/Cargo.lock index 6a0e49846..7cd243c90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,8 +567,7 @@ dependencies = [ [[package]] name = "dimpl" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +source = "git+https://github.com/algesten/dimpl.git#52e8331274e5bc7399246363080b27bc8d2f0fc1" dependencies = [ "aes-gcm", "arrayvec", @@ -2017,6 +2016,7 @@ dependencies = [ name = "str0m-openssl" version = "0.2.0" dependencies = [ + "dimpl", "libc", "openssl", "openssl-sys", @@ -2055,6 +2055,7 @@ dependencies = [ name = "str0m-wincrypto" version = "0.4.0" dependencies = [ + "dimpl", "str0m-proto", "tracing", "windows", diff --git a/Cargo.toml b/Cargo.toml index 951e38bea..ae995e1a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,10 @@ apple-crypto = ["dep:str0m-apple-crypto"] vendored = ["str0m-openssl?/vendored"] unversioned = [] +# For crypto providers that have dimpl and non-dimpl DTLS support, use dimpl. +# This has no effect for providers that always dimpl. +prefer_dimpl = ["str0m-openssl?/prefer_dimpl", "str0m-wincrypto?/prefer_dimpl"] + # Redacts personally identifiable information (PII) from logs debug and above pii = [] @@ -56,7 +60,8 @@ crc = "3" serde = { version = "1.0.152", features = ["derive"] } # DTLS made for str0m (required dependency) -dimpl = { version = "0.4.3", default-features = false } +#dimpl = { version = "0.4.3", default-features = false } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false } time = "0.3" diff --git a/crypto/apple-crypto/Cargo.lock b/crypto/apple-crypto/Cargo.lock index 9ca86dbdd..7eecba7e6 100644 --- a/crypto/apple-crypto/Cargo.lock +++ b/crypto/apple-crypto/Cargo.lock @@ -100,8 +100,7 @@ dependencies = [ [[package]] name = "dimpl" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +source = "git+https://github.com/algesten/dimpl.git#52e8331274e5bc7399246363080b27bc8d2f0fc1" dependencies = [ "arrayvec", "log", diff --git a/crypto/apple-crypto/Cargo.toml b/crypto/apple-crypto/Cargo.toml index cd5184ed4..605ceaab7 100644 --- a/crypto/apple-crypto/Cargo.toml +++ b/crypto/apple-crypto/Cargo.toml @@ -12,7 +12,8 @@ rust-version = "1.85.0" [dependencies] str0m-proto = { version = "0.2.0", path = "../../proto" } -dimpl = { version = "0.4.3", default-features = false } +#dimpl = { version = "0.4.3", default-features = false } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false} time = "0.3" # Apple platform bindings diff --git a/crypto/apple-crypto/src/cert.rs b/crypto/apple-crypto/src/cert.rs new file mode 100644 index 000000000..fd5e5e4f8 --- /dev/null +++ b/crypto/apple-crypto/src/cert.rs @@ -0,0 +1,207 @@ +//! Certificate generation routines. +//! + +use str0m_proto::crypto::CryptoError; + +// --------------------------------------------------------------------------- +// X.509 / PKCS#8 certificate helpers +// --------------------------------------------------------------------------- + +/// Build a self-signed X.509 v3 certificate. +pub fn build_self_signed_certificate( + common_name: &str, + serial_number: [u8; 16], + public_key_bytes: &[u8], + sign_with_ecdsa_sha256: SignFn, +) -> Result, CryptoError> +where + SignFn: FnOnce(&[u8]) -> Result, CryptoError>, +{ + let mut tbs_certificate = Vec::new(); + + // Version: v3 (encoded as [0] EXPLICIT INTEGER 2) + let version = encode_explicit_tag(0, &encode_integer(&[2])); + tbs_certificate.extend_from_slice(&version); + + // Serial number (random) + tbs_certificate.extend_from_slice(&encode_integer(&serial_number)); + + // Signature algorithm: ecdsa-with-SHA256 + let ecdsa_with_sha256_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; + tbs_certificate.extend_from_slice(&encode_algorithm_identifier(ecdsa_with_sha256_oid)); + + // Issuer: CN=WebRTC + let issuer = encode_name(common_name); + tbs_certificate.extend_from_slice(&issuer); + + // Validity: 1 year from now + let validity = encode_validity()?; + tbs_certificate.extend_from_slice(&validity); + + // Subject: CN=WebRTC (same as issuer for self-signed) + tbs_certificate.extend_from_slice(&issuer); + + // Subject Public Key Info + let spki = encode_ec_public_key_info(public_key_bytes)?; + tbs_certificate.extend_from_slice(&spki); + + let tbs_certificate = encode_sequence(&tbs_certificate); + + // Sign the TBS certificate using the private key directly + let signature = sign_with_ecdsa_sha256(&tbs_certificate)?; + + // Encode the full certificate + let ecdsa_with_sha256_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; // 1.2.840.10045.4.3.2 + let sig_algorithm = encode_algorithm_identifier(ecdsa_with_sha256_oid); + + // Signature as BIT STRING (prepend 0x00 for no unused bits) + let signature_bits = encode_bit_string(&signature); + + // Full certificate SEQUENCE + let mut signed_certificate = Vec::new(); + signed_certificate.extend_from_slice(&tbs_certificate); + signed_certificate.extend_from_slice(&sig_algorithm); + signed_certificate.extend_from_slice(&signature_bits); + + Ok(encode_sequence(&signed_certificate)) +} + +/// Build a PKCS#8 `PrivateKeyInfo` for an EC P-256 key. +pub fn build_pkcs8( + private_scalar: &[u8; 32], + public_key_bytes: &[u8], +) -> Result, CryptoError> { + // Version: 0 + let version = encode_integer(&[0]); + + // Algorithm: ecPublicKey with prime256v1 + let ec_public_key_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; + let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; + let algorithm = + encode_sequence(&[encode_oid(ec_public_key_oid), encode_oid(prime256v1_oid)].concat()); + + // PrivateKey: SEC1 ECPrivateKey wrapped in OCTET STRING + let ec_private_key = encode_ec_private_key(private_scalar, public_key_bytes)?; + let private_key = encode_tag(0x04, &ec_private_key); // OCTET STRING + + Ok(encode_sequence(&[version, algorithm, private_key].concat())) +} + +// --- ASN.1 DER encoding helpers (private) --- + +fn encode_sequence(content: &[u8]) -> Vec { + encode_tag(0x30, content) +} + +fn encode_tag(tag: u8, content: &[u8]) -> Vec { + let mut result = vec![tag]; + encode_length(content.len(), &mut result); + result.extend_from_slice(content); + result +} + +fn encode_length(len: usize, out: &mut Vec) { + if len < 128 { + // len fits in a single byte + } else if len < 256 { + out.push(0x81); + } else { + out.push(0x82); + out.push((len >> 8) as u8); + } + out.push(len as u8); +} + +fn encode_integer(value: &[u8]) -> Vec { + // Skip leading zeros but keep at least one byte + let mut start = 0; + while start < value.len() - 1 && value[start] == 0 { + start += 1; + } + + let value = &value[start..]; + + // If high bit is set, prepend 0x00 + if value[0] & 0x80 != 0 { + let mut content = vec![0x00]; + content.extend_from_slice(value); + encode_tag(0x02, &content) + } else { + encode_tag(0x02, value) + } +} + +fn encode_explicit_tag(tag_num: u8, content: &[u8]) -> Vec { + encode_tag(0xA0 | tag_num, content) +} + +fn encode_oid(oid_bytes: &[u8]) -> Vec { + encode_tag(0x06, oid_bytes) +} + +fn encode_algorithm_identifier(oid_bytes: &[u8]) -> Vec { + let oid = encode_oid(oid_bytes); + encode_sequence(&oid) +} + +fn encode_name(cn: &str) -> Vec { + // CN OID: 2.5.4.3 + let cn_oid = &[0x55, 0x04, 0x03]; + let oid = encode_oid(cn_oid); + let value = encode_tag(0x0C, cn.as_bytes()); // UTF8String + let attr_type_value = encode_sequence(&[oid, value].concat()); + let rdn = encode_tag(0x31, &attr_type_value); // SET + encode_sequence(&rdn) +} + +fn encode_validity() -> Result, CryptoError> { + // For simplicity, use fixed dates that are valid + // Format: YYYYMMDDHHMMSSZ + let not_before = b"20240101000000Z"; + let not_after = b"20251231235959Z"; + + let nb = encode_tag(0x18, not_before); // GeneralizedTime + let na = encode_tag(0x18, not_after); + + Ok(encode_sequence(&[nb, na].concat())) +} + +fn encode_ec_public_key_info(public_key_bytes: &[u8]) -> Result, CryptoError> { + // OID: 1.2.840.10045.2.1 (ecPublicKey) + let ec_public_key_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; + // OID: 1.2.840.10045.3.1.7 (prime256v1/secp256r1) + let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; + + let algorithm = + encode_sequence(&[encode_oid(ec_public_key_oid), encode_oid(prime256v1_oid)].concat()); + + let public_key_bits = encode_bit_string(public_key_bytes); + + Ok(encode_sequence(&[algorithm, public_key_bits].concat())) +} + +fn encode_bit_string(data: &[u8]) -> Vec { + let mut content = vec![0x00]; // No unused bits + content.extend_from_slice(data); + encode_tag(0x03, &content) +} + +fn encode_ec_private_key( + private_scalar: &[u8], + public_key_bytes: &[u8], +) -> Result, CryptoError> { + let version = encode_integer(&[1]); + let private_key = encode_tag(0x04, private_scalar); // OCTET STRING + + // Parameters: prime256v1 OID + let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; + let params = encode_explicit_tag(0, &encode_oid(prime256v1_oid)); + + // Public key as [1] BIT STRING + let public_key_bits = encode_bit_string(public_key_bytes); + let public_key = encode_explicit_tag(1, &public_key_bits); + + Ok(encode_sequence( + &[version, private_key, params, public_key].concat(), + )) +} diff --git a/crypto/apple-crypto/src/dimpl_provider/hash.rs b/crypto/apple-crypto/src/dimpl_provider/hash.rs index 7d8e09d17..2a2fbe136 100644 --- a/crypto/apple-crypto/src/dimpl_provider/hash.rs +++ b/crypto/apple-crypto/src/dimpl_provider/hash.rs @@ -99,122 +99,3 @@ impl HashContext for Sha384Context { out.extend_from_slice(&digest); } } - -#[cfg(test)] -mod test { - use super::*; - - // Test vectors from NIST CAVP - // https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program - fn slice_to_hex(data: &[u8]) -> String { - let mut s = String::new(); - for byte in data.iter() { - s.push_str(&format!("{:02x}", byte)); - } - s - } - - // SHA-256 Test Vectors - - #[test] - fn test_sha256_context_single_update() { - let mut ctx = Sha256Context::new(); - ctx.update(b"abc"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - ); - } - - #[test] - fn test_sha256_context_multiple_updates() { - let mut ctx = Sha256Context::new(); - ctx.update(b"abcdbcde"); - ctx.update(b"cdefdefg"); - ctx.update(b"efghfghighijhijkijkljklmklmnlmnomnopnopq"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" - ); - } - - #[test] - fn test_sha256_context_snapshot() { - let mut ctx = Sha256Context::new(); - ctx.update(b"abc"); - let mut out1 = Buf::new(); - ctx.clone_and_finalize(&mut out1); - - // Update again and verify first snapshot is unchanged - ctx.update(b"def"); - let mut out2 = Buf::new(); - ctx.clone_and_finalize(&mut out2); - - assert_eq!( - slice_to_hex(&out1), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - ); - assert_eq!( - slice_to_hex(&out2), - "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721" - ); - } - - // SHA-384 Test Vectors - - #[test] - fn test_sha384_context_single_update() { - let mut ctx = Sha384Context::new(); - ctx.update(b"abc"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7" - ); - } - - #[test] - fn test_sha384_context_multiple_updates() { - let mut ctx = Sha384Context::new(); - ctx.update(b"abcdefghbcdefghicdefghijdefghijk"); - ctx.update(b"efghijklfghijklmghijklmnhijklmno"); - ctx.update(b"ijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039" - ); - } - - #[test] - fn test_hash_provider_sha256() { - let provider = AppleHashProvider; - let mut ctx = provider.create_hash(HashAlgorithm::SHA256); - ctx.update(b"abc"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - ); - } - - #[test] - fn test_hash_provider_sha384() { - let provider = AppleHashProvider; - let mut ctx = provider.create_hash(HashAlgorithm::SHA384); - ctx.update(b"abc"); - let mut out = Buf::new(); - ctx.clone_and_finalize(&mut out); - assert_eq!( - slice_to_hex(&out), - "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7" - ); - } -} diff --git a/crypto/apple-crypto/src/dimpl_provider/hkdf.rs b/crypto/apple-crypto/src/dimpl_provider/hkdf.rs deleted file mode 100644 index 1614350c4..000000000 --- a/crypto/apple-crypto/src/dimpl_provider/hkdf.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! HKDF implementation using Apple CommonCrypto HMAC for TLS 1.3 key derivation. - -use dimpl::crypto::{Buf, HashAlgorithm, HkdfProvider}; - -#[derive(Debug)] -pub(super) struct AppleHkdfProvider; - -/// Compute HMAC for the given hash algorithm. -fn hmac(hash: HashAlgorithm, key: &[u8], data: &[u8]) -> Result, String> { - match hash { - HashAlgorithm::SHA256 => { - let result = apple_cryptokit::hmac_sha256(key, data).map_err(|e| format!("{e:?}"))?; - Ok(result.to_vec()) - } - HashAlgorithm::SHA384 => { - let result = apple_cryptokit::authentication::sha384::hmac_sha384(key, data) - .map_err(|e| format!("{e:?}"))?; - Ok(result.to_vec()) - } - _ => Err(format!("Unsupported hash for HKDF: {hash:?}")), - } -} - -impl HkdfProvider for AppleHkdfProvider { - fn hkdf_extract( - &self, - hash: HashAlgorithm, - salt: &[u8], - ikm: &[u8], - out: &mut Buf, - ) -> Result<(), String> { - out.clear(); - - // HKDF-Extract: PRK = HMAC-Hash(salt, IKM) - // If salt is empty, use a zero-filled salt of hash length - let hash_len = hash.output_len(); - let zero_salt: Vec; - let actual_salt = if salt.is_empty() { - zero_salt = vec![0u8; hash_len]; - &zero_salt[..] - } else { - salt - }; - - let prk = hmac(hash, actual_salt, ikm)?; - out.extend_from_slice(&prk); - Ok(()) - } - - fn hkdf_expand( - &self, - hash: HashAlgorithm, - prk: &[u8], - info: &[u8], - out: &mut Buf, - output_len: usize, - ) -> Result<(), String> { - out.clear(); - - // HKDF-Expand per RFC 5869 Section 2.3 - // T(0) = empty - // T(i) = HMAC-Hash(PRK, T(i-1) || info || i) for i = 1..N - let hash_len = hash.output_len(); - let n = output_len.div_ceil(hash_len); - if n > 255 { - return Err("HKDF output too long".into()); - } - - let mut t_prev = Vec::new(); - let mut okm = Vec::with_capacity(output_len); - - for i in 1..=n { - let mut input = Vec::with_capacity(t_prev.len() + info.len() + 1); - input.extend_from_slice(&t_prev); - input.extend_from_slice(info); - input.push(i as u8); - - t_prev = hmac(hash, prk, &input)?; - okm.extend_from_slice(&t_prev); - } - - okm.truncate(output_len); - out.extend_from_slice(&okm); - Ok(()) - } - - fn hkdf_expand_label( - &self, - hash: HashAlgorithm, - secret: &[u8], - label: &[u8], - context: &[u8], - out: &mut Buf, - output_len: usize, - ) -> Result<(), String> { - // HkdfLabel per RFC 8446 Section 7.1 with "tls13 " prefix - let info = build_hkdf_label(b"tls13 ", label, context, output_len)?; - self.hkdf_expand(hash, secret, &info, out, output_len) - } - - fn hkdf_expand_label_dtls13( - &self, - hash: HashAlgorithm, - secret: &[u8], - label: &[u8], - context: &[u8], - out: &mut Buf, - output_len: usize, - ) -> Result<(), String> { - // HkdfLabel per RFC 9147 with "dtls13" prefix (no space) - let info = build_hkdf_label(b"dtls13", label, context, output_len)?; - self.hkdf_expand(hash, secret, &info, out, output_len) - } -} - -/// Build the HkdfLabel structure. -/// -/// ```text -/// struct { -/// uint16 length; -/// opaque label<6..255> = prefix + Label; -/// opaque context<0..255> = Context; -/// } HkdfLabel; -/// ``` -fn build_hkdf_label( - prefix: &[u8], - label: &[u8], - context: &[u8], - output_len: usize, -) -> Result, String> { - let full_label_len = prefix.len() + label.len(); - - if full_label_len > 255 { - return Err("Label too long for HKDF-Expand-Label".into()); - } - if context.len() > 255 { - return Err("Context too long for HKDF-Expand-Label".into()); - } - if output_len > 65535 { - return Err("Output length too large for HKDF-Expand-Label".into()); - } - - let info_len = 2 + 1 + full_label_len + 1 + context.len(); - let mut info = Vec::with_capacity(info_len); - - // uint16 length - info.extend_from_slice(&(output_len as u16).to_be_bytes()); - // opaque label - info.push(full_label_len as u8); - info.extend_from_slice(prefix); - info.extend_from_slice(label); - // opaque context - info.push(context.len() as u8); - info.extend_from_slice(context); - - Ok(info) -} - -pub(super) static HKDF_PROVIDER: AppleHkdfProvider = AppleHkdfProvider; diff --git a/crypto/apple-crypto/src/dimpl_provider/hmac.rs b/crypto/apple-crypto/src/dimpl_provider/hmac.rs index 9a096e8b6..31ed37138 100644 --- a/crypto/apple-crypto/src/dimpl_provider/hmac.rs +++ b/crypto/apple-crypto/src/dimpl_provider/hmac.rs @@ -9,112 +9,47 @@ impl HmacProvider for AppleHmacProvider { fn hmac_sha256(&self, key: &[u8], data: &[u8]) -> Result<[u8; 32], String> { apple_cryptokit::hmac_sha256(key, data).map_err(|err| format!("{err:?}")) } -} - -pub(super) static HMAC_PROVIDER: AppleHmacProvider = AppleHmacProvider; - -#[cfg(test)] -mod test { - use super::*; - - // Test vectors from RFC 4231: Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256, - // HMAC-SHA-384, and HMAC-SHA-512 - // https://tools.ietf.org/html/rfc4231 - - fn hex_to_vec(hex: &str) -> Vec { - let hex = hex.replace(" ", "").replace("\n", ""); - let mut v = Vec::new(); - for i in 0..hex.len() / 2 { - let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap(); - v.push(byte); - } - v - } - fn slice_to_hex(data: &[u8]) -> String { - let mut s = String::new(); - for byte in data.iter() { - s.push_str(&format!("{:02x}", byte)); + fn hmac( + &self, + hash: dimpl::HashAlgorithm, + key: &[u8], + data: &[u8], + out: &mut [u8], + ) -> Result { + match hash { + dimpl::HashAlgorithm::SHA256 => { + let result = + apple_cryptokit::hmac_sha256(key, data).map_err(|err| format!("{err:?}"))?; + let hmac_len = result.len(); + out[0..hmac_len].copy_from_slice(&result); + if hmac_len <= out.len() { + out[0..hmac_len].copy_from_slice(&result); + Ok(hmac_len) + } else { + Err(format!( + "Output buffer too small for SHA256. Needed: {hmac_len}, Was: {}", + out.len() + )) + } + } + dimpl::HashAlgorithm::SHA384 => { + let result = + apple_cryptokit::hmac_sha384(key, data).map_err(|err| format!("{err:?}"))?; + let hmac_len = result.len(); + if hmac_len <= out.len() { + out[0..hmac_len].copy_from_slice(&result); + Ok(hmac_len) + } else { + Err(format!( + "Output buffer too small for SHA384. Needed: {hmac_len}, Was: {}", + out.len() + )) + } + } + _ => Err(format!("Unsupported HMAC Hash: {hash:?}")), } - s - } - - // HMAC-SHA-256 Test Vectors from RFC 4231 - - #[test] - fn test_hmac_sha256_test_case_1() { - let key = hex_to_vec("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); - let data = b"Hi There"; - let expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(&key, data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); - } - - #[test] - fn test_hmac_sha256_test_case_2() { - let key = b"Jefe"; - let data = b"what do ya want for nothing?"; - let expected = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(key, data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); - } - - #[test] - fn test_hmac_sha256_test_case_3() { - let key = hex_to_vec("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - let data = hex_to_vec( - "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\ - dddddddddddddddddddddddddddddddddddd", - ); - let expected = "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(&key, &data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); - } - - #[test] - fn test_hmac_sha256_test_case_4() { - let key = hex_to_vec("0102030405060708090a0b0c0d0e0f10111213141516171819"); - let data = hex_to_vec( - "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\ - cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", - ); - let expected = "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(&key, &data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); - } - - #[test] - fn test_hmac_sha256_test_case_6() { - // Test with a key larger than block size (> 64 bytes) - // RFC 4231: key is 0xaa repeated 131 times - let key = vec![0xaa; 131]; - let data = b"Test Using Larger Than Block-Size Key - Hash Key First"; - let expected = "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(&key, data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); - } - - #[test] - fn test_hmac_sha256_test_case_7() { - // Test with a key larger than block size and large data - // RFC 4231: key is 0xaa repeated 131 times - let key = vec![0xaa; 131]; - let data = b"This is a test using a larger than block-size key and a larger \ -than block-size data. The key needs to be hashed before being used by the HMAC algorithm."; - let expected = "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2"; - - let provider = AppleHmacProvider; - let result = provider.hmac_sha256(&key, data).unwrap(); - assert_eq!(slice_to_hex(&result), expected); } } + +pub(super) static HMAC_PROVIDER: AppleHmacProvider = AppleHmacProvider; diff --git a/crypto/apple-crypto/src/dimpl_provider/mod.rs b/crypto/apple-crypto/src/dimpl_provider/mod.rs index e2fdc7991..0d9fccdd7 100644 --- a/crypto/apple-crypto/src/dimpl_provider/mod.rs +++ b/crypto/apple-crypto/src/dimpl_provider/mod.rs @@ -7,11 +7,9 @@ mod cipher_suite; mod hash; -mod hkdf; mod hmac; mod kx_group; mod sign; -mod tls12; use dimpl::crypto::{CryptoProvider, SecureRandom}; @@ -25,9 +23,7 @@ pub fn default_provider() -> CryptoProvider { key_provider: &sign::KEY_PROVIDER, secure_random: &SECURE_RANDOM, hash_provider: &hash::HASH_PROVIDER, - prf_provider: &tls12::PRF_PROVIDER, hmac_provider: &hmac::HMAC_PROVIDER, - hkdf_provider: &hkdf::HKDF_PROVIDER, } } @@ -44,3 +40,13 @@ impl SecureRandom for AppleSecureRandom { } static SECURE_RANDOM: AppleSecureRandom = AppleSecureRandom; + +#[cfg(test)] +mod tests { + #[test] + fn validate_dimpl_provider() -> Result<(), String> { + super::default_provider() + .validate() + .map_err(|err| format!("{err:?}")) + } +} diff --git a/crypto/apple-crypto/src/dimpl_provider/tls12.rs b/crypto/apple-crypto/src/dimpl_provider/tls12.rs deleted file mode 100644 index dfdefd603..000000000 --- a/crypto/apple-crypto/src/dimpl_provider/tls12.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! TLS 1.2 PRF implementation using Apple CommonCrypto. - -use dimpl::crypto::Buf; -use dimpl::crypto::{HashAlgorithm, PrfProvider}; - -#[derive(Debug)] -pub(super) struct ApplePrfProvider; - -impl PrfProvider for ApplePrfProvider { - fn prf_tls12( - &self, - secret: &[u8], - label: &str, - seed: &[u8], - out: &mut Buf, - output_len: usize, - scratch: &mut Buf, - hash: HashAlgorithm, - ) -> Result<(), String> { - // Sized to the largest hash size we support. - let mut hmac_a = [0; apple_cryptokit::authentication::HMAC_SHA384_OUTPUT_SIZE]; - let hash_len = match hash { - HashAlgorithm::SHA256 => apple_cryptokit::authentication::HMAC_SHA256_OUTPUT_SIZE, - HashAlgorithm::SHA384 => apple_cryptokit::authentication::HMAC_SHA384_OUTPUT_SIZE, - _ => return Err(format!("Unsupported hash algorithm for PRF: {hash:?}")), - }; - - // Build label + seed (this is our "seed" in P_hash terminology) - scratch.clear(); - scratch.extend_from_slice(label.as_bytes()); - scratch.extend_from_slice(seed); - let label_seed = scratch.as_ref(); - - // Compute A(1) = HMAC(secret, label + seed) - match hash { - HashAlgorithm::SHA256 => apple_cryptokit::authentication::hmac_sha256_to( - secret, - label_seed, - hmac_a.as_mut_slice(), - ), - HashAlgorithm::SHA384 => apple_cryptokit::authentication::hmac_sha384_to( - secret, - label_seed, - hmac_a.as_mut_slice(), - ), - _ => return Err(format!("Unsupported hash algorithm for PRF: {hash:?}")), - } - .map_err(|err| format!("{err:?}"))?; - - // Build A(i) + label + seed - scratch.clear(); - scratch.extend_from_slice(&hmac_a[..hash_len]); - scratch.extend_from_slice(label.as_bytes()); - scratch.extend_from_slice(seed); - let payload = scratch.as_mut(); - - out.clear(); - while out.len() < output_len { - // Compute HMAC(secret, A(i) + label + seed) - let mut hmac_block = [0; apple_cryptokit::authentication::HMAC_SHA384_OUTPUT_SIZE]; - let hmac_block_length = match hash { - HashAlgorithm::SHA256 => apple_cryptokit::authentication::hmac_sha256_to( - secret, - payload, - hmac_block.as_mut_slice(), - ), - HashAlgorithm::SHA384 => apple_cryptokit::authentication::hmac_sha384_to( - secret, - payload, - hmac_block.as_mut_slice(), - ), - _ => return Err(format!("Unsupported hash algorithm for PRF: {hash:?}")), - } - .map_err(|err| format!("{err:?}"))?; - - let remaining = output_len - out.len(); - let to_copy = std::cmp::min(remaining, hmac_block_length); - out.extend_from_slice(&hmac_block[..to_copy]); - - if out.len() < output_len { - // Calculate A(i+1) = HMAC(secret, A(i)) - // We take A(i) from the payload, since we need the src and dst to be different. - match hash { - HashAlgorithm::SHA256 => apple_cryptokit::authentication::hmac_sha256_to( - secret, - &payload[..hash_len], - hmac_a.as_mut_slice(), - ), - HashAlgorithm::SHA384 => apple_cryptokit::authentication::hmac_sha384_to( - secret, - &payload[..hash_len], - hmac_a.as_mut_slice(), - ), - _ => return Err(format!("Unsupported hash algorithm for PRF: {hash:?}")), - } - .map_err(|err| format!("{err:?}"))?; - // Copy it into the payload for the next round. - payload[..hash_len].copy_from_slice(&hmac_a[..hash_len]); - } - } - Ok(()) - } -} - -pub(super) static PRF_PROVIDER: ApplePrfProvider = ApplePrfProvider; - -#[cfg(test)] -mod tests { - use dimpl::crypto::Buf; - use dimpl::crypto::HashAlgorithm; - use dimpl::crypto::PrfProvider; - - /// Convert an ASCII hex array into a byte array at compile time. - macro_rules! hex_as_bytes { - ($input:expr) => {{ - const fn from_hex_char(c: u8) -> u8 { - match c { - b'0'..=b'9' => c - b'0', - b'a'..=b'f' => c - b'a' + 10, - b'A'..=b'F' => c - b'A' + 10, - _ => panic!("Invalid hex character"), - } - } - - const INPUT: &[u8] = $input; - const LEN: usize = INPUT.len(); - const OUTPUT_LEN: usize = LEN / 2; - - const fn convert() -> [u8; OUTPUT_LEN] { - assert!(LEN % 2 == 0, "Hex string length must be even"); - - let mut out = [0u8; OUTPUT_LEN]; - let mut i = 0; - while i < LEN { - out[i / 2] = (from_hex_char(INPUT[i]) << 4) | from_hex_char(INPUT[i + 1]); - i += 2; - } - out - } - - convert() - }}; - } - - #[test] - fn test_prf_tls12_sha256() { - // This test vector was found here: https://github.com/xomexh/TLS-PRF - let mut output = Buf::new(); - let mut scratch = Buf::new(); - let provider = super::ApplePrfProvider {}; - assert!( - provider - .prf_tls12( - &hex_as_bytes!(b"9bbe436ba940f017b17652849a71db35"), - "test label", - &hex_as_bytes!(b"a0ba9f936cda311827a6f796ffd5198c"), - &mut output, - 100, - &mut scratch, - HashAlgorithm::SHA256, - ) - .is_ok() - ); - assert_eq!( - output.as_ref(), - &hex_as_bytes!( - b"e3f229ba727be17b8d122620557cd453c2aab21d\ - 07c3d495329b52d4e61edb5a6b301791e90d35c9\ - c9a46b4e14baf9af0fa022f7077def17abfd3797\ - c0564bab4fbc91666e9def9b97fce34f796789ba\ - a48082d122ee42c5a72e5a5110fff70187347b66" - ) - ); - } -} diff --git a/crypto/apple-crypto/src/dtls.rs b/crypto/apple-crypto/src/dtls.rs index 004d52b70..e30493e2a 100644 --- a/crypto/apple-crypto/src/dtls.rs +++ b/crypto/apple-crypto/src/dtls.rs @@ -1,11 +1,10 @@ //! DTLS implementation using dimpl with Apple CommonCrypto backend. -use std::sync::Arc; -use std::time::Instant; - +use dimpl::{Config, Dtls, DtlsCertificate}; use security_framework::access_control::SecAccessControl; use security_framework::key::{GenerateKeyOptions, KeyType, SecKey}; - +use std::sync::Arc; +use std::time::Instant; use str0m_proto::crypto::CryptoError; use str0m_proto::crypto::DtlsVersion; use str0m_proto::crypto::dtls::{DtlsCert, DtlsImplError, DtlsInstance, DtlsOutput, DtlsProvider}; @@ -41,275 +40,42 @@ fn generate_certificate_impl() -> Result { .external_representation() .ok_or_else(|| CryptoError::Other("Failed to export public key".into()))?; - let private_key_bytes = private_key_data.bytes().to_vec(); - let public_key_bytes = public_key_data.bytes().to_vec(); - - // Create a simple self-signed certificate - pass the SecKey directly for signing - let certificate = build_self_signed_cert(&public_key_bytes, &private_key)?; - - // Apple exports private key as: 04 || X || Y || D (97 bytes for P-256) - // We need to wrap this into proper PKCS#8 with SEC1 ECPrivateKey that includes public key - let private_key_der = wrap_ec_private_key_pkcs8(&private_key_bytes, &public_key_bytes)?; - - Ok(DtlsCert { - certificate, - private_key: private_key_der, - }) -} - -/// Build a minimal self-signed X.509 v3 certificate -fn build_self_signed_cert( - public_key_bytes: &[u8], - private_key: &SecKey, -) -> Result, CryptoError> { - // Build TBSCertificate - let tbs = build_tbs_certificate(public_key_bytes)?; - - // Sign the TBS certificate using the private key directly - let signature = sign_with_ecdsa_sha256(&tbs, private_key)?; - - // Encode the full certificate - let ecdsa_with_sha256_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; // 1.2.840.10045.4.3.2 - let sig_algorithm = encode_algorithm_identifier(ecdsa_with_sha256_oid); - - // Signature as BIT STRING (prepend 0x00 for no unused bits) - let signature_bits = encode_bit_string(&signature); - - // Full certificate SEQUENCE - let mut cert_content = Vec::new(); - cert_content.extend_from_slice(&tbs); - cert_content.extend_from_slice(&sig_algorithm); - cert_content.extend_from_slice(&signature_bits); - - Ok(encode_sequence(&cert_content)) -} - -fn build_tbs_certificate(public_key_bytes: &[u8]) -> Result, CryptoError> { - let mut tbs = Vec::new(); - - // Version: v3 (encoded as [0] EXPLICIT INTEGER 2) - let version = encode_explicit_tag(0, &encode_integer(&[2])); - tbs.extend_from_slice(&version); - - // Serial number (random) let mut serial = [0u8; 16]; - use security_framework::random::SecRandom; - SecRandom::default() + security_framework::random::SecRandom::default() .copy_bytes(&mut serial) .map_err(|_| CryptoError::Other("Failed to generate random serial".into()))?; serial[0] &= 0x7F; // Ensure positive - tbs.extend_from_slice(&encode_integer(&serial)); - - // Signature algorithm: ecdsa-with-SHA256 - let ecdsa_with_sha256_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; - tbs.extend_from_slice(&encode_algorithm_identifier(ecdsa_with_sha256_oid)); - - // Issuer: CN=WebRTC - let issuer = encode_name("WebRTC"); - tbs.extend_from_slice(&issuer); - - // Validity: 1 year from now - let validity = encode_validity()?; - tbs.extend_from_slice(&validity); - - // Subject: CN=WebRTC (same as issuer for self-signed) - tbs.extend_from_slice(&issuer); - - // Subject Public Key Info - let spki = encode_ec_public_key_info(public_key_bytes)?; - tbs.extend_from_slice(&spki); - - Ok(encode_sequence(&tbs)) -} - -fn encode_sequence(content: &[u8]) -> Vec { - encode_tag(0x30, content) -} - -fn encode_tag(tag: u8, content: &[u8]) -> Vec { - let mut result = vec![tag]; - encode_length(content.len(), &mut result); - result.extend_from_slice(content); - result -} - -fn encode_length(len: usize, out: &mut Vec) { - if len < 128 { - // len fits in a single byte - } else if len < 256 { - out.push(0x81); - } else { - out.push(0x82); - out.push((len >> 8) as u8); - } - out.push(len as u8); -} - -fn encode_integer(value: &[u8]) -> Vec { - // Skip leading zeros but keep at least one byte - let mut start = 0; - while start < value.len() - 1 && value[start] == 0 { - start += 1; - } - - let value = &value[start..]; - - // If high bit is set, prepend 0x00 - if value[0] & 0x80 != 0 { - let mut content = vec![0x00]; - content.extend_from_slice(value); - encode_tag(0x02, &content) - } else { - encode_tag(0x02, value) - } -} -fn encode_explicit_tag(tag_num: u8, content: &[u8]) -> Vec { - encode_tag(0xA0 | tag_num, content) -} - -fn encode_oid(oid_bytes: &[u8]) -> Vec { - encode_tag(0x06, oid_bytes) -} - -fn encode_algorithm_identifier(oid_bytes: &[u8]) -> Vec { - let oid = encode_oid(oid_bytes); - encode_sequence(&oid) -} - -fn encode_name(cn: &str) -> Vec { - // CN OID: 2.5.4.3 - let cn_oid = &[0x55, 0x04, 0x03]; - let oid = encode_oid(cn_oid); - let value = encode_tag(0x0C, cn.as_bytes()); // UTF8String - let attr_type_value = encode_sequence(&[oid, value].concat()); - let rdn = encode_tag(0x31, &attr_type_value); // SET - encode_sequence(&rdn) -} - -fn encode_validity() -> Result, CryptoError> { - // Use GeneralizedTime for dates - // Not before: now - // Not after: 1 year from now - - // For simplicity, use fixed dates that are valid - // Format: YYYYMMDDHHMMSSZ - let not_before = b"20240101000000Z"; - let not_after = b"20251231235959Z"; - - let nb = encode_tag(0x18, not_before); // GeneralizedTime - let na = encode_tag(0x18, not_after); - - Ok(encode_sequence(&[nb, na].concat())) -} + let certificate = crate::cert::build_self_signed_certificate( + "WebRTC", + serial, + public_key_data.bytes(), + |tbs_certificate| { + // Sign using ECDSA with SHA-256 directly with the generated key + private_key + .create_signature( + security_framework::key::Algorithm::ECDSASignatureMessageX962SHA256, + tbs_certificate, + ) + .map_err(|e| CryptoError::Other(format!("Failed to sign: {e}"))) + }, + )?; -fn encode_ec_public_key_info(public_key_bytes: &[u8]) -> Result, CryptoError> { - // AlgorithmIdentifier for EC public key - // OID: 1.2.840.10045.2.1 (ecPublicKey) - let ec_public_key_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; - // OID: 1.2.840.10045.3.1.7 (prime256v1/secp256r1) - let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; - - let algorithm = - encode_sequence(&[encode_oid(ec_public_key_oid), encode_oid(prime256v1_oid)].concat()); - - // Public key as BIT STRING - let public_key_bits = encode_bit_string(public_key_bytes); - - Ok(encode_sequence(&[algorithm, public_key_bits].concat())) -} - -fn encode_bit_string(data: &[u8]) -> Vec { - let mut content = vec![0x00]; // No unused bits - content.extend_from_slice(data); - encode_tag(0x03, &content) -} - -fn sign_with_ecdsa_sha256(data: &[u8], private_key: &SecKey) -> Result, CryptoError> { - // Sign using ECDSA with SHA-256 directly with the generated key - let signature = private_key - .create_signature( - security_framework::key::Algorithm::ECDSASignatureMessageX962SHA256, - data, - ) - .map_err(|e| CryptoError::Other(format!("Failed to sign: {e}")))?; - - Ok(signature) -} - -fn wrap_ec_private_key_pkcs8( - apple_private_key: &[u8], - public_key_bytes: &[u8], -) -> Result, CryptoError> { // Apple exports private key as: 04 || X (32 bytes) || Y (32 bytes) || D (32 bytes) = 97 bytes for P-256 // Apple exports public key as: 04 || X (32 bytes) || Y (32 bytes) = 65 bytes for P-256 // We need just the D (private scalar) for SEC1 format + let private_scalar = &private_key_data.bytes()[65..].try_into().map_err(|err| { + CryptoError::Other(format!("Unexpected Apple private key contents: {err:?}")) + })?; + let private_key_der = crate::cert::build_pkcs8(private_scalar, public_key_data.bytes())?; - // PKCS#8 PrivateKeyInfo structure: - // PrivateKeyInfo ::= SEQUENCE { - // version Version, - // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, - // privateKey PrivateKey, - // attributes [0] IMPLICIT Attributes OPTIONAL - // } - - if apple_private_key.len() != 97 { - return Err(CryptoError::Other(format!( - "Unexpected Apple private key length: {} (expected 97)", - apple_private_key.len() - ))); - } - - // Extract D (private scalar) from Apple format - last 32 bytes - let private_scalar = &apple_private_key[65..97]; - - // Version: 0 - let version = encode_integer(&[0]); - - // Algorithm: ecPublicKey with prime256v1 - let ec_public_key_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; - let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; - let algorithm = - encode_sequence(&[encode_oid(ec_public_key_oid), encode_oid(prime256v1_oid)].concat()); - - // PrivateKey: SEC1 ECPrivateKey wrapped in OCTET STRING - let ec_private_key = encode_ec_private_key(private_scalar, public_key_bytes)?; - let private_key = encode_tag(0x04, &ec_private_key); // OCTET STRING - - Ok(encode_sequence(&[version, algorithm, private_key].concat())) -} - -fn encode_ec_private_key( - private_scalar: &[u8], - public_key_bytes: &[u8], -) -> Result, CryptoError> { - // ECPrivateKey ::= SEQUENCE { - // version INTEGER { ecPrivkeyVer1(1) }, - // privateKey OCTET STRING, - // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, - // publicKey [1] BIT STRING OPTIONAL - // } - - let version = encode_integer(&[1]); - let private_key = encode_tag(0x04, private_scalar); // OCTET STRING - - // Parameters: prime256v1 OID - let prime256v1_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; - let params = encode_explicit_tag(0, &encode_oid(prime256v1_oid)); - - // Public key as [1] BIT STRING - let public_key_bits = encode_bit_string(public_key_bytes); - let public_key = encode_explicit_tag(1, &public_key_bits); - - Ok(encode_sequence( - &[version, private_key, params, public_key].concat(), - )) + Ok(DtlsCert { + certificate, + private_key: private_key_der, + }) } // DTLS Provider Implementation - -use dimpl::{Config, Dtls, DtlsCertificate}; - #[derive(Debug)] pub(crate) struct AppleCryptoDtlsProvider; @@ -394,3 +160,12 @@ impl DtlsInstance for AppleCryptoDtlsInstance { self.dtls.is_active() } } + +#[cfg(test)] +mod tests { + #[test] + fn generate_self_signed_certificate() { + let cert = super::generate_certificate_impl().unwrap(); + assert_eq!(150, cert.private_key.len()); + } +} diff --git a/crypto/apple-crypto/src/lib.rs b/crypto/apple-crypto/src/lib.rs index e2e960f07..ab4e26a47 100644 --- a/crypto/apple-crypto/src/lib.rs +++ b/crypto/apple-crypto/src/lib.rs @@ -5,6 +5,7 @@ #![allow(unsafe_code)] #![cfg(target_vendor = "apple")] +mod cert; mod common_crypto; mod dimpl_provider; mod dtls; diff --git a/crypto/aws-lc-rs/Cargo.lock b/crypto/aws-lc-rs/Cargo.lock index 35db8e01f..8dafa0b05 100644 --- a/crypto/aws-lc-rs/Cargo.lock +++ b/crypto/aws-lc-rs/Cargo.lock @@ -177,8 +177,7 @@ dependencies = [ [[package]] name = "dimpl" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +source = "git+https://github.com/algesten/dimpl.git#52e8331274e5bc7399246363080b27bc8d2f0fc1" dependencies = [ "arrayvec", "aws-lc-rs", diff --git a/crypto/aws-lc-rs/Cargo.toml b/crypto/aws-lc-rs/Cargo.toml index 0d1fd460b..54d1f760c 100644 --- a/crypto/aws-lc-rs/Cargo.toml +++ b/crypto/aws-lc-rs/Cargo.toml @@ -14,5 +14,6 @@ rust-version = "1.85.0" [dependencies] str0m-proto = { version = "0.2.0", path = "../../proto" } aws-lc-rs = { version = "1", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"] } -dimpl = { version = "0.4.3", default-features = false, features = ["aws-lc-rs", "rcgen"] } +#dimpl = { version = "0.4.3", default-features = false, features = ["aws-lc-rs", "rcgen"] } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false, features = ["aws-lc-rs", "rcgen"]} time = "0.3" diff --git a/crypto/openssl/Cargo.lock b/crypto/openssl/Cargo.lock index e8c0ec88d..6b0152a1f 100644 --- a/crypto/openssl/Cargo.lock +++ b/crypto/openssl/Cargo.lock @@ -48,8 +48,7 @@ dependencies = [ [[package]] name = "dimpl" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +source = "git+https://github.com/algesten/dimpl.git#52e8331274e5bc7399246363080b27bc8d2f0fc1" dependencies = [ "arrayvec", "log", @@ -296,6 +295,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" name = "str0m-openssl" version = "0.2.0" dependencies = [ + "arrayvec", + "dimpl", "libc", "openssl", "openssl-sys", diff --git a/crypto/openssl/Cargo.toml b/crypto/openssl/Cargo.toml index 2c7f1cbd3..0b18f7233 100644 --- a/crypto/openssl/Cargo.toml +++ b/crypto/openssl/Cargo.toml @@ -13,6 +13,7 @@ rust-version = "1.85.0" [features] vendored = ["openssl/vendored"] +prefer_dimpl = ["dep:dimpl"] [dependencies] tracing = "0.1.37" @@ -20,3 +21,8 @@ str0m-proto = { version = "0.2.0", path = "../../proto", features = ["openssl"] openssl = { version = "0.10.70" } openssl-sys = { version = "0.9.80" } libc = { version = "0.2" } +#dimpl = { version = "0.4.3", default-features = false, optional = true } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false, optional = true} + +[dev-dependencies] +arrayvec = "0.7" \ No newline at end of file diff --git a/crypto/openssl/src/cert.rs b/crypto/openssl/src/cert.rs new file mode 100644 index 000000000..a411bcbd8 --- /dev/null +++ b/crypto/openssl/src/cert.rs @@ -0,0 +1,73 @@ +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::ec::{EcGroup, EcKey}; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, KeyUsage}; +use openssl::x509::{X509Builder, X509NameBuilder}; + +use str0m_proto::crypto::CryptoError; +use str0m_proto::crypto::dtls::DtlsCert; + +pub(crate) fn generate_certificate_impl() -> Result { + // Generate EC key pair using P-256 curve + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; + let ec_key = EcKey::generate(&group)?; + let pkey = PKey::from_ec_key(ec_key)?; + + // Build the X509 certificate + let mut builder = X509Builder::new()?; + builder.set_version(2)?; // X509 v3 + + // Generate random serial number + let mut serial = BigNum::new()?; + serial.rand(128, openssl::bn::MsbOption::MAYBE_ZERO, false)?; + builder.set_serial_number(serial.to_asn1_integer()?.as_ref())?; + + // Set validity period (1 year) + let not_before = Asn1Time::days_from_now(0)?; + let not_after = Asn1Time::days_from_now(365)?; + builder.set_not_before(¬_before)?; + builder.set_not_after(¬_after)?; + + // Set subject name + let mut name_builder = X509NameBuilder::new()?; + name_builder.append_entry_by_text("CN", "WebRTC")?; + let name = name_builder.build(); + builder.set_subject_name(&name)?; + builder.set_issuer_name(&name)?; + + builder.set_pubkey(&pkey)?; + + // Add extensions + let basic_constraints = BasicConstraints::new().critical().ca().build()?; + builder.append_extension(basic_constraints)?; + + let key_usage = KeyUsage::new() + .critical() + .digital_signature() + .key_encipherment() + .build()?; + builder.append_extension(key_usage)?; + + let ext_key_usage = ExtendedKeyUsage::new() + .server_auth() + .client_auth() + .build()?; + builder.append_extension(ext_key_usage)?; + + // Sign the certificate + builder.sign(&pkey, MessageDigest::sha256())?; + + let cert = builder.build(); + + // Convert to DER format + let certificate = cert.to_der()?; + let private_key = pkey.private_key_to_der()?; + + Ok(DtlsCert { + certificate, + private_key, + }) +} diff --git a/crypto/openssl/src/dimpl_provider/cipher_suite.rs b/crypto/openssl/src/dimpl_provider/cipher_suite.rs new file mode 100644 index 000000000..cf1514daa --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/cipher_suite.rs @@ -0,0 +1,1146 @@ +//! Cipher suite implementations using OpenSSL. + +use dimpl::crypto::SupportedDtls12CipherSuite; +use dimpl::crypto::SupportedDtls13CipherSuite; +use dimpl::crypto::{Aad, Cipher, Dtls12CipherSuite, HashAlgorithm, Nonce}; +use dimpl::crypto::{Buf, Dtls13CipherSuite, TmpBuf}; + +use openssl::cipher::CipherRef; +use openssl::cipher_ctx::CipherCtx; + +const AES_GCM_TAG_LEN: usize = 16; + +// ============================================================================ +// Shared AEAD helpers +// ============================================================================ + +/// Encrypt plaintext in-place using an AEAD cipher, appending the authentication tag. +fn aead_encrypt( + cipher: &CipherRef, + key: &[u8], + plaintext: &mut Buf, + aad: Aad, + nonce: Nonce, + tag_len: usize, +) -> Result<(), String> { + debug_assert!(tag_len <= 16); + + let mut ctx = CipherCtx::new().map_err(|e| format!("{e}"))?; + + ctx.encrypt_init(Some(cipher), Some(key), Some(&nonce)) + .map_err(|e| format!("{e}"))?; + + ctx.cipher_update(&aad, None).map_err(|e| format!("{e}"))?; + + // OpenSSL may write up to block_size extra bytes during cipher_update/cipher_final. + let mut ciphertext = vec![0u8; plaintext.len() + tag_len + 16]; + let count = ctx + .cipher_update(plaintext, Some(&mut ciphertext)) + .map_err(|e| format!("{e}"))?; + let final_count = ctx + .cipher_final(&mut ciphertext[count..]) + .map_err(|e| format!("{e}"))?; + + let ct_len = count + final_count; + + let mut tag = [0u8; 16]; + ctx.tag(&mut tag[..tag_len]).map_err(|e| format!("{e}"))?; + + plaintext.clear(); + plaintext.extend_from_slice(&ciphertext[..ct_len]); + plaintext.extend_from_slice(&tag[..tag_len]); + Ok(()) +} + +/// Decrypt ciphertext in-place using an AEAD cipher, verifying the authentication tag. +fn aead_decrypt( + cipher: &CipherRef, + key: &[u8], + ciphertext: &mut TmpBuf, + aad: Aad, + nonce: Nonce, + tag_len: usize, +) -> Result<(), String> { + if ciphertext.len() < tag_len { + return Err("Ciphertext too short for authentication tag".into()); + } + + let ct_len = ciphertext.len() - tag_len; + let (ct, tag) = ciphertext.as_ref().split_at(ct_len); + + let mut ctx = CipherCtx::new().map_err(|e| format!("{e}"))?; + + ctx.decrypt_init(Some(cipher), Some(key), Some(&nonce)) + .map_err(|e| format!("{e}"))?; + + ctx.cipher_update(&aad, None).map_err(|e| format!("{e}"))?; + + // OpenSSL may write up to block_size extra bytes during cipher_update/cipher_final. + let mut plaintext = vec![0u8; ct_len + 16]; + let count = ctx + .cipher_update(ct, Some(&mut plaintext)) + .map_err(|e| format!("{e}"))?; + + ctx.set_tag(tag).map_err(|e| format!("{e}"))?; + + let final_count = ctx + .cipher_final(&mut plaintext[count..]) + .map_err(|e| format!("{e}"))?; + + let pt_len = count + final_count; + ciphertext.truncate(pt_len); + ciphertext.as_mut().copy_from_slice(&plaintext[..pt_len]); + Ok(()) +} + +// ============================================================================ +// AES-GCM +// ============================================================================ + +/// AES-GCM cipher implementation using OpenSSL. +/// +/// Uses a fixed-size `[u8; 32]` buffer (the max AES key size) with a length +/// field so the key lives on the stack and can be reliably zeroed on drop. +struct AesGcm { + key: [u8; 32], + key_len: usize, +} + +impl std::fmt::Debug for AesGcm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AesGcm").finish_non_exhaustive() + } +} + +impl AesGcm { + fn new(key: &[u8]) -> Result { + if key.len() != 16 && key.len() != 32 { + return Err(format!("Invalid key size for AES-GCM: {}", key.len())); + } + let mut buf = [0u8; 32]; + buf[..key.len()].copy_from_slice(key); + Ok(Self { + key: buf, + key_len: key.len(), + }) + } + + fn key_bytes(&self) -> &[u8] { + &self.key[..self.key_len] + } + + fn cipher(&self) -> &'static CipherRef { + if self.key_len == 16 { + openssl::cipher::Cipher::aes_128_gcm() + } else { + openssl::cipher::Cipher::aes_256_gcm() + } + } +} + +impl Drop for AesGcm { + fn drop(&mut self) { + for b in self.key.iter_mut() { + // SAFETY: Volatile write prevents the compiler from eliding this zeroing. + unsafe { std::ptr::write_volatile(b, 0) }; + } + } +} + +impl Cipher for AesGcm { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + aead_encrypt( + self.cipher(), + self.key_bytes(), + plaintext, + aad, + nonce, + AES_GCM_TAG_LEN, + ) + } + + fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + aead_decrypt( + self.cipher(), + self.key_bytes(), + ciphertext, + aad, + nonce, + AES_GCM_TAG_LEN, + ) + } +} + +/// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite. +#[derive(Debug)] +struct Aes128GcmSha256; + +impl SupportedDtls12CipherSuite for Aes128GcmSha256 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 16, 4) + } + + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + AES_GCM_TAG_LEN + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } +} + +/// TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 cipher suite. +#[derive(Debug)] +struct Aes256GcmSha384; + +impl SupportedDtls12CipherSuite for Aes256GcmSha384 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA384 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 32, 4) + } + + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + AES_GCM_TAG_LEN + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } +} + +/// Static instances of supported DTLS 1.2 cipher suites. +static AES_128_GCM_SHA256: Aes128GcmSha256 = Aes128GcmSha256; +static AES_256_GCM_SHA384: Aes256GcmSha384 = Aes256GcmSha384; + +/// All supported DTLS 1.2 cipher suites. +pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = + &[&AES_128_GCM_SHA256, &AES_256_GCM_SHA384]; + +/// TLS_AES_128_GCM_SHA256 cipher suite (TLS 1.3 / DTLS 1.3). +#[derive(Debug)] +struct Tls13Aes128GcmSha256; + +impl SupportedDtls13CipherSuite for Tls13Aes128GcmSha256 { + fn suite(&self) -> Dtls13CipherSuite { + Dtls13CipherSuite::AES_128_GCM_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_len(&self) -> usize { + 16 // AES-128 + } + + fn iv_len(&self) -> usize { + 12 // GCM IV + } + + fn tag_len(&self) -> usize { + 16 // GCM tag + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } + + fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16] { + aes_ecb_encrypt(sn_key, sample) + } +} + +/// TLS_AES_256_GCM_SHA384 cipher suite (TLS 1.3 / DTLS 1.3). +#[derive(Debug)] +struct Tls13Aes256GcmSha384; + +impl SupportedDtls13CipherSuite for Tls13Aes256GcmSha384 { + fn suite(&self) -> Dtls13CipherSuite { + Dtls13CipherSuite::AES_256_GCM_SHA384 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA384 + } + + fn key_len(&self) -> usize { + 32 // AES-256 + } + + fn iv_len(&self) -> usize { + 12 // GCM IV + } + + fn tag_len(&self) -> usize { + 16 // GCM tag + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } + + fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16] { + aes_ecb_encrypt(sn_key, sample) + } +} + +/// Static instances of supported DTLS 1.3 cipher suites. +static TLS13_AES_128_GCM_SHA256: Tls13Aes128GcmSha256 = Tls13Aes128GcmSha256; +static TLS13_AES_256_GCM_SHA384: Tls13Aes256GcmSha384 = Tls13Aes256GcmSha384; + +/// All supported DTLS 1.3 cipher suites. +pub(super) static ALL_DTLS13_CIPHER_SUITES: &[&dyn SupportedDtls13CipherSuite] = &[ + &TLS13_AES_128_GCM_SHA256, + &TLS13_AES_256_GCM_SHA384, + &TLS13_CHACHA20_POLY1305_SHA256, +]; + +// ============================================================================ +// ChaCha20-Poly1305 AEAD (DTLS 1.3) +// ============================================================================ + +const CHACHA20_POLY1305_TAG_LEN: usize = 16; +const CHACHA20_POLY1305_KEY_LEN: usize = 32; +const CHACHA20_POLY1305_IV_LEN: usize = 12; + +/// ChaCha20-Poly1305 cipher implementation using OpenSSL. +struct ChaCha20Poly1305 { + key: [u8; CHACHA20_POLY1305_KEY_LEN], +} + +impl std::fmt::Debug for ChaCha20Poly1305 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ChaCha20Poly1305").finish_non_exhaustive() + } +} + +impl ChaCha20Poly1305 { + fn new(key: &[u8]) -> Result { + let key: [u8; CHACHA20_POLY1305_KEY_LEN] = key + .try_into() + .map_err(|_| format!("Invalid key size for ChaCha20-Poly1305: {}", key.len()))?; + Ok(Self { key }) + } +} + +impl Drop for ChaCha20Poly1305 { + fn drop(&mut self) { + for b in self.key.iter_mut() { + // SAFETY: Volatile write prevents the compiler from eliding this zeroing. + unsafe { std::ptr::write_volatile(b, 0) }; + } + } +} + +impl Cipher for ChaCha20Poly1305 { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + aead_encrypt( + openssl::cipher::Cipher::chacha20_poly1305(), + &self.key, + plaintext, + aad, + nonce, + CHACHA20_POLY1305_TAG_LEN, + ) + } + + fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + aead_decrypt( + openssl::cipher::Cipher::chacha20_poly1305(), + &self.key, + ciphertext, + aad, + nonce, + CHACHA20_POLY1305_TAG_LEN, + ) + } +} + +/// TLS_CHACHA20_POLY1305_SHA256 cipher suite (TLS 1.3 / DTLS 1.3). +#[derive(Debug)] +struct Tls13ChaCha20Poly1305Sha256; + +impl SupportedDtls13CipherSuite for Tls13ChaCha20Poly1305Sha256 { + fn suite(&self) -> Dtls13CipherSuite { + Dtls13CipherSuite::CHACHA20_POLY1305_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_len(&self) -> usize { + CHACHA20_POLY1305_KEY_LEN + } + + fn iv_len(&self) -> usize { + CHACHA20_POLY1305_IV_LEN + } + + fn tag_len(&self) -> usize { + CHACHA20_POLY1305_TAG_LEN + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(ChaCha20Poly1305::new(key)?)) + } + + fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16] { + if sn_key.len() != 32 { + panic!( + "encrypt_sn: invalid ChaCha20 key length {} (expected 32)", + sn_key.len() + ); + } + // RFC 9147 Section 4.2.3 / RFC 9001 Section 5.4.4: For ChaCha20-Poly1305, + // the mask is generated by treating the sample as a nonce for ChaCha20 + // with a zero block counter and encrypting zero bytes. + let cipher = openssl::cipher::Cipher::chacha20(); + let mut ctx = CipherCtx::new().expect("CipherCtx::new"); + ctx.encrypt_init(Some(cipher), Some(sn_key), Some(sample)) + .expect("encrypt_init"); + + let mut output = [0u8; 32]; + let input = [0u8; 16]; + let count = ctx + .cipher_update(&input, Some(&mut output)) + .expect("cipher_update"); + let final_count = ctx + .cipher_final(&mut output[count..]) + .expect("cipher_final"); + debug_assert_eq!( + count + final_count, + 16, + "ChaCha20 stream cipher should not pad" + ); + + let mut result = [0u8; 16]; + result.copy_from_slice(&output[..16]); + result + } +} + +static TLS13_CHACHA20_POLY1305_SHA256: Tls13ChaCha20Poly1305Sha256 = Tls13ChaCha20Poly1305Sha256; + +/// AES-ECB single block encryption for record number protection. +fn aes_ecb_encrypt(key: &[u8], input: &[u8; 16]) -> [u8; 16] { + let cipher = match key.len() { + 16 => openssl::cipher::Cipher::aes_128_ecb(), + 32 => openssl::cipher::Cipher::aes_256_ecb(), + n => panic!("aes_ecb_encrypt: invalid AES key length {n} (expected 16 or 32)"), + }; + + let mut ctx = CipherCtx::new().expect("CipherCtx::new"); + ctx.encrypt_init(Some(cipher), Some(key), None) + .expect("encrypt_init"); + ctx.set_padding(false); + + let mut output = [0u8; 32]; // Extra space for block cipher + let count = ctx + .cipher_update(input, Some(&mut output)) + .expect("cipher_update"); + let final_count = ctx + .cipher_final(&mut output[count..]) + .expect("cipher_final"); + debug_assert_eq!( + count + final_count, + 16, + "AES-ECB should produce exactly one block" + ); + + let mut result = [0u8; 16]; + result.copy_from_slice(&output[..16]); + result +} + +#[cfg(test)] +mod tests { + use super::*; + use dimpl::crypto::Cipher; + + #[test] + fn aes128_gcm_encrypt_decrypt_roundtrip() { + let key = [0x42u8; 16]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"hello world, this is a test message for AES-GCM"; + + let mut cipher = AesGcm::new(&key).unwrap(); + + // Encrypt + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + // Ciphertext should be plaintext_len + 16 (tag) + assert_eq!(buf.len(), plaintext.len() + AES_GCM_TAG_LEN); + // Should differ from plaintext + assert_ne!(&buf.as_ref()[..plaintext.len()], &plaintext[..]); + + // Decrypt + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), plaintext); + } + + #[test] + fn aes256_gcm_encrypt_decrypt_roundtrip() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x02u8; 12]); + let plaintext = b"AES-256-GCM test"; + + let mut cipher = AesGcm::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), plaintext); + } + + #[test] + fn aes_gcm_wrong_key_fails_decrypt() { + let key1 = [0x42u8; 16]; + let key2 = [0x43u8; 16]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"secret"; + + let mut cipher1 = AesGcm::new(&key1).unwrap(); + let mut cipher2 = AesGcm::new(&key2).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher1 + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher2 + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + #[test] + fn aes_gcm_invalid_key_size_rejected() { + assert!(AesGcm::new(&[0u8; 15]).is_err()); + assert!(AesGcm::new(&[0u8; 24]).is_err()); + assert!(AesGcm::new(&[0u8; 16]).is_ok()); + assert!(AesGcm::new(&[0u8; 32]).is_ok()); + } + + #[test] + fn aes_ecb_encrypt_deterministic() { + let key = [0u8; 16]; + let input = [0u8; 16]; + let result = aes_ecb_encrypt(&key, &input); + assert_eq!(result.len(), 16); + // Different input produces different output + let input2 = [0x01u8; 16]; + let result2 = aes_ecb_encrypt(&key, &input2); + assert_ne!(result, result2); + } + + #[test] + fn chacha20_poly1305_encrypt_decrypt_roundtrip() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"hello world, this is a test for ChaCha20-Poly1305"; + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + // Encrypt + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + // Ciphertext should be plaintext_len + 16 (tag) + assert_eq!(buf.len(), plaintext.len() + CHACHA20_POLY1305_TAG_LEN); + assert_ne!(&buf.as_ref()[..plaintext.len()], &plaintext[..]); + + // Decrypt + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), plaintext); + } + + #[test] + fn chacha20_poly1305_wrong_key_fails_decrypt() { + let key1 = [0x42u8; 32]; + let key2 = [0x43u8; 32]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"secret"; + + let mut cipher1 = ChaCha20Poly1305::new(&key1).unwrap(); + let mut cipher2 = ChaCha20Poly1305::new(&key2).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher1 + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher2 + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + #[test] + fn chacha20_poly1305_invalid_key_size_rejected() { + assert!(ChaCha20Poly1305::new(&[0u8; 16]).is_err()); + assert!(ChaCha20Poly1305::new(&[0u8; 31]).is_err()); + assert!(ChaCha20Poly1305::new(&[0u8; 32]).is_ok()); + } + + use crate::dimpl_provider::test_utils::hex_to_vec as hex; + + /// RFC 8439 Section 2.8.2 — AEAD construction test vector. + #[test] + fn chacha20_poly1305_rfc8439_aead_test_vector() { + let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"); + let nonce_bytes = hex("070000004041424344454647"); + let aad_bytes = hex("50515253c0c1c2c3c4c5c6c7"); + let plaintext = b"Ladies and Gentlemen of the class of '99: \ +If I could offer you only one tip for the future, sunscreen would be it."; + + let expected_ciphertext = hex("d31a8d34648e60db7b86afbc53ef7ec2\ + a4aded51296e08fea9e2b5a736ee62d6\ + 3dbea45e8ca9671282fafb69da92728b\ + 1a71de0a9e060b2905d6a5b67ecd3b36\ + 92ddbd7f2d778b8c9803aee328091b58\ + fab324e4fad675945585808b4831d7bc\ + 3ff4def08e4b7a9de576d26586cec64b\ + 6116"); + let expected_tag = hex("1ae10b594f09e26a7e902ecbd0600691"); + + let nonce = Nonce(nonce_bytes.as_slice().try_into().unwrap()); + let mut aad_arr = arrayvec::ArrayVec::::new(); + aad_arr.try_extend_from_slice(&aad_bytes).unwrap(); + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + // Encrypt + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher.encrypt(&mut buf, Aad(aad_arr), nonce).unwrap(); + + // Verify ciphertext (excluding tag) + let ct_len = buf.len() - CHACHA20_POLY1305_TAG_LEN; + assert_eq!(&buf.as_ref()[..ct_len], &expected_ciphertext[..]); + // Verify tag + assert_eq!(&buf.as_ref()[ct_len..], &expected_tag[..]); + + // Verify decrypt roundtrip + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + let mut aad_arr2 = arrayvec::ArrayVec::::new(); + aad_arr2.try_extend_from_slice(&aad_bytes).unwrap(); + cipher.decrypt(&mut tmp, Aad(aad_arr2), nonce).unwrap(); + assert_eq!(tmp.as_ref(), plaintext); + } + + /// Verify that modifying the AAD causes decryption to fail. + #[test] + fn chacha20_poly1305_aad_tamper_detected() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"authenticated data test"; + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + + cipher + .encrypt(&mut buf, Aad([0x00u8; 13].into()), nonce) + .unwrap(); + + // Tamper with AAD + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0x01u8; 13].into()), nonce) + .is_err() + ); + } + + /// Verify that a wrong nonce causes decryption to fail. + #[test] + fn chacha20_poly1305_wrong_nonce_fails() { + let key = [0x42u8; 32]; + let nonce1 = Nonce([0x01u8; 12]); + let nonce2 = Nonce([0x02u8; 12]); + let plaintext = b"nonce test"; + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce1) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce2) + .is_err() + ); + } + + /// Corrupted ciphertext tag byte should cause decryption failure. + #[test] + fn chacha20_poly1305_tag_corruption_detected() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"tag corruption test"; + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + // Flip a bit in the last byte (inside the tag) + let mut backing = buf.as_ref().to_vec(); + let last = backing.len() - 1; + backing[last] ^= 0x01; + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + /// Corrupted ciphertext body byte should cause decryption failure. + #[test] + fn chacha20_poly1305_ciphertext_corruption_detected() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"ciphertext corruption test"; + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + // Flip a bit in the first ciphertext byte + let mut backing = buf.as_ref().to_vec(); + backing[0] ^= 0x01; + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + /// Empty plaintext should produce tag-only output. + #[test] + fn chacha20_poly1305_empty_plaintext() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut buf = Buf::new(); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + // Should be exactly the tag length + assert_eq!(buf.len(), CHACHA20_POLY1305_TAG_LEN); + + // Decrypt back to empty + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.len(), 0); + } + + /// Truncated ciphertext (shorter than tag) should fail. + #[test] + fn chacha20_poly1305_truncated_ciphertext_rejected() { + let key = [0x42u8; 32]; + let nonce = Nonce([0x01u8; 12]); + + let mut cipher = ChaCha20Poly1305::new(&key).unwrap(); + + let mut backing = vec![0u8; 8]; // less than 16 byte tag + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + /// Same test for AES-GCM: AAD tamper detected. + #[test] + fn aes_gcm_aad_tamper_detected() { + let key = [0x42u8; 16]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"authenticated data test"; + + let mut cipher = AesGcm::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0x00u8; 13].into()), nonce) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0x01u8; 13].into()), nonce) + .is_err() + ); + } + + /// Same test for AES-GCM: wrong nonce fails. + #[test] + fn aes_gcm_wrong_nonce_fails() { + let key = [0x42u8; 16]; + let nonce1 = Nonce([0x01u8; 12]); + let nonce2 = Nonce([0x02u8; 12]); + let plaintext = b"nonce test"; + + let mut cipher = AesGcm::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce1) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce2) + .is_err() + ); + } + + /// AES-GCM tag corruption should fail decrypt. + #[test] + fn aes_gcm_tag_corruption_detected() { + let key = [0x42u8; 16]; + let nonce = Nonce([0x01u8; 12]); + let plaintext = b"tag test"; + + let mut cipher = AesGcm::new(&key).unwrap(); + + let mut buf = Buf::new(); + buf.extend_from_slice(plaintext); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + + let mut backing = buf.as_ref().to_vec(); + let last = backing.len() - 1; + backing[last] ^= 0x01; + let mut tmp = TmpBuf::new(&mut backing); + assert!( + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .is_err() + ); + } + + /// AES-GCM empty plaintext roundtrip. + #[test] + fn aes_gcm_empty_plaintext() { + let key = [0x42u8; 16]; + let nonce = Nonce([0x01u8; 12]); + + let mut cipher = AesGcm::new(&key).unwrap(); + + let mut buf = Buf::new(); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(buf.len(), AES_GCM_TAG_LEN); + + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.len(), 0); + } + + /// AES-ECB known-answer test (NIST FIPS 197 Appendix B). + #[test] + fn aes_ecb_nist_fips197_appendix_b() { + let key = hex("2b7e151628aed2a6abf7158809cf4f3c"); + let input: [u8; 16] = hex("3243f6a8885a308d313198a2e0370734").try_into().unwrap(); + let expected = hex("3925841d02dc09fbdc118597196a0b32"); + + let result = aes_ecb_encrypt(&key, &input); + assert_eq!(&result[..], &expected[..]); + } + + /// SN encryption for DTLS 1.3 cipher suites should be deterministic. + #[test] + fn tls13_cipher_suites_encrypt_sn_deterministic() { + let sn_key = [0x42u8; 16]; + let sample: [u8; 16] = [0x01u8; 16]; + + let result_128 = TLS13_AES_128_GCM_SHA256.encrypt_sn(&sn_key, &sample); + let result_128b = TLS13_AES_128_GCM_SHA256.encrypt_sn(&sn_key, &sample); + assert_eq!(result_128, result_128b); + + let sn_key_32 = [0x42u8; 32]; + let result_chacha = TLS13_CHACHA20_POLY1305_SHA256.encrypt_sn(&sn_key_32, &sample); + let result_chacha_b = TLS13_CHACHA20_POLY1305_SHA256.encrypt_sn(&sn_key_32, &sample); + assert_eq!(result_chacha, result_chacha_b); + } + + /// RFC 9001 Appendix A.5 ChaCha20 header protection test vector. + #[test] + fn tls13_chacha20_encrypt_sn_rfc9001_vector() { + let sn_key: [u8; 32] = [ + 0x25, 0xa2, 0x82, 0xb9, 0xe8, 0x2f, 0x06, 0xf2, 0x1f, 0x48, 0x89, 0x17, 0xa4, 0xfc, + 0x8f, 0x1b, 0x73, 0x57, 0x36, 0x85, 0x60, 0x85, 0x97, 0xd0, 0xef, 0xcb, 0x07, 0x6b, + 0x0a, 0xb7, 0xa7, 0xa4, + ]; + let sample: [u8; 16] = [ + 0x5e, 0x5c, 0xd5, 0x5c, 0x41, 0xf6, 0x90, 0x80, 0x57, 0x5d, 0x79, 0x99, 0xc2, 0x5a, + 0x5b, 0xfb, + ]; + + let mask = TLS13_CHACHA20_POLY1305_SHA256.encrypt_sn(&sn_key, &sample); + assert_eq!(&mask[..5], &[0xae, 0xfe, 0xfe, 0x7d, 0x03]); + } + + /// Verify DTLS 1.3 suite metadata is consistent. + #[test] + fn tls13_chacha20_suite_metadata() { + assert_eq!( + TLS13_CHACHA20_POLY1305_SHA256.suite(), + Dtls13CipherSuite::CHACHA20_POLY1305_SHA256 + ); + assert_eq!( + TLS13_CHACHA20_POLY1305_SHA256.hash_algorithm(), + HashAlgorithm::SHA256 + ); + assert_eq!(TLS13_CHACHA20_POLY1305_SHA256.key_len(), 32); + assert_eq!(TLS13_CHACHA20_POLY1305_SHA256.iv_len(), 12); + assert_eq!(TLS13_CHACHA20_POLY1305_SHA256.tag_len(), 16); + } + + /// Verify DTLS 1.3 AES suite metadata. + #[test] + fn tls13_aes128_suite_metadata() { + assert_eq!( + TLS13_AES_128_GCM_SHA256.suite(), + Dtls13CipherSuite::AES_128_GCM_SHA256 + ); + assert_eq!( + TLS13_AES_128_GCM_SHA256.hash_algorithm(), + HashAlgorithm::SHA256 + ); + assert_eq!(TLS13_AES_128_GCM_SHA256.key_len(), 16); + assert_eq!(TLS13_AES_128_GCM_SHA256.iv_len(), 12); + assert_eq!(TLS13_AES_128_GCM_SHA256.tag_len(), 16); + } + + #[test] + fn tls13_aes256_suite_metadata() { + assert_eq!( + TLS13_AES_256_GCM_SHA384.suite(), + Dtls13CipherSuite::AES_256_GCM_SHA384 + ); + assert_eq!( + TLS13_AES_256_GCM_SHA384.hash_algorithm(), + HashAlgorithm::SHA384 + ); + assert_eq!(TLS13_AES_256_GCM_SHA384.key_len(), 32); + assert_eq!(TLS13_AES_256_GCM_SHA384.iv_len(), 12); + assert_eq!(TLS13_AES_256_GCM_SHA384.tag_len(), 16); + } + + /// All DTLS 1.3 suites in the static list should have distinct ids. + #[test] + fn all_dtls13_suites_unique() { + let suites: Vec<_> = ALL_DTLS13_CIPHER_SUITES.iter().map(|s| s.suite()).collect(); + for (i, a) in suites.iter().enumerate() { + for b in &suites[i + 1..] { + assert_ne!(a, b, "duplicate DTLS 1.3 cipher suite"); + } + } + } + + /// All DTLS 1.2 suites in the static list should have distinct ids. + #[test] + fn all_dtls12_suites_unique() { + let suites: Vec<_> = ALL_CIPHER_SUITES.iter().map(|s| s.suite()).collect(); + for (i, a) in suites.iter().enumerate() { + for b in &suites[i + 1..] { + assert_ne!(a, b, "duplicate DTLS 1.2 cipher suite"); + } + } + } + + /// Verify DTLS 1.2 AES-128-GCM-SHA256 suite metadata. + #[test] + fn dtls12_aes128_suite_metadata() { + assert_eq!( + AES_128_GCM_SHA256.suite(), + Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + ); + assert_eq!(AES_128_GCM_SHA256.hash_algorithm(), HashAlgorithm::SHA256); + assert_eq!(AES_128_GCM_SHA256.key_lengths(), (0, 16, 4)); + } + + /// Verify DTLS 1.2 AES-256-GCM-SHA384 suite metadata. + #[test] + fn dtls12_aes256_suite_metadata() { + assert_eq!( + AES_256_GCM_SHA384.suite(), + Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 + ); + assert_eq!(AES_256_GCM_SHA384.hash_algorithm(), HashAlgorithm::SHA384); + assert_eq!(AES_256_GCM_SHA384.key_lengths(), (0, 32, 4)); + } + + /// AES-256 encrypt_sn should be deterministic. + #[test] + fn tls13_aes256_encrypt_sn_deterministic() { + let sn_key = [0x42u8; 32]; + let sample: [u8; 16] = [0x01u8; 16]; + let result = TLS13_AES_256_GCM_SHA384.encrypt_sn(&sn_key, &sample); + let result_b = TLS13_AES_256_GCM_SHA384.encrypt_sn(&sn_key, &sample); + assert_eq!(result, result_b); + } + + /// Exercise create_cipher factory for DTLS 1.2 suites. + #[test] + fn dtls12_create_cipher_roundtrip() { + let nonce = Nonce([0x01u8; 12]); + + // AES-128-GCM via factory + let mut cipher = AES_128_GCM_SHA256.create_cipher(&[0x42u8; 16]).unwrap(); + let mut buf = Buf::new(); + buf.extend_from_slice(b"factory test"); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), b"factory test"); + + // AES-256-GCM via factory + let mut cipher = AES_256_GCM_SHA384.create_cipher(&[0x42u8; 32]).unwrap(); + let mut buf = Buf::new(); + buf.extend_from_slice(b"factory test 256"); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), b"factory test 256"); + } + + /// Exercise create_cipher factory for DTLS 1.3 suites. + #[test] + fn dtls13_create_cipher_roundtrip() { + let nonce = Nonce([0x01u8; 12]); + + // AES-128-GCM via factory + let mut cipher = TLS13_AES_128_GCM_SHA256 + .create_cipher(&[0x42u8; 16]) + .unwrap(); + let mut buf = Buf::new(); + buf.extend_from_slice(b"dtls13 factory"); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), b"dtls13 factory"); + + // ChaCha20-Poly1305 via factory + let mut cipher = TLS13_CHACHA20_POLY1305_SHA256 + .create_cipher(&[0x42u8; 32]) + .unwrap(); + let mut buf = Buf::new(); + buf.extend_from_slice(b"dtls13 chacha"); + cipher + .encrypt(&mut buf, Aad([0u8; 13].into()), nonce) + .unwrap(); + let mut backing = buf.as_ref().to_vec(); + let mut tmp = TmpBuf::new(&mut backing); + cipher + .decrypt(&mut tmp, Aad([0u8; 13].into()), nonce) + .unwrap(); + assert_eq!(tmp.as_ref(), b"dtls13 chacha"); + } +} diff --git a/crypto/openssl/src/dimpl_provider/hash.rs b/crypto/openssl/src/dimpl_provider/hash.rs new file mode 100644 index 000000000..ebf57bf8e --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/hash.rs @@ -0,0 +1,131 @@ +//! Hash implementations using OpenSSL. + +use dimpl::crypto::Buf; +use dimpl::crypto::{HashAlgorithm, HashContext, HashProvider}; + +use openssl::hash::{Hasher, MessageDigest}; + +#[derive(Debug)] +pub(super) struct OsslHashProvider; + +impl HashProvider for OsslHashProvider { + fn create_hash(&self, algorithm: HashAlgorithm) -> Box { + match algorithm { + HashAlgorithm::SHA256 => Box::new(OsslHashContext::new(MessageDigest::sha256())), + HashAlgorithm::SHA384 => Box::new(OsslHashContext::new(MessageDigest::sha384())), + _ => panic!("Unsupported hash algorithm: {algorithm:?}"), + } + } +} + +pub(super) static HASH_PROVIDER: OsslHashProvider = OsslHashProvider; + +struct OsslHashContext { + digest: MessageDigest, + /// Accumulated data for `clone_and_finalize` support. + /// + /// OpenSSL's `Hasher` doesn't expose `EVP_MD_CTX_copy_ex` for cloning, + /// so we replay all data into a fresh hasher on each `clone_and_finalize` call. + /// This is O(n) in accumulated data size, but acceptable since DTLS handshake + /// transcripts are bounded (typically a few KB). + data: Vec, +} + +impl std::fmt::Debug for OsslHashContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OsslHashContext").finish_non_exhaustive() + } +} + +impl OsslHashContext { + fn new(digest: MessageDigest) -> Self { + Self { + digest, + data: Vec::new(), + } + } +} + +impl HashContext for OsslHashContext { + fn update(&mut self, data: &[u8]) { + self.data.extend_from_slice(data); + } + + fn clone_and_finalize(&self, out: &mut Buf) { + // Create a new hasher, replay all data, and finalize + let mut hasher = Hasher::new(self.digest).expect("Hasher::new"); + hasher.update(&self.data).expect("hasher update"); + let digest = hasher.finish().expect("hasher finish"); + out.clear(); + out.extend_from_slice(&digest); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dimpl_provider::test_utils::to_hex; + + #[test] + fn sha256_single_update() { + let mut ctx = OsslHashContext::new(MessageDigest::sha256()); + ctx.update(b"abc"); + let mut out = Buf::new(); + ctx.clone_and_finalize(&mut out); + assert_eq!( + to_hex(&out), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + #[test] + fn sha256_multiple_updates() { + let mut ctx = OsslHashContext::new(MessageDigest::sha256()); + ctx.update(b"abcdbcde"); + ctx.update(b"cdefdefg"); + ctx.update(b"efghfghighijhijkijkljklmklmnlmnomnopnopq"); + let mut out = Buf::new(); + ctx.clone_and_finalize(&mut out); + assert_eq!( + to_hex(&out), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" + ); + } + + #[test] + fn sha256_clone_and_finalize_does_not_consume_state() { + let mut ctx = OsslHashContext::new(MessageDigest::sha256()); + ctx.update(b"abc"); + let mut out1 = Buf::new(); + ctx.clone_and_finalize(&mut out1); + + // Continue updating after first finalize + ctx.update(b"def"); + let mut out2 = Buf::new(); + ctx.clone_and_finalize(&mut out2); + + // First snapshot should be SHA-256("abc") + assert_eq!( + to_hex(&out1), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + // Second snapshot should be SHA-256("abcdef") + assert_eq!( + to_hex(&out2), + "bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721" + ); + } + + #[test] + fn sha384_single_update() { + let mut ctx = OsslHashContext::new(MessageDigest::sha384()); + ctx.update(b"abc"); + let mut out = Buf::new(); + ctx.clone_and_finalize(&mut out); + assert_eq!( + to_hex(&out), + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed\ + 8086072ba1e7cc2358baeca134c825a7" + ); + } +} diff --git a/crypto/openssl/src/dimpl_provider/hmac.rs b/crypto/openssl/src/dimpl_provider/hmac.rs new file mode 100644 index 000000000..20b7b972b --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/hmac.rs @@ -0,0 +1,80 @@ +//! HMAC implementations using OpenSSL. + +use dimpl::crypto::HmacProvider; + +use openssl::hash::MessageDigest; + +#[derive(Debug)] +pub(super) struct OsslHmacProvider; + +impl HmacProvider for OsslHmacProvider { + fn hmac( + &self, + hash: dimpl::HashAlgorithm, + key: &[u8], + data: &[u8], + out: &mut [u8], + ) -> Result { + let pkey = openssl::pkey::PKey::hmac(key).map_err(|e| format!("{e}"))?; + let mut signer = match hash { + dimpl::HashAlgorithm::SHA256 => { + openssl::sign::Signer::new(MessageDigest::sha256(), &pkey) + .map_err(|e| format!("{e}")) + } + dimpl::HashAlgorithm::SHA384 => { + openssl::sign::Signer::new(MessageDigest::sha384(), &pkey) + .map_err(|e| format!("{e}")) + } + _ => Err(format!("Unsupported HMAC Hash: {hash:?}")), + }?; + signer.update(data).map_err(|e| format!("{e}"))?; + signer.sign(out).map_err(|e| format!("{e}")) + } +} + +pub(super) static HMAC_PROVIDER: OsslHmacProvider = OsslHmacProvider; + +#[cfg(test)] +mod tests { + use super::*; + use crate::dimpl_provider::test_utils::{hex_to_vec, to_hex}; + + // RFC 4231 Test Case 1 + #[test] + fn hmac_sha256_rfc4231_case1() { + let key = hex_to_vec("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + let data = b"Hi There"; + let result = OsslHmacProvider.hmac_sha256(&key, data).unwrap(); + assert_eq!( + to_hex(&result), + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" + ); + } + + // RFC 4231 Test Case 2 + #[test] + fn hmac_sha256_rfc4231_case2() { + let result = OsslHmacProvider + .hmac_sha256(b"Jefe", b"what do ya want for nothing?") + .unwrap(); + assert_eq!( + to_hex(&result), + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" + ); + } + + // RFC 4231 Test Case 3 + #[test] + fn hmac_sha256_rfc4231_case3() { + let key = hex_to_vec("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let data = hex_to_vec( + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\ + dddddddddddddddddddddddddddddddddddd", + ); + let result = OsslHmacProvider.hmac_sha256(&key, &data).unwrap(); + assert_eq!( + to_hex(&result), + "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" + ); + } +} diff --git a/crypto/openssl/src/dimpl_provider/kx_group.rs b/crypto/openssl/src/dimpl_provider/kx_group.rs new file mode 100644 index 000000000..872493790 --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/kx_group.rs @@ -0,0 +1,439 @@ +//! Key exchange group implementations using OpenSSL. + +use dimpl::crypto::Buf; +use dimpl::crypto::{ActiveKeyExchange, NamedGroup, SupportedKxGroup}; + +use openssl::bn::BigNumContext; +use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm}; +use openssl::nid::Nid; +use openssl::pkey::PKey; + +/// Map a `NamedGroup` to the corresponding OpenSSL `Nid`. +fn nid_for_group(group: NamedGroup) -> Result { + match group { + NamedGroup::Secp256r1 => Ok(Nid::X9_62_PRIME256V1), + NamedGroup::Secp384r1 => Ok(Nid::SECP384R1), + _ => Err(format!("Unsupported group: {group:?}")), + } +} + +/// ECDHE key exchange implementation using OpenSSL. +struct EcdhKeyExchange { + private_key: EcKey, + public_key_bytes: Buf, + group: NamedGroup, +} + +impl std::fmt::Debug for EcdhKeyExchange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.group { + NamedGroup::Secp256r1 => f + .debug_struct("EcdhKeyExchange::P256") + .field("public_key_len", &self.public_key_bytes.len()) + .finish_non_exhaustive(), + NamedGroup::Secp384r1 => f + .debug_struct("EcdhKeyExchange::P384") + .field("public_key_len", &self.public_key_bytes.len()) + .finish_non_exhaustive(), + _ => f + .debug_struct("EcdhKeyExchange::Unknown") + .finish_non_exhaustive(), + } + } +} + +impl EcdhKeyExchange { + fn new(group: NamedGroup, mut buf: Buf) -> Result { + let nid = nid_for_group(group)?; + + let ec_group = EcGroup::from_curve_name(nid).map_err(|e| format!("{e}"))?; + let ec_key = EcKey::generate(&ec_group).map_err(|e| format!("{e}"))?; + + // Export public key as SEC1 uncompressed point format + let mut ctx = BigNumContext::new().map_err(|e| format!("{e}"))?; + let public_key_bytes = ec_key + .public_key() + .to_bytes(&ec_group, PointConversionForm::UNCOMPRESSED, &mut ctx) + .map_err(|e| format!("{e}"))?; + + buf.clear(); + buf.extend_from_slice(&public_key_bytes); + + Ok(Self { + private_key: ec_key, + public_key_bytes: buf, + group, + }) + } +} + +impl ActiveKeyExchange for EcdhKeyExchange { + fn pub_key(&self) -> &[u8] { + &self.public_key_bytes + } + + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + let nid = nid_for_group(self.group)?; + + let ec_group = EcGroup::from_curve_name(nid).map_err(|e| format!("{e}"))?; + let mut ctx = BigNumContext::new().map_err(|e| format!("{e}"))?; + + // Import peer's public key + let peer_point = + EcPoint::from_bytes(&ec_group, peer_pub, &mut ctx).map_err(|e| format!("{e}"))?; + + // Perform ECDH key agreement + let pkey = PKey::from_ec_key(self.private_key).map_err(|e| format!("{e}"))?; + let peer_ec_key = + EcKey::from_public_key(&ec_group, &peer_point).map_err(|e| format!("{e}"))?; + let peer_pkey = PKey::from_ec_key(peer_ec_key).map_err(|e| format!("{e}"))?; + + let mut deriver = openssl::derive::Deriver::new(&pkey).map_err(|e| format!("{e}"))?; + deriver.set_peer(&peer_pkey).map_err(|e| format!("{e}"))?; + + let shared_secret = deriver.derive_to_vec().map_err(|e| format!("{e}"))?; + + out.clear(); + out.extend_from_slice(&shared_secret); + Ok(()) + } + + fn group(&self) -> NamedGroup { + self.group + } +} + +/// P-256 (secp256r1) key exchange group. +#[derive(Debug)] +struct P256; + +impl SupportedKxGroup for P256 { + fn name(&self) -> NamedGroup { + NamedGroup::Secp256r1 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp256r1, buf)?)) + } +} + +/// P-384 (secp384r1) key exchange group. +#[derive(Debug)] +struct P384; + +impl SupportedKxGroup for P384 { + fn name(&self) -> NamedGroup { + NamedGroup::Secp384r1 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp384r1, buf)?)) + } +} + +static KX_GROUP_P256: P256 = P256; +static KX_GROUP_P384: P384 = P384; +static KX_GROUP_X25519: X25519 = X25519; + +pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = + &[&KX_GROUP_P256, &KX_GROUP_P384, &KX_GROUP_X25519]; + +// ============================================================================ +// X25519 Key Exchange +// ============================================================================ + +/// X25519 key exchange group. +#[derive(Debug)] +struct X25519; + +impl SupportedKxGroup for X25519 { + fn name(&self) -> NamedGroup { + NamedGroup::X25519 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(X25519KeyExchange::new(buf)?)) + } +} + +/// X25519 key exchange using OpenSSL's EVP_PKEY_X25519. +struct X25519KeyExchange { + private_key: PKey, + public_key_bytes: Buf, +} + +impl std::fmt::Debug for X25519KeyExchange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("X25519KeyExchange") + .field("public_key_len", &self.public_key_bytes.len()) + .finish_non_exhaustive() + } +} + +impl X25519KeyExchange { + fn new(mut buf: Buf) -> Result { + let private_key = + PKey::generate_x25519().map_err(|e| format!("X25519 key generation failed: {e}"))?; + + let raw_pub = private_key + .raw_public_key() + .map_err(|e| format!("X25519 public key export failed: {e}"))?; + + buf.clear(); + buf.extend_from_slice(&raw_pub); + + Ok(Self { + private_key, + public_key_bytes: buf, + }) + } +} + +impl ActiveKeyExchange for X25519KeyExchange { + fn pub_key(&self) -> &[u8] { + &self.public_key_bytes + } + + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + let peer_key = PKey::public_key_from_raw_bytes(peer_pub, openssl::pkey::Id::X25519) + .map_err(|e| format!("Invalid X25519 peer public key: {e}"))?; + + let mut deriver = + openssl::derive::Deriver::new(&self.private_key).map_err(|e| format!("{e}"))?; + deriver.set_peer(&peer_key).map_err(|e| format!("{e}"))?; + + let shared_secret = deriver.derive_to_vec().map_err(|e| format!("{e}"))?; + + out.clear(); + out.extend_from_slice(&shared_secret); + Ok(()) + } + + fn group(&self) -> NamedGroup { + NamedGroup::X25519 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn x25519_key_exchange_roundtrip() { + let alice = X25519KeyExchange::new(Buf::new()).unwrap(); + let bob = X25519KeyExchange::new(Buf::new()).unwrap(); + + // Public keys should be 32 bytes + assert_eq!(alice.pub_key().len(), 32); + assert_eq!(bob.pub_key().len(), 32); + + let bob_pub = bob.pub_key().to_vec(); + let alice_pub = alice.pub_key().to_vec(); + + let mut alice_secret = Buf::new(); + Box::new(alice) + .complete(&bob_pub, &mut alice_secret) + .unwrap(); + + let mut bob_secret = Buf::new(); + Box::new(bob).complete(&alice_pub, &mut bob_secret).unwrap(); + + // Both sides should derive the same shared secret + assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); + assert_eq!(alice_secret.len(), 32); + } + + #[test] + fn x25519_invalid_peer_key_rejected() { + let alice = X25519KeyExchange::new(Buf::new()).unwrap(); + let mut out = Buf::new(); + // Wrong length peer key + assert!(Box::new(alice).complete(&[0u8; 16], &mut out).is_err()); + } + + /// Each X25519 key generation should produce a unique keypair. + #[test] + fn x25519_keys_are_unique() { + let a = X25519KeyExchange::new(Buf::new()).unwrap(); + let b = X25519KeyExchange::new(Buf::new()).unwrap(); + assert_ne!(a.pub_key(), b.pub_key()); + } + + /// X25519 group metadata is correct. + #[test] + fn x25519_group_metadata() { + let kx = X25519KeyExchange::new(Buf::new()).unwrap(); + assert_eq!(kx.group(), NamedGroup::X25519); + assert_eq!(X25519.name(), NamedGroup::X25519); + } + + /// The same local keypair produces different shared secrets with different peers. + #[test] + fn x25519_different_peers_different_secrets() { + let alice = X25519KeyExchange::new(Buf::new()).unwrap(); + let bob = X25519KeyExchange::new(Buf::new()).unwrap(); + let carol = X25519KeyExchange::new(Buf::new()).unwrap(); + + // Derive from the same local key against two different peers. + let bob_key = + PKey::public_key_from_raw_bytes(bob.pub_key(), openssl::pkey::Id::X25519).unwrap(); + let carol_key = + PKey::public_key_from_raw_bytes(carol.pub_key(), openssl::pkey::Id::X25519).unwrap(); + + let mut deriver_ab = openssl::derive::Deriver::new(&alice.private_key).unwrap(); + deriver_ab.set_peer(&bob_key).unwrap(); + let secret_ab = deriver_ab.derive_to_vec().unwrap(); + + let mut deriver_ac = openssl::derive::Deriver::new(&alice.private_key).unwrap(); + deriver_ac.set_peer(&carol_key).unwrap(); + let secret_ac = deriver_ac.derive_to_vec().unwrap(); + + // Shared secrets with different peers should differ + assert_ne!(secret_ab.as_slice(), secret_ac.as_slice()); + } + + /// X25519 via the SupportedKxGroup trait interface. + #[test] + fn x25519_via_supported_kx_group_trait() { + let group: &dyn SupportedKxGroup = &KX_GROUP_X25519; + assert_eq!(group.name(), NamedGroup::X25519); + + let exchange = group.start_exchange(Buf::new()).unwrap(); + assert_eq!(exchange.pub_key().len(), 32); + assert_eq!(exchange.group(), NamedGroup::X25519); + } + + // ======================================================================== + // ECDH (P-256 / P-384) tests + // ======================================================================== + + #[test] + fn p256_key_exchange_roundtrip() { + let alice = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + let bob = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + + // P-256 uncompressed point: 1 + 32 + 32 = 65 bytes + assert_eq!(alice.pub_key().len(), 65); + assert_eq!(bob.pub_key().len(), 65); + assert_eq!(alice.group(), NamedGroup::Secp256r1); + + let bob_pub = bob.pub_key().to_vec(); + let alice_pub = alice.pub_key().to_vec(); + + let mut alice_secret = Buf::new(); + Box::new(alice) + .complete(&bob_pub, &mut alice_secret) + .unwrap(); + + let mut bob_secret = Buf::new(); + Box::new(bob).complete(&alice_pub, &mut bob_secret).unwrap(); + + assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); + assert_eq!(alice_secret.len(), 32); // P-256 shared secret is 32 bytes + } + + #[test] + fn p384_key_exchange_roundtrip() { + let alice = EcdhKeyExchange::new(NamedGroup::Secp384r1, Buf::new()).unwrap(); + let bob = EcdhKeyExchange::new(NamedGroup::Secp384r1, Buf::new()).unwrap(); + + // P-384 uncompressed point: 1 + 48 + 48 = 97 bytes + assert_eq!(alice.pub_key().len(), 97); + assert_eq!(bob.pub_key().len(), 97); + assert_eq!(alice.group(), NamedGroup::Secp384r1); + + let bob_pub = bob.pub_key().to_vec(); + let alice_pub = alice.pub_key().to_vec(); + + let mut alice_secret = Buf::new(); + Box::new(alice) + .complete(&bob_pub, &mut alice_secret) + .unwrap(); + + let mut bob_secret = Buf::new(); + Box::new(bob).complete(&alice_pub, &mut bob_secret).unwrap(); + + assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); + assert_eq!(alice_secret.len(), 48); // P-384 shared secret is 48 bytes + } + + #[test] + fn p256_keys_are_unique() { + let a = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + let b = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + assert_ne!(a.pub_key(), b.pub_key()); + } + + #[test] + fn p256_invalid_peer_key_rejected() { + let alice = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + let mut out = Buf::new(); + // Garbage peer key + assert!(Box::new(alice).complete(&[0xffu8; 65], &mut out).is_err()); + } + + #[test] + fn p384_invalid_peer_key_rejected() { + let alice = EcdhKeyExchange::new(NamedGroup::Secp384r1, Buf::new()).unwrap(); + let mut out = Buf::new(); + assert!(Box::new(alice).complete(&[0xffu8; 97], &mut out).is_err()); + } + + /// Cross-group exchange should fail (P-256 key to P-384 peer). + #[test] + fn cross_group_exchange_fails() { + let alice = EcdhKeyExchange::new(NamedGroup::Secp256r1, Buf::new()).unwrap(); + let bob = EcdhKeyExchange::new(NamedGroup::Secp384r1, Buf::new()).unwrap(); + + let bob_pub = bob.pub_key().to_vec(); + let mut out = Buf::new(); + // P-384 public key is 97 bytes, not a valid P-256 point + assert!(Box::new(alice).complete(&bob_pub, &mut out).is_err()); + } + + /// Verify all groups in ALL_KX_GROUPS are distinct. + #[test] + fn all_kx_groups_unique() { + let names: Vec<_> = ALL_KX_GROUPS.iter().map(|g| g.name()).collect(); + for (i, a) in names.iter().enumerate() { + for b in &names[i + 1..] { + assert_ne!(a, b, "duplicate key exchange group"); + } + } + } + + /// ALL_KX_GROUPS contains the expected groups. + #[test] + fn all_kx_groups_contains_expected() { + let names: Vec<_> = ALL_KX_GROUPS.iter().map(|g| g.name()).collect(); + assert!(names.contains(&NamedGroup::Secp256r1)); + assert!(names.contains(&NamedGroup::Secp384r1)); + assert!(names.contains(&NamedGroup::X25519)); + } + + /// SupportedKxGroup trait produces working exchanges for all groups. + #[test] + fn all_kx_groups_produce_working_exchanges() { + for group in ALL_KX_GROUPS { + let alice = group.start_exchange(Buf::new()).unwrap(); + let bob = group.start_exchange(Buf::new()).unwrap(); + + let bob_pub = bob.pub_key().to_vec(); + let alice_pub = alice.pub_key().to_vec(); + + let mut alice_secret = Buf::new(); + alice.complete(&bob_pub, &mut alice_secret).unwrap(); + + let mut bob_secret = Buf::new(); + bob.complete(&alice_pub, &mut bob_secret).unwrap(); + + assert_eq!( + alice_secret.as_ref(), + bob_secret.as_ref(), + "shared secret mismatch for {:?}", + group.name() + ); + assert!(!alice_secret.is_empty()); + } + } +} diff --git a/crypto/openssl/src/dimpl_provider/mod.rs b/crypto/openssl/src/dimpl_provider/mod.rs new file mode 100644 index 000000000..2d73e0789 --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/mod.rs @@ -0,0 +1,62 @@ +//! OpenSSL cryptographic provider for dimpl. +//! +//! This module implements the dimpl crypto provider traits using +//! OpenSSL for cryptographic operations. + +mod cipher_suite; +mod hash; +mod hmac; +mod kx_group; +mod sign; + +use dimpl::crypto::{CryptoProvider, SecureRandom}; + +/// Get the OpenSSL based crypto provider for dimpl. +pub(crate) fn default_provider() -> CryptoProvider { + CryptoProvider { + cipher_suites: cipher_suite::ALL_CIPHER_SUITES, + dtls13_cipher_suites: cipher_suite::ALL_DTLS13_CIPHER_SUITES, + kx_groups: kx_group::ALL_KX_GROUPS, + signature_verification: &sign::SIGNATURE_VERIFIER, + key_provider: &sign::KEY_PROVIDER, + secure_random: &SECURE_RANDOM, + hash_provider: &hash::HASH_PROVIDER, + hmac_provider: &hmac::HMAC_PROVIDER, + } +} + +#[derive(Debug)] +struct OsslSecureRandom; + +impl SecureRandom for OsslSecureRandom { + fn fill(&self, buf: &mut [u8]) -> Result<(), String> { + openssl::rand::rand_bytes(buf).map_err(|e| format!("OpenSSL random failed: {e}")) + } +} + +static SECURE_RANDOM: OsslSecureRandom = OsslSecureRandom; + +#[cfg(test)] +pub(super) mod test_utils { + pub fn hex_to_vec(hex: &str) -> Vec { + assert!(hex.len() % 2 == 0, "hex string must have even length"); + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()) + .collect() + } + + pub fn to_hex(data: &[u8]) -> String { + data.iter().map(|b| format!("{b:02x}")).collect() + } +} + +#[cfg(test)] +mod tests { + #[test] + fn validate_dimpl_provider() -> Result<(), String> { + super::default_provider() + .validate() + .map_err(|err| format!("{err:?}")) + } +} diff --git a/crypto/openssl/src/dimpl_provider/sign.rs b/crypto/openssl/src/dimpl_provider/sign.rs new file mode 100644 index 000000000..82a28a199 --- /dev/null +++ b/crypto/openssl/src/dimpl_provider/sign.rs @@ -0,0 +1,167 @@ +//! Signing and key loading implementations using OpenSSL. + +use dimpl::crypto::Buf; +use dimpl::crypto::{HashAlgorithm, KeyProvider}; +use dimpl::crypto::{SignatureAlgorithm, SignatureVerifier, SigningKey as SigningKeyTrait}; + +use openssl::ec::EcKey; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; + +/// ECDSA signing key implementation using OpenSSL. +struct EcdsaSigningKey { + pkey: PKey, + curve: EcCurve, +} + +#[derive(Clone, Copy, Debug)] +enum EcCurve { + P256, + P384, +} + +impl std::fmt::Debug for EcdsaSigningKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.curve { + EcCurve::P256 => f.debug_tuple("EcdsaSigningKey::P256").finish(), + EcCurve::P384 => f.debug_tuple("EcdsaSigningKey::P384").finish(), + } + } +} + +impl SigningKeyTrait for EcdsaSigningKey { + fn sign(&mut self, data: &[u8], out: &mut Buf) -> Result<(), String> { + let md = match self.curve { + EcCurve::P256 => MessageDigest::sha256(), + EcCurve::P384 => MessageDigest::sha384(), + }; + + let mut signer = + openssl::sign::Signer::new(md, &self.pkey).map_err(|e| format!("Signer::new: {e}"))?; + signer.update(data).map_err(|e| format!("update: {e}"))?; + let signature = signer + .sign_to_vec() + .map_err(|e| format!("sign_to_vec: {e}"))?; + + out.clear(); + out.extend_from_slice(&signature); + Ok(()) + } + + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::ECDSA + } + + fn hash_algorithm(&self) -> HashAlgorithm { + match self.curve { + EcCurve::P256 => HashAlgorithm::SHA256, + EcCurve::P384 => HashAlgorithm::SHA384, + } + } +} + +/// Key provider implementation. +#[derive(Debug)] +pub(super) struct OsslKeyProvider; + +impl OsslKeyProvider { + fn pkey_to_signing_key( + &self, + pkey: PKey, + ) -> Result, String> { + let ec_key = pkey.ec_key().map_err(|e| format!("Not an EC key: {e}"))?; + let nid = ec_key + .group() + .curve_name() + .ok_or_else(|| "Unknown EC curve".to_string())?; + + let curve = match nid { + Nid::X9_62_PRIME256V1 => EcCurve::P256, + Nid::SECP384R1 => EcCurve::P384, + _ => return Err(format!("Unsupported EC curve: {:?}", nid)), + }; + + Ok(Box::new(EcdsaSigningKey { pkey, curve })) + } +} + +impl KeyProvider for OsslKeyProvider { + fn load_private_key(&self, key_der: &[u8]) -> Result, String> { + // Try PEM format first using OpenSSL's native parser + if key_der.starts_with(b"-----BEGIN") { + let pkey = PKey::private_key_from_pem(key_der) + .map_err(|e| format!("Failed to parse PEM private key: {e}"))?; + return self.pkey_to_signing_key(pkey); + } + + // Try PKCS#8 format first, then raw EC key + let pkey = PKey::private_key_from_der(key_der) + .or_else(|_| { + // Try as SEC1 EC private key + let ec_key = EcKey::private_key_from_der(key_der) + .map_err(|e| format!("Failed to parse EC key: {e}"))?; + PKey::from_ec_key(ec_key).map_err(|e| format!("PKey from EC: {e}")) + }) + .map_err(|e| format!("Failed to load private key: {e}"))?; + + self.pkey_to_signing_key(pkey) + } +} + +/// Signature verifier implementation. +#[derive(Debug)] +pub(super) struct OsslSignatureVerifier; + +impl SignatureVerifier for OsslSignatureVerifier { + fn verify_signature( + &self, + cert_der: &[u8], + data: &[u8], + signature: &[u8], + hash_alg: HashAlgorithm, + sig_alg: SignatureAlgorithm, + ) -> Result<(), String> { + if sig_alg != SignatureAlgorithm::ECDSA { + return Err(format!("Unsupported signature algorithm: {sig_alg:?}")); + } + + let md = match hash_alg { + HashAlgorithm::SHA256 => MessageDigest::sha256(), + HashAlgorithm::SHA384 => MessageDigest::sha384(), + _ => { + return Err(format!( + "Unsupported hash algorithm for ECDSA: {hash_alg:?}" + )); + } + }; + + // Use OpenSSL's native X.509 parser to extract the public key directly. + // This avoids fragile key-length-based curve detection and manual SPKI parsing. + let cert = openssl::x509::X509::from_der(cert_der) + .map_err(|e| format!("Failed to parse certificate: {e}"))?; + let pkey = cert + .public_key() + .map_err(|e| format!("Failed to extract public key: {e}"))?; + + // Verify the signature + let mut verifier = + openssl::sign::Verifier::new(md, &pkey).map_err(|e| format!("Verifier::new: {e}"))?; + verifier.update(data).map_err(|e| format!("update: {e}"))?; + let valid = verifier + .verify(signature) + .map_err(|e| format!("verify: {e}"))?; + + if valid { + Ok(()) + } else { + Err("ECDSA signature verification failed".to_string()) + } + } +} + +/// Static instance of the key provider. +pub(super) static KEY_PROVIDER: OsslKeyProvider = OsslKeyProvider; + +/// Static instance of the signature verifier. +pub(super) static SIGNATURE_VERIFIER: OsslSignatureVerifier = OsslSignatureVerifier; diff --git a/crypto/openssl/src/dtls_dimpl.rs b/crypto/openssl/src/dtls_dimpl.rs new file mode 100644 index 000000000..36743bb2e --- /dev/null +++ b/crypto/openssl/src/dtls_dimpl.rs @@ -0,0 +1,105 @@ +//! DTLS implementation using OpenSSL via dimpl. + +use std::sync::Arc; +use std::time::Instant; + +use str0m_proto::crypto::dtls::DtlsImplError; +use str0m_proto::crypto::dtls::{DtlsCert, DtlsInstance, DtlsOutput, DtlsProvider}; +use str0m_proto::crypto::{CryptoError, DtlsVersion}; + +// ============================================================================ +// DTLS Provider Implementation +// ============================================================================ + +pub(super) struct OsslDtlsProvider; + +impl std::fmt::Debug for OsslDtlsProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OsslDtlsProvider").finish() + } +} + +impl DtlsProvider for OsslDtlsProvider { + fn generate_certificate(&self) -> Option { + crate::cert::generate_certificate_impl().ok() + } + + fn new_dtls( + &self, + cert: &DtlsCert, + now: Instant, + dtls_version: DtlsVersion, + ) -> Result, CryptoError> { + let dimpl_cert = dimpl::DtlsCertificate { + certificate: cert.certificate.clone(), + private_key: cert.private_key.clone(), + }; + + let mut builder = dimpl::Config::builder(); + if self.is_test() { + // We need the DTLS impl to be deterministic for the BWE tests. + builder = builder.dangerously_set_rng_seed(42); + } + + let config = builder + .with_crypto_provider(crate::dimpl_provider::default_provider()) + .build() + .map_err(|e| CryptoError::Other(format!("dimpl config creation failed: {e}")))?; + + let config = Arc::new(config); + let dtls = match dtls_version { + DtlsVersion::Dtls12 => dimpl::Dtls::new_12(config, dimpl_cert, now), + DtlsVersion::Dtls13 => dimpl::Dtls::new_13(config, dimpl_cert, now), + DtlsVersion::Auto => dimpl::Dtls::new_auto(config, dimpl_cert, now), + _ => { + return Err(CryptoError::Other(format!( + "Unknown DTLS version: {dtls_version}" + ))); + } + }; + + Ok(Box::new(DimplDtlsInstance { dtls })) + } +} + +// ============================================================================ +// Dimpl DTLS Instance Wrapper +// ============================================================================ + +struct DimplDtlsInstance { + dtls: dimpl::Dtls, +} + +impl std::fmt::Debug for DimplDtlsInstance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DimplDtlsInstance") + .field("is_active", &self.dtls.is_active()) + .finish() + } +} + +impl DtlsInstance for DimplDtlsInstance { + fn set_active(&mut self, active: bool) { + self.dtls.set_active(active); + } + + fn handle_packet(&mut self, packet: &[u8]) -> Result<(), DtlsImplError> { + self.dtls.handle_packet(packet) + } + + fn poll_output<'a>(&mut self, buf: &'a mut [u8]) -> DtlsOutput<'a> { + self.dtls.poll_output(buf) + } + + fn handle_timeout(&mut self, now: Instant) -> Result<(), DtlsImplError> { + self.dtls.handle_timeout(now) + } + + fn send_application_data(&mut self, data: &[u8]) -> Result<(), DtlsImplError> { + self.dtls.send_application_data(data) + } + + fn is_active(&self) -> bool { + self.dtls.is_active() + } +} diff --git a/crypto/openssl/src/dtls.rs b/crypto/openssl/src/dtls_ossl.rs similarity index 88% rename from crypto/openssl/src/dtls.rs rename to crypto/openssl/src/dtls_ossl.rs index 3f4779c31..fc0c55ae4 100644 --- a/crypto/openssl/src/dtls.rs +++ b/crypto/openssl/src/dtls_ossl.rs @@ -5,18 +5,12 @@ use std::panic::UnwindSafe; use std::time::{Duration, Instant}; use std::{io, mem}; -use openssl::asn1::Asn1Time; -use openssl::bn::BigNum; -use openssl::ec::{EcGroup, EcKey}; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; use openssl::pkey::PKey; use openssl::srtp::SrtpProfileId; use openssl::ssl::{HandshakeError, MidHandshakeSslStream, Ssl}; use openssl::ssl::{SslContext, SslContextBuilder, SslMethod}; use openssl::ssl::{SslOptions, SslStream, SslVerifyMode}; -use openssl::x509::extension::{BasicConstraints, ExtendedKeyUsage, KeyUsage}; -use openssl::x509::{X509, X509Builder, X509NameBuilder}; +use openssl::x509::X509; use str0m_proto::crypto::dtls::{DtlsCert, KeyingMaterial, SrtpProfile}; use str0m_proto::crypto::dtls::{DtlsImplError, DtlsInstance, DtlsOutput, DtlsProvider}; @@ -636,7 +630,7 @@ impl std::fmt::Debug for OsslDtlsProvider { impl DtlsProvider for OsslDtlsProvider { fn generate_certificate(&self) -> Option { - generate_certificate_impl().ok() + crate::cert::generate_certificate_impl().ok() } fn new_dtls( @@ -645,75 +639,13 @@ impl DtlsProvider for OsslDtlsProvider { _now: Instant, dtls_version: DtlsVersion, ) -> Result, CryptoError> { - if dtls_version != DtlsVersion::Dtls12 { - return Err(CryptoError::Other( - "OpenSSL DTLS provider only supports DTLS 1.2. \ - Use aws-lc-rs or rust-crypto backend for DTLS 1.3/Auto." + match dtls_version { + DtlsVersion::Dtls12 | DtlsVersion::Auto => Ok(Box::new(OsslDtlsInstance::new(cert)?)), + _ => Err(CryptoError::Other( + "OpenSSL DTLS provider only supports DTLS 1.2 without prefer_dimpl. \ + Enable the prefer_dimpl feature for DTLS 1.3." .to_string(), - )); + )), } - Ok(Box::new(OsslDtlsInstance::new(cert)?)) } } - -fn generate_certificate_impl() -> Result { - // Generate EC key pair using P-256 curve - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; - let ec_key = EcKey::generate(&group)?; - let pkey = PKey::from_ec_key(ec_key)?; - - // Build the X509 certificate - let mut builder = X509Builder::new()?; - builder.set_version(2)?; // X509 v3 - - // Generate random serial number - let mut serial = BigNum::new()?; - serial.rand(128, openssl::bn::MsbOption::MAYBE_ZERO, false)?; - builder.set_serial_number(serial.to_asn1_integer()?.as_ref())?; - - // Set validity period (1 year) - let not_before = Asn1Time::days_from_now(0)?; - let not_after = Asn1Time::days_from_now(365)?; - builder.set_not_before(¬_before)?; - builder.set_not_after(¬_after)?; - - // Set subject name - let mut name_builder = X509NameBuilder::new()?; - name_builder.append_entry_by_text("CN", "WebRTC")?; - let name = name_builder.build(); - builder.set_subject_name(&name)?; - builder.set_issuer_name(&name)?; - - builder.set_pubkey(&pkey)?; - - // Add extensions - let basic_constraints = BasicConstraints::new().critical().ca().build()?; - builder.append_extension(basic_constraints)?; - - let key_usage = KeyUsage::new() - .critical() - .digital_signature() - .key_encipherment() - .build()?; - builder.append_extension(key_usage)?; - - let ext_key_usage = ExtendedKeyUsage::new() - .server_auth() - .client_auth() - .build()?; - builder.append_extension(ext_key_usage)?; - - // Sign the certificate - builder.sign(&pkey, MessageDigest::sha256())?; - - let cert = builder.build(); - - // Convert to DER format - let certificate = cert.to_der()?; - let private_key = pkey.private_key_to_der()?; - - Ok(DtlsCert { - certificate, - private_key, - }) -} diff --git a/crypto/openssl/src/lib.rs b/crypto/openssl/src/lib.rs index b4ec4dd00..9de3e24a6 100644 --- a/crypto/openssl/src/lib.rs +++ b/crypto/openssl/src/lib.rs @@ -1,6 +1,11 @@ //! OpenSSL implementation of cryptographic functions. //! DTLS via OpenSSL's native DTLS implementation. +mod cert; +#[cfg(feature = "prefer_dimpl")] +mod dimpl_provider; +#[cfg_attr(feature = "prefer_dimpl", path = "dtls_dimpl.rs")] +#[cfg_attr(not(feature = "prefer_dimpl"), path = "dtls_ossl.rs")] mod dtls; mod sha1; mod sha256; @@ -12,6 +17,7 @@ use sha256::OsslSha256Provider; use srtp::OsslSrtpProvider; use str0m_proto::crypto::CryptoProvider; +#[cfg(not(feature = "prefer_dimpl"))] #[macro_use] extern crate tracing; diff --git a/crypto/rust-crypto/Cargo.lock b/crypto/rust-crypto/Cargo.lock index 0b0292783..429eabb9f 100644 --- a/crypto/rust-crypto/Cargo.lock +++ b/crypto/rust-crypto/Cargo.lock @@ -335,8 +335,7 @@ dependencies = [ [[package]] name = "dimpl" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +source = "git+https://github.com/algesten/dimpl.git#52e8331274e5bc7399246363080b27bc8d2f0fc1" dependencies = [ "aes-gcm", "arrayvec", diff --git a/crypto/rust-crypto/Cargo.toml b/crypto/rust-crypto/Cargo.toml index ce512de97..1e9c92ce9 100644 --- a/crypto/rust-crypto/Cargo.toml +++ b/crypto/rust-crypto/Cargo.toml @@ -13,7 +13,8 @@ rust-version = "1.85.0" [dependencies] str0m-proto = { version = "0.2.0", path = "../../proto" } -dimpl = { version = "0.4.3", default-features = false, features = ["rust-crypto", "rcgen"] } +#dimpl = { version = "0.4.3", default-features = false } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false, features = ["rust-crypto", "rcgen"]} time = "0.3" hmac = { version = "0.12.1" } aes-gcm = { version = "0.10" } diff --git a/crypto/wincrypto/Cargo.lock b/crypto/wincrypto/Cargo.lock index 8579f5eba..5eb72e9ad 100644 --- a/crypto/wincrypto/Cargo.lock +++ b/crypto/wincrypto/Cargo.lock @@ -20,6 +20,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.5.8" @@ -31,9 +66,8 @@ dependencies = [ [[package]] name = "dimpl" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da42827f6904e69bf1cc78ddcb74fab007a2b476099ab0a42e8cacf1ea1f6c0" +version = "0.5.0" +source = "git+https://github.com/algesten/dimpl.git#b01a83f89311e5334edba22a55a64232c83bad1c" dependencies = [ "arrayvec", "log", @@ -44,6 +78,23 @@ dependencies = [ "time", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -153,7 +204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -163,7 +214,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -172,7 +232,32 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", ] [[package]] @@ -209,9 +294,12 @@ dependencies = [ name = "str0m-wincrypto" version = "0.4.0" dependencies = [ + "dimpl", + "rand_core 0.6.4", "str0m-proto", "tracing", "windows", + "x25519-dalek", ] [[package]] @@ -299,6 +387,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -424,6 +518,18 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "zerocopy" version = "0.8.47" @@ -443,3 +549,23 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crypto/wincrypto/Cargo.toml b/crypto/wincrypto/Cargo.toml index b00fbd137..1516c8234 100644 --- a/crypto/wincrypto/Cargo.toml +++ b/crypto/wincrypto/Cargo.toml @@ -11,11 +11,20 @@ edition = "2024" rust-version = "1.85.0" [dependencies] -tracing = "0.1.37" +#dimpl = { version = "0.4.3", default-features = false, optional = true } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false, optional = true} str0m-proto = { version = "0.2.0", path = "../../proto" } +tracing = "0.1.37" windows = { version = "0.62", features = [ "Win32_Security_Cryptography", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_System_Rpc", ] } + +[dev-dependencies] +x25519-dalek = { version = "2", features = ["static_secrets"] } +rand_core = { version = "0.6", features = ["getrandom"] } + +[features] +prefer_dimpl = ["dep:dimpl"] diff --git a/crypto/wincrypto/src/dimpl_provider/cipher_suite.rs b/crypto/wincrypto/src/dimpl_provider/cipher_suite.rs new file mode 100644 index 000000000..4f6ebf664 --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/cipher_suite.rs @@ -0,0 +1,432 @@ +//! Cipher suite implementations using Windows CNG AES-GCM. + +use std::cell::RefCell; +use std::ptr::{addr_of, write_volatile}; + +use dimpl::crypto::SupportedDtls12CipherSuite; +use dimpl::crypto::SupportedDtls13CipherSuite; +use dimpl::crypto::{Aad, Cipher, Dtls12CipherSuite, HashAlgorithm, Nonce}; +use dimpl::crypto::{Buf, Dtls13CipherSuite, TmpBuf}; + +use windows::Win32::Security::Cryptography::BCRYPT_AES_ECB_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_AES_GCM_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO; +use windows::Win32::Security::Cryptography::BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION; +use windows::Win32::Security::Cryptography::BCRYPT_FLAGS; +use windows::Win32::Security::Cryptography::BCRYPT_KEY_HANDLE; +use windows::Win32::Security::Cryptography::BCryptDecrypt; +use windows::Win32::Security::Cryptography::BCryptEncrypt; +use windows::Win32::Security::Cryptography::BCryptGenerateSymmetricKey; +use windows::core::Owned; + +use crate::WinCryptoError; + +const AES_GCM_TAG_LEN: usize = 16; + +/// AES-GCM cipher implementation using Windows CNG. +struct AesGcm { + key: Owned, +} + +// SAFETY: `BCRYPT_KEY_HANDLE` is an opaque CNG handle documented by Microsoft +// Learn for the BCrypt APIs; this wrapper never dereferences it directly and +// only passes it back to those APIs. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +unsafe impl Send for AesGcm {} +unsafe impl Sync for AesGcm {} + +impl std::fmt::Debug for AesGcm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AesGcm").finish_non_exhaustive() + } +} + +impl AesGcm { + fn new(key: &[u8]) -> Result { + if key.len() != 16 && key.len() != 32 { + return Err(format!("Invalid key size for AES-GCM: {}", key.len())); + } + // SAFETY: Microsoft Learn documents `BCryptGenerateSymmetricKey` as + // borrowing the caller-provided key bytes and output handle only for + // the duration of the call; both outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptgeneratesymmetrickey + let key_handle = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptGenerateSymmetricKey( + BCRYPT_AES_GCM_ALG_HANDLE, + &mut *key_handle, + None, + key, + 0, + )) + .map_err(|e| format!("AES-GCM key creation failed: {e}"))?; + key_handle + }; + Ok(Self { key: key_handle }) + } +} + +impl Cipher for AesGcm { + fn encrypt(&mut self, plaintext: &mut Buf, aad: Aad, nonce: Nonce) -> Result<(), String> { + let plain_len = plaintext.len(); + let mut ciphertext = vec![0u8; plain_len]; + let mut tag = [0u8; AES_GCM_TAG_LEN]; + + let auth_info = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO { + dwInfoVersion: BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION, + cbSize: std::mem::size_of::() as u32, + pbNonce: nonce.0.as_ptr() as *mut u8, + cbNonce: nonce.0.len() as u32, + pbAuthData: aad.0.as_ptr() as *mut u8, + cbAuthData: aad.0.len() as u32, + pbTag: tag.as_mut_ptr(), + cbTag: AES_GCM_TAG_LEN as u32, + ..Default::default() + }; + + let mut count = 0u32; + // SAFETY: Microsoft Learn documents `BCryptEncrypt` as borrowing the + // input, output, and authenticated-cipher-mode-info buffers for the + // duration of the call; `auth_info` only points at data that outlives + // this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptencrypt + unsafe { + WinCryptoError::from_ntstatus(BCryptEncrypt( + *self.key, + Some(plaintext.as_ref()), + Some(addr_of!(auth_info) as *const std::ffi::c_void), + None, + Some(&mut ciphertext), + &mut count, + BCRYPT_FLAGS(0), + )) + .map_err(|e| format!("AES-GCM encrypt failed: {e}"))?; + } + + plaintext.clear(); + plaintext.extend_from_slice(&ciphertext[..count as usize]); + plaintext.extend_from_slice(&tag); + Ok(()) + } + + fn decrypt(&mut self, ciphertext: &mut TmpBuf, aad: Aad, nonce: Nonce) -> Result<(), String> { + if ciphertext.len() < AES_GCM_TAG_LEN { + return Err("Ciphertext too short for AES-GCM".into()); + } + + let ct_len = ciphertext.len() - AES_GCM_TAG_LEN; + // Split ciphertext and tag - we need to copy because we can't borrow twice. + let ct_data: Vec = ciphertext.as_ref()[..ct_len].to_vec(); + let mut tag = [0u8; AES_GCM_TAG_LEN]; + tag.copy_from_slice(&ciphertext.as_ref()[ct_len..]); + + let mut plaintext = vec![0u8; ct_len]; + + let auth_info = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO { + dwInfoVersion: BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO_VERSION, + cbSize: std::mem::size_of::() as u32, + pbNonce: nonce.0.as_ptr() as *mut u8, + cbNonce: nonce.0.len() as u32, + pbAuthData: aad.0.as_ptr() as *mut u8, + cbAuthData: aad.0.len() as u32, + pbTag: tag.as_mut_ptr(), + cbTag: AES_GCM_TAG_LEN as u32, + ..Default::default() + }; + + let mut count = 0u32; + // SAFETY: Microsoft Learn documents `BCryptDecrypt` as borrowing the + // input, output, and authenticated-cipher-mode-info buffers for the + // duration of the call; all referenced data outlives this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptdecrypt + unsafe { + WinCryptoError::from_ntstatus(BCryptDecrypt( + *self.key, + Some(&ct_data), + Some(addr_of!(auth_info) as *const std::ffi::c_void), + None, + Some(&mut plaintext), + &mut count, + BCRYPT_FLAGS(0), + )) + .map_err(|e| format!("AES-GCM decrypt failed: {e}"))?; + } + + ciphertext.truncate(count as usize); + ciphertext.as_mut()[..count as usize].copy_from_slice(&plaintext[..count as usize]); + Ok(()) + } +} + +// DTLS 1.2 cipher suites + +/// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +#[derive(Debug)] +struct Aes128GcmSha256; + +impl SupportedDtls12CipherSuite for Aes128GcmSha256 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_AES128_GCM_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 16, 4) + } + + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } +} + +/// TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +#[derive(Debug)] +struct Aes256GcmSha384; + +impl SupportedDtls12CipherSuite for Aes256GcmSha384 { + fn suite(&self) -> Dtls12CipherSuite { + Dtls12CipherSuite::ECDHE_ECDSA_AES256_GCM_SHA384 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA384 + } + + fn key_lengths(&self) -> (usize, usize, usize) { + (0, 32, 4) + } + + fn explicit_nonce_len(&self) -> usize { + 8 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } +} + +static AES_128_GCM_SHA256: Aes128GcmSha256 = Aes128GcmSha256; +static AES_256_GCM_SHA384: Aes256GcmSha384 = Aes256GcmSha384; + +pub(super) static ALL_CIPHER_SUITES: &[&dyn SupportedDtls12CipherSuite] = + &[&AES_128_GCM_SHA256, &AES_256_GCM_SHA384]; + +// DTLS 1.3 cipher suites + +/// TLS_AES_128_GCM_SHA256 (DTLS 1.3) +#[derive(Debug)] +struct Tls13Aes128GcmSha256; + +impl SupportedDtls13CipherSuite for Tls13Aes128GcmSha256 { + fn suite(&self) -> Dtls13CipherSuite { + Dtls13CipherSuite::AES_128_GCM_SHA256 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA256 + } + + fn key_len(&self) -> usize { + 16 + } + + fn iv_len(&self) -> usize { + 12 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } + + fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16] { + aes_ecb_encrypt(sn_key, sample) + } +} + +/// TLS_AES_256_GCM_SHA384 (DTLS 1.3) +#[derive(Debug)] +struct Tls13Aes256GcmSha384; + +impl SupportedDtls13CipherSuite for Tls13Aes256GcmSha384 { + fn suite(&self) -> Dtls13CipherSuite { + Dtls13CipherSuite::AES_256_GCM_SHA384 + } + + fn hash_algorithm(&self) -> HashAlgorithm { + HashAlgorithm::SHA384 + } + + fn key_len(&self) -> usize { + 32 + } + + fn iv_len(&self) -> usize { + 12 + } + + fn tag_len(&self) -> usize { + 16 + } + + fn create_cipher(&self, key: &[u8]) -> Result, String> { + Ok(Box::new(AesGcm::new(key)?)) + } + + fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16] { + aes_ecb_encrypt(sn_key, sample) + } +} + +static TLS13_AES_128_GCM_SHA256: Tls13Aes128GcmSha256 = Tls13Aes128GcmSha256; +static TLS13_AES_256_GCM_SHA384: Tls13Aes256GcmSha384 = Tls13Aes256GcmSha384; + +pub(super) static ALL_DTLS13_CIPHER_SUITES: &[&dyn SupportedDtls13CipherSuite] = + &[&TLS13_AES_128_GCM_SHA256, &TLS13_AES_256_GCM_SHA384]; + +/// Key bytes wrapper that volatile-zeroes its contents on drop. +struct ZeroizingKey(Vec); + +impl Drop for ZeroizingKey { + fn drop(&mut self) { + for byte in self.0.iter_mut() { + // SAFETY: `byte` is a valid, aligned, dereferenceable pointer into + // our own Vec's heap buffer. `write_volatile` prevents the compiler + // from eliding this zero-write. + unsafe { write_volatile(byte, 0) }; + } + } +} + +impl ZeroizingKey { + fn new(key: &[u8]) -> Self { + Self(key.to_vec()) + } +} + +impl PartialEq<[u8]> for ZeroizingKey { + fn eq(&self, other: &[u8]) -> bool { + self.0 == other + } +} + +// DTLS 1.3 record number protection (`encrypt_sn`) requires a single AES-ECB +// block encryption per packet. On other platforms this is cheap: Apple's +// `CCCrypt` and pure-Rust `aes` both accept raw key bytes and do the AES key +// expansion inline (~100 ns). Windows CNG, however, forces a two-step model: +// +// 1. `BCryptGenerateSymmetricKey` — allocate a kernel object, expand the key +// schedule, return a `BCRYPT_KEY_HANDLE`. (~5–20 µs) +// 2. `BCryptEncrypt` — encrypt using that handle. +// +// Creating and destroying a handle per packet is 100–400× slower than the +// inline approach. We want to cache the handle, but the dimpl trait that calls +// us is: +// +// fn encrypt_sn(&self, sn_key: &[u8], sample: &[u8; 16]) -> [u8; 16]; +// +// The implementor (`Tls13Aes128GcmSha256`) is a unit struct stored as a global +// `static`, shared across all connections. `&self` is immutable and the trait +// requires `Send + Sync`, so we cannot stash a `BCRYPT_KEY_HANDLE` inside +// `self` without a `Mutex` — which would serialize all DTLS traffic in the +// process through a single lock. +// +// `thread_local!` sidesteps both problems: +// • No lock — each OS thread has its own small LRU cache (capacity 4). +// • No mutation of `self` — the cache lives outside the trait object. +// • Handles are cleaned up automatically when the thread exits. +// • Key bytes are volatile-zeroed on eviction via `ZeroizingKey`. +// +// The `sn_key` is constant for a given DTLS epoch (usually the entire +// session), so the cache hit rate is effectively 100 % during steady state. +thread_local! { + /// Cache for AES-ECB keys for DTLS 1.3 record number protection. + static AES_ECB_KEY_CACHE: RefCell< + Vec<(ZeroizingKey, Owned)>, + > = RefCell::new(Vec::with_capacity(4)); +} + +fn aes_ecb_encrypt(key: &[u8], input: &[u8; 16]) -> [u8; 16] { + AES_ECB_KEY_CACHE.with(|cache_cell| { + let mut cache = cache_cell.borrow_mut(); + + // 1. Try to find in cache + let cached_handle = if let Some(pos) = cache.iter().position(|(k, _)| k == key) { + // Move to front (LRU) + if pos > 0 { + let item = cache.remove(pos); + cache.insert(0, item); + } + Some(*cache[0].1) + } else { + None + }; + + if let Some(handle) = cached_handle { + return do_aes_ecb_encrypt(handle, input); + } + + // 2. Not found, generate new + // SAFETY: Microsoft Learn documents `BCryptGenerateSymmetricKey`... + let new_handle_owned = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptGenerateSymmetricKey( + BCRYPT_AES_ECB_ALG_HANDLE, + &mut *key_handle, + None, + key, + 0, + )) + .expect("AES-ECB key creation"); + key_handle + }; + + let raw_handle = *new_handle_owned; + + // 3. Store in cache + if cache.len() >= 4 { + cache.pop(); + } + cache.insert(0, (ZeroizingKey::new(key), new_handle_owned)); + + do_aes_ecb_encrypt(raw_handle, input) + }) +} + +fn do_aes_ecb_encrypt(key_handle: BCRYPT_KEY_HANDLE, input: &[u8; 16]) -> [u8; 16] { + let mut output = [0u8; 16]; + unsafe { + let mut count = 0u32; + WinCryptoError::from_ntstatus(BCryptEncrypt( + key_handle, + Some(input), + None, + None, + Some(&mut output), + &mut count, + BCRYPT_FLAGS(0), + )) + .expect("AES-ECB encrypt"); + } + let mut result = [0u8; 16]; + result.copy_from_slice(&output); + result +} diff --git a/crypto/wincrypto/src/dimpl_provider/hash.rs b/crypto/wincrypto/src/dimpl_provider/hash.rs new file mode 100644 index 000000000..48b7745ac --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/hash.rs @@ -0,0 +1,118 @@ +//! Hash implementations using Windows CNG. + +use dimpl::crypto::Buf; +use dimpl::crypto::{HashAlgorithm, HashContext, HashProvider}; + +use windows::Win32::Security::Cryptography::BCRYPT_HASH_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_SHA256_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_SHA384_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCryptCreateHash; +use windows::Win32::Security::Cryptography::BCryptFinishHash; +use windows::Win32::Security::Cryptography::BCryptHashData; +use windows::core::Owned; + +use crate::WinCryptoError; + +#[derive(Debug)] +pub(super) struct WinCngHashProvider; + +impl HashProvider for WinCngHashProvider { + fn create_hash(&self, algorithm: HashAlgorithm) -> Box { + match algorithm { + HashAlgorithm::SHA256 => Box::new(WinCngHashContext::new(HashKind::Sha256)), + HashAlgorithm::SHA384 => Box::new(WinCngHashContext::new(HashKind::Sha384)), + _ => panic!("Unsupported hash algorithm: {algorithm:?}"), + } + } +} + +pub(super) static HASH_PROVIDER: WinCngHashProvider = WinCngHashProvider; + +#[derive(Clone, Copy)] +enum HashKind { + Sha256, + Sha384, +} + +/// Incremental hash context using Windows CNG. +/// +/// CNG hash handles are NOT clonable, so to implement `clone_and_finalize` +/// we buffer all input and re-hash from scratch each time. +struct WinCngHashContext { + kind: HashKind, + data: Vec, +} + +// SAFETY: This type stores only owned Rust data; it does not retain any live +// CNG handle between calls, so `Send`/`Sync` rely only on ordinary Rust +// ownership invariants rather than undocumented FFI aliasing. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash +unsafe impl Send for WinCngHashContext {} +unsafe impl Sync for WinCngHashContext {} + +impl WinCngHashContext { + fn new(kind: HashKind) -> Self { + Self { + kind, + data: Vec::new(), + } + } + + fn hash_len(&self) -> usize { + match self.kind { + HashKind::Sha256 => 32, + HashKind::Sha384 => 48, + } + } + + fn finalize_snapshot(&self) -> Vec { + let mut hash = vec![0u8; self.hash_len()]; + let alg_handle = match self.kind { + HashKind::Sha256 => BCRYPT_SHA256_ALG_HANDLE, + HashKind::Sha384 => BCRYPT_SHA384_ALG_HANDLE, + }; + // SAFETY: Microsoft Learn documents `BCryptCreateHash`, + // `BCryptHashData`, and `BCryptFinishHash` as borrowing the handle and + // buffer arguments only for the duration of each call; all of them + // outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcrypthashdata + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinishhash + unsafe { + let mut hash_handle = Owned::new(BCRYPT_HASH_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptCreateHash( + alg_handle, + &mut *hash_handle, + None, + None, + 0, + )) + .expect("hash creation"); + + WinCryptoError::from_ntstatus(BCryptHashData(*hash_handle, &self.data, 0)) + .expect("hash data"); + + WinCryptoError::from_ntstatus(BCryptFinishHash(*hash_handle, &mut hash, 0)) + .expect("hash finish"); + } + hash + } +} + +impl std::fmt::Debug for WinCngHashContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WinCngHashContext").finish_non_exhaustive() + } +} + +impl HashContext for WinCngHashContext { + fn update(&mut self, data: &[u8]) { + self.data.extend_from_slice(data); + } + + fn clone_and_finalize(&self, out: &mut Buf) { + let digest = self.finalize_snapshot(); + out.clear(); + out.extend_from_slice(&digest); + } +} diff --git a/crypto/wincrypto/src/dimpl_provider/hmac.rs b/crypto/wincrypto/src/dimpl_provider/hmac.rs new file mode 100644 index 000000000..e56447043 --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/hmac.rs @@ -0,0 +1,109 @@ +//! HMAC implementations using Windows CNG. + +use dimpl::crypto::HmacProvider; + +use windows::Win32::Security::Cryptography::BCRYPT_HASH_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_HMAC_SHA256_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_HMAC_SHA384_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCryptCreateHash; +use windows::Win32::Security::Cryptography::BCryptFinishHash; +use windows::Win32::Security::Cryptography::BCryptHashData; +use windows::core::Owned; + +use crate::WinCryptoError; + +#[derive(Debug)] +pub(super) struct WinCngHmacProvider; + +impl HmacProvider for WinCngHmacProvider { + fn hmac( + &self, + hash: dimpl::HashAlgorithm, + key: &[u8], + data: &[u8], + out: &mut [u8], + ) -> Result { + match hash { + dimpl::HashAlgorithm::SHA256 => { + let result = win_hmac_sha256(key, data).map_err(|err| format!("{err:?}"))?; + let hmac_len = result.len(); + out[0..hmac_len].copy_from_slice(&result); + if hmac_len <= out.len() { + out[0..hmac_len].copy_from_slice(&result); + Ok(hmac_len) + } else { + Err(format!( + "Output buffer too small for SHA256. Needed: {hmac_len}, Was: {}", + out.len() + )) + } + } + dimpl::HashAlgorithm::SHA384 => { + let result = win_hmac_sha384(key, data).map_err(|err| format!("{err:?}"))?; + let hmac_len = result.len(); + if hmac_len <= out.len() { + out[0..hmac_len].copy_from_slice(&result); + Ok(hmac_len) + } else { + Err(format!( + "Output buffer too small for SHA384. Needed: {hmac_len}, Was: {}", + out.len() + )) + } + } + _ => Err(format!("Unsupported HMAC Hash: {hash:?}")), + } + } +} + +pub(super) static HMAC_PROVIDER: WinCngHmacProvider = WinCngHmacProvider; + +pub(super) fn win_hmac_sha256(key: &[u8], data: &[u8]) -> Result<[u8; 32], WinCryptoError> { + let mut hash = [0u8; 32]; + // SAFETY: Microsoft Learn documents `BCryptCreateHash`, + // `BCryptHashData`, and `BCryptFinishHash` as borrowing the handle, key, + // input, and output buffers only for the duration of each call; all of + // them outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcrypthashdata + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinishhash + unsafe { + let mut hash_handle = Owned::new(BCRYPT_HASH_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptCreateHash( + BCRYPT_HMAC_SHA256_ALG_HANDLE, + &mut *hash_handle, + None, + Some(key), + 0, + ))?; + + WinCryptoError::from_ntstatus(BCryptHashData(*hash_handle, data, 0))?; + WinCryptoError::from_ntstatus(BCryptFinishHash(*hash_handle, &mut hash, 0))?; + } + Ok(hash) +} + +pub(super) fn win_hmac_sha384(key: &[u8], data: &[u8]) -> Result<[u8; 48], WinCryptoError> { + let mut hash = [0u8; 48]; + // SAFETY: Microsoft Learn documents `BCryptCreateHash`, + // `BCryptHashData`, and `BCryptFinishHash` as borrowing the handle, key, + // input, and output buffers only for the duration of each call; all of + // them outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcrypthashdata + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinishhash + unsafe { + let mut hash_handle = Owned::new(BCRYPT_HASH_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptCreateHash( + BCRYPT_HMAC_SHA384_ALG_HANDLE, + &mut *hash_handle, + None, + Some(key), + 0, + ))?; + + WinCryptoError::from_ntstatus(BCryptHashData(*hash_handle, data, 0))?; + WinCryptoError::from_ntstatus(BCryptFinishHash(*hash_handle, &mut hash, 0))?; + } + Ok(hash) +} diff --git a/crypto/wincrypto/src/dimpl_provider/kx_group.rs b/crypto/wincrypto/src/dimpl_provider/kx_group.rs new file mode 100644 index 000000000..7e6937768 --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/kx_group.rs @@ -0,0 +1,633 @@ +//! Key exchange group implementations using Windows CNG BCrypt ECDH. + +use std::sync::LazyLock; + +use dimpl::crypto::Buf; +use dimpl::crypto::{ActiveKeyExchange, NamedGroup, SupportedKxGroup}; + +use windows::Win32::Security::Cryptography::BCRYPT_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_ECCPUBLIC_BLOB; +use windows::Win32::Security::Cryptography::BCRYPT_ECDH_ALGORITHM; +use windows::Win32::Security::Cryptography::BCRYPT_ECDH_P256_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_ECDH_P384_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_KDF_RAW_SECRET; +use windows::Win32::Security::Cryptography::BCRYPT_KEY_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS; +use windows::Win32::Security::Cryptography::BCRYPT_SECRET_HANDLE; +use windows::Win32::Security::Cryptography::BCryptDeriveKey; +use windows::Win32::Security::Cryptography::BCryptExportKey; +use windows::Win32::Security::Cryptography::BCryptFinalizeKeyPair; +use windows::Win32::Security::Cryptography::BCryptGenerateKeyPair; +use windows::Win32::Security::Cryptography::BCryptImportKeyPair; +use windows::Win32::Security::Cryptography::BCryptOpenAlgorithmProvider; +use windows::Win32::Security::Cryptography::BCryptSecretAgreement; +use windows::Win32::Security::Cryptography::BCryptSetProperty; +use windows::core::Owned; + +use crate::WinCryptoError; + +/// ECDHE key exchange implementation using Windows CNG. +struct EcdhKeyExchange { + key_handle: Owned, + public_key_bytes: Buf, + group: NamedGroup, +} + +// SAFETY: `BCRYPT_KEY_HANDLE` is an opaque CNG handle documented by Microsoft +// Learn for the BCrypt APIs; this wrapper never dereferences it directly and +// only passes it back to those APIs. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +unsafe impl Send for EcdhKeyExchange {} +unsafe impl Sync for EcdhKeyExchange {} + +impl std::fmt::Debug for EcdhKeyExchange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EcdhKeyExchange") + .field("group", &self.group) + .finish_non_exhaustive() + } +} + +impl EcdhKeyExchange { + fn new(group: NamedGroup, mut buf: Buf) -> Result { + let (alg_handle, coord_size) = match group { + NamedGroup::Secp256r1 => (BCRYPT_ECDH_P256_ALG_HANDLE, 32usize), + NamedGroup::Secp384r1 => (BCRYPT_ECDH_P384_ALG_HANDLE, 48usize), + _ => return Err(format!("Unsupported group: {group:?}")), + }; + + // SAFETY: Microsoft Learn documents `BCryptGenerateKeyPair` and + // `BCryptFinalizeKeyPair` as initializing the caller-provided handle + // for the duration of the call; the output handle outlives this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptgeneratekeypair + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinalizekeypair + let key_handle = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptGenerateKeyPair( + alg_handle, + &mut *key_handle, + (coord_size * 8) as u32, + 0, + )) + .map_err(|e| format!("BCryptGenerateKeyPair failed: {e}"))?; + + WinCryptoError::from_ntstatus(BCryptFinalizeKeyPair(*key_handle, 0)) + .map_err(|e| format!("BCryptFinalizeKeyPair failed: {e}"))?; + + key_handle + }; + + // Export public key as SEC1 uncompressed point: 04 || X || Y + let pub_key_bytes = export_ec_public_key(*key_handle, coord_size)?; + + buf.clear(); + buf.extend_from_slice(&pub_key_bytes); + + Ok(Self { + key_handle, + public_key_bytes: buf, + group, + }) + } +} + +impl ActiveKeyExchange for EcdhKeyExchange { + fn pub_key(&self) -> &[u8] { + &self.public_key_bytes + } + + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + let coord_size = match self.group { + NamedGroup::Secp256r1 => 32usize, + NamedGroup::Secp384r1 => 48usize, + _ => return Err("Unsupported group".into()), + }; + + let alg_handle = match self.group { + NamedGroup::Secp256r1 => BCRYPT_ECDH_P256_ALG_HANDLE, + NamedGroup::Secp384r1 => BCRYPT_ECDH_P384_ALG_HANDLE, + _ => return Err("Unsupported group".into()), + }; + + // peer_pub should be uncompressed point: 04 || X || Y + if peer_pub.len() != 1 + 2 * coord_size || peer_pub[0] != 0x04 { + return Err(format!( + "Invalid peer public key length: {} (expected {})", + peer_pub.len(), + 1 + 2 * coord_size, + )); + } + + // Import peer public key as BCRYPT_ECCPUBLIC_BLOB + let peer_key_handle = import_ec_public_key(alg_handle, peer_pub, coord_size)?; + + // Perform ECDH key exchange + // SAFETY: Microsoft Learn documents `BCryptSecretAgreement` as + // borrowing both key handles and the output secret handle for the + // duration of the call; all handles outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptsecretagreement + let shared_secret = unsafe { + let mut secret_handle = Owned::new(BCRYPT_SECRET_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptSecretAgreement( + *self.key_handle, + *peer_key_handle, + &mut *secret_handle, + 0, + )) + .map_err(|e| format!("BCryptSecretAgreement failed: {e}"))?; + + // Derive raw shared secret + derive_raw_secret(*secret_handle)? + }; + + // Windows returns the raw shared secret in big-endian, which is what we want. + // However, BCryptDeriveKey with BCRYPT_KDF_RAW_SECRET returns it in LITTLE-endian. + // We need to reverse it. + let mut secret_be = shared_secret; + secret_be.reverse(); + + out.clear(); + out.extend_from_slice(&secret_be); + + Ok(()) + } + + fn group(&self) -> NamedGroup { + self.group + } +} + +/// Export EC public key as SEC1 uncompressed point format: 04 || X || Y +fn export_ec_public_key( + key_handle: BCRYPT_KEY_HANDLE, + coord_size: usize, +) -> Result, String> { + // SAFETY: Microsoft Learn documents `BCryptExportKey` as borrowing the key + // handle and optional output buffer for the duration of each call; both + // outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptexportkey + unsafe { + // Query size first + let mut blob_size = 0u32; + WinCryptoError::from_ntstatus(BCryptExportKey( + key_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + None, + &mut blob_size, + 0, + )) + .map_err(|e| format!("BCryptExportKey size query failed: {e}"))?; + + let mut blob = vec![0u8; blob_size as usize]; + WinCryptoError::from_ntstatus(BCryptExportKey( + key_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + Some(&mut blob), + &mut blob_size, + 0, + )) + .map_err(|e| format!("BCryptExportKey failed: {e}"))?; + + // BCRYPT_ECCKEY_BLOB header is 8 bytes: dwMagic(4) + cbKey(4) + // followed by X(cbKey) + Y(cbKey) + let header_size = 8; + if blob.len() < header_size + 2 * coord_size { + return Err("Exported key blob too small".into()); + } + + let x = &blob[header_size..header_size + coord_size]; + let y = &blob[header_size + coord_size..header_size + 2 * coord_size]; + + let mut pub_key = Vec::with_capacity(1 + 2 * coord_size); + pub_key.push(0x04); // Uncompressed point + pub_key.extend_from_slice(x); + pub_key.extend_from_slice(y); + + Ok(pub_key) + } +} + +/// Import an EC public key from SEC1 uncompressed point format. +fn import_ec_public_key( + alg_handle: BCRYPT_ALG_HANDLE, + pub_key: &[u8], + coord_size: usize, +) -> Result, String> { + // Build BCRYPT_ECCKEY_BLOB + let magic: u32 = match coord_size { + 32 => 0x314B4345, // BCRYPT_ECDH_PUBLIC_P256_MAGIC + 48 => 0x334B4345, // BCRYPT_ECDH_PUBLIC_P384_MAGIC + _ => return Err("Unsupported coord size".into()), + }; + + let header_size = 8; + let mut blob = Vec::with_capacity(header_size + 2 * coord_size); + blob.extend_from_slice(&magic.to_le_bytes()); + blob.extend_from_slice(&(coord_size as u32).to_le_bytes()); + // pub_key is 04 || X || Y, skip the 04 prefix + blob.extend_from_slice(&pub_key[1..]); + + // SAFETY: Microsoft Learn documents `BCryptImportKeyPair` as borrowing the + // key blob and output handle only for the duration of the call; both + // outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptimportkeypair + unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptImportKeyPair( + alg_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + &mut *key_handle, + &blob, + 0, + )) + .map_err(|e| format!("BCryptImportKeyPair failed: {e}"))?; + + Ok(key_handle) + } +} + +/// Derive the raw shared secret from an ECDH secret agreement. +unsafe fn derive_raw_secret(secret_handle: BCRYPT_SECRET_HANDLE) -> Result, String> { + let mut derived_size = 0u32; + + unsafe { + WinCryptoError::from_ntstatus(BCryptDeriveKey( + secret_handle, + BCRYPT_KDF_RAW_SECRET, + None, + None, + &mut derived_size, + 0, + )) + .map_err(|e| format!("BCryptDeriveKey size query failed: {e}"))?; + } + + let mut derived = vec![0u8; derived_size as usize]; + unsafe { + WinCryptoError::from_ntstatus(BCryptDeriveKey( + secret_handle, + BCRYPT_KDF_RAW_SECRET, + None, + Some(&mut derived), + &mut derived_size, + 0, + )) + .map_err(|e| format!("BCryptDeriveKey failed: {e}"))?; + } + + derived.truncate(derived_size as usize); + Ok(derived) +} + +/// P-256 (secp256r1) key exchange group. +#[derive(Debug)] +struct P256; + +impl SupportedKxGroup for P256 { + fn name(&self) -> NamedGroup { + NamedGroup::Secp256r1 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp256r1, buf)?)) + } +} + +/// P-384 (secp384r1) key exchange group. +#[derive(Debug)] +struct P384; + +impl SupportedKxGroup for P384 { + fn name(&self) -> NamedGroup { + NamedGroup::Secp384r1 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(EcdhKeyExchange::new(NamedGroup::Secp384r1, buf)?)) + } +} + +// ============================================================================= +// X25519 key exchange using Windows CNG (requires Windows 10 1607+) +// ============================================================================= + +/// Wrapper to allow BCRYPT_ALG_HANDLE in a LazyLock (Send + Sync). +/// SAFETY: `BCRYPT_ALG_HANDLE` is an opaque CNG algorithm-provider handle +/// documented by Microsoft Learn for the BCrypt APIs; this wrapper never +/// dereferences it directly and only passes it back to those APIs. +/// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +struct X25519Alg(BCRYPT_ALG_HANDLE); +unsafe impl Send for X25519Alg {} +unsafe impl Sync for X25519Alg {} + +/// Cached algorithm provider for X25519 ECDH. +static X25519_PROVIDER: LazyLock = LazyLock::new(|| { + // SAFETY: Microsoft Learn documents `BCryptOpenAlgorithmProvider` and + // `BCryptSetProperty` as borrowing the output handle and property buffer + // for the duration of each call; both outlive this initialization block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptsetproperty + unsafe { + let mut handle = BCRYPT_ALG_HANDLE::default(); + WinCryptoError::from_ntstatus(BCryptOpenAlgorithmProvider( + &mut handle, + BCRYPT_ECDH_ALGORITHM, + None, + BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS(0), + )) + .expect("BCryptOpenAlgorithmProvider ECDH for X25519"); + + // Set the curve to Curve25519 + let curve_name: Vec = "curve25519\0".encode_utf16().collect(); + let curve_bytes = + std::slice::from_raw_parts(curve_name.as_ptr() as *const u8, curve_name.len() * 2); + WinCryptoError::from_ntstatus(BCryptSetProperty( + BCRYPT_HANDLE(handle.0), + windows::core::w!("ECCCurveName"), + curve_bytes, + 0, + )) + .expect("BCryptSetProperty curve25519"); + + X25519Alg(handle) + } +}); + +/// X25519 key exchange using Windows CNG Curve25519 support. +struct X25519KeyExchange { + key_handle: Owned, + public_key_bytes: Buf, +} + +// SAFETY: `BCRYPT_KEY_HANDLE` is an opaque CNG handle documented by Microsoft +// Learn for the BCrypt APIs; this wrapper never dereferences it directly and +// only passes it back to those APIs. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +unsafe impl Send for X25519KeyExchange {} +unsafe impl Sync for X25519KeyExchange {} + +impl std::fmt::Debug for X25519KeyExchange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("X25519KeyExchange").finish_non_exhaustive() + } +} + +impl X25519KeyExchange { + fn new(mut buf: Buf) -> Result { + let alg_handle = X25519_PROVIDER.0; + + // SAFETY: Microsoft Learn documents `BCryptGenerateKeyPair` and + // `BCryptFinalizeKeyPair` as initializing the caller-provided handle + // for the duration of the call; the output handle outlives this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptgeneratekeypair + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinalizekeypair + let key_handle = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptGenerateKeyPair( + alg_handle, + &mut *key_handle, + 255, + 0, + )) + .map_err(|e| format!("BCryptGenerateKeyPair X25519 failed: {e}"))?; + + WinCryptoError::from_ntstatus(BCryptFinalizeKeyPair(*key_handle, 0)) + .map_err(|e| format!("BCryptFinalizeKeyPair X25519 failed: {e}"))?; + + key_handle + }; + + // Export public key — 32 raw bytes for X25519 + let pub_key = export_x25519_public_key(*key_handle)?; + + buf.clear(); + buf.extend_from_slice(&pub_key); + + Ok(Self { + key_handle, + public_key_bytes: buf, + }) + } +} + +impl ActiveKeyExchange for X25519KeyExchange { + fn pub_key(&self) -> &[u8] { + &self.public_key_bytes + } + + fn complete(self: Box, peer_pub: &[u8], out: &mut Buf) -> Result<(), String> { + if peer_pub.len() != 32 { + return Err(format!( + "Invalid X25519 public key length: {} (expected 32)", + peer_pub.len(), + )); + } + + let alg_handle = X25519_PROVIDER.0; + let peer_key_handle = import_x25519_public_key(alg_handle, peer_pub)?; + + // SAFETY: Microsoft Learn documents `BCryptSecretAgreement` as + // borrowing both key handles and the output secret handle for the + // duration of the call; all handles outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptsecretagreement + let shared_secret = unsafe { + let mut secret_handle = Owned::new(BCRYPT_SECRET_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptSecretAgreement( + *self.key_handle, + *peer_key_handle, + &mut *secret_handle, + 0, + )) + .map_err(|e| format!("BCryptSecretAgreement X25519 failed: {e}"))?; + + derive_raw_secret(*secret_handle)? + }; + + // RFC 7748 §6.1: reject all-zero shared secret (non-contributory / low-order point) + if shared_secret.iter().all(|&b| b == 0) { + return Err("X25519 shared secret is zero (non-contributory)".into()); + } + + // BCryptDeriveKey with BCRYPT_KDF_RAW_SECRET returns the raw secret in + // little-endian (least-significant byte first) for ALL curve types. + // X25519 is natively little-endian per RFC 7748, but CNG returns it in + // the opposite order. Reverse to match the RFC 7748 wire format that + // other implementations (x25519-dalek, BoringSSL, etc.) produce. + let mut secret = shared_secret; + secret.reverse(); + + out.clear(); + out.extend_from_slice(&secret); + + Ok(()) + } + + fn group(&self) -> NamedGroup { + NamedGroup::X25519 + } +} + +/// Export X25519 public key as 32 raw bytes. +fn export_x25519_public_key(key_handle: BCRYPT_KEY_HANDLE) -> Result, String> { + // SAFETY: Microsoft Learn documents `BCryptExportKey` as borrowing the key + // handle and optional output buffer for the duration of each call; both + // outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptexportkey + unsafe { + let mut blob_size = 0u32; + WinCryptoError::from_ntstatus(BCryptExportKey( + key_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + None, + &mut blob_size, + 0, + )) + .map_err(|e| format!("BCryptExportKey X25519 size query failed: {e}"))?; + + let mut blob = vec![0u8; blob_size as usize]; + WinCryptoError::from_ntstatus(BCryptExportKey( + key_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + Some(&mut blob), + &mut blob_size, + 0, + )) + .map_err(|e| format!("BCryptExportKey X25519 failed: {e}"))?; + + // BCRYPT_ECCKEY_BLOB: dwMagic(4) + cbKey(4) then X[cbKey] + Y[cbKey] + // For Curve25519 cbKey=32, so X is the 32-byte u-coordinate. + let header_size = 8; + let cb_key = u32::from_le_bytes(blob[4..8].try_into().unwrap()) as usize; + if blob.len() < header_size + cb_key { + return Err(format!("X25519 public key blob too small: {}", blob.len())); + } + + Ok(blob[header_size..header_size + cb_key].to_vec()) + } +} + +/// Import an X25519 public key from 32 raw bytes. +fn import_x25519_public_key( + alg_handle: BCRYPT_ALG_HANDLE, + pub_key: &[u8], +) -> Result, String> { + // BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC + let magic: u32 = 0x504B4345; + let cb_key: u32 = 32; + + // BCRYPT_ECCPUBLIC_BLOB: header(8) + X[cbKey] + Y[cbKey] + // For Curve25519, Y is not meaningful; pad with zeros. + let mut blob = Vec::with_capacity(8 + 64); + blob.extend_from_slice(&magic.to_le_bytes()); + blob.extend_from_slice(&cb_key.to_le_bytes()); + blob.extend_from_slice(pub_key); // X (32 bytes) + blob.extend_from_slice(&[0u8; 32]); // Y (32 zeros) + + // SAFETY: Microsoft Learn documents `BCryptImportKeyPair` as borrowing the + // key blob and output handle only for the duration of the call; both + // outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptimportkeypair + unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptImportKeyPair( + alg_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + &mut *key_handle, + &blob, + 0, + )) + .map_err(|e| format!("BCryptImportKeyPair X25519 failed: {e}"))?; + + Ok(key_handle) + } +} + +/// X25519 key exchange group. +#[derive(Debug)] +struct X25519Kx; + +impl SupportedKxGroup for X25519Kx { + fn name(&self) -> NamedGroup { + NamedGroup::X25519 + } + + fn start_exchange(&self, buf: Buf) -> Result, String> { + Ok(Box::new(X25519KeyExchange::new(buf)?)) + } +} + +static KX_GROUP_X25519: X25519Kx = X25519Kx; +static KX_GROUP_P256: P256 = P256; +static KX_GROUP_P384: P384 = P384; + +pub(super) static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = + &[&KX_GROUP_X25519, &KX_GROUP_P256, &KX_GROUP_P384]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Two wincrypto X25519 exchanges must produce identical shared secrets. + #[test] + fn x25519_roundtrip_symmetric() { + let alice = X25519Kx.start_exchange(Buf::new()).unwrap(); + let bob = X25519Kx.start_exchange(Buf::new()).unwrap(); + + let alice_pub = alice.pub_key().to_vec(); + let bob_pub = bob.pub_key().to_vec(); + + let mut alice_secret = Buf::new(); + let mut bob_secret = Buf::new(); + alice.complete(&bob_pub, &mut alice_secret).unwrap(); + bob.complete(&alice_pub, &mut bob_secret).unwrap(); + + assert_eq!( + &alice_secret[..], + &bob_secret[..], + "X25519 shared secrets must be identical regardless of which side completes" + ); + assert_eq!(alice_secret.len(), 32); + } + + /// Verify wincrypto X25519 shared secret matches x25519-dalek (reference implementation). + /// + /// This catches byte-order bugs: BCryptDeriveKey returns the raw secret in + /// reversed byte order, and we must reverse it to match RFC 7748. + #[test] + fn x25519_interop_with_dalek() { + use rand_core::OsRng; + use x25519_dalek::{EphemeralSecret, PublicKey}; + + // Generate a key pair with x25519-dalek (reference) + let dalek_secret = EphemeralSecret::random_from_rng(&mut OsRng); + let dalek_pub = PublicKey::from(&dalek_secret); + + // Generate a key pair with wincrypto CNG + let win_kx = X25519Kx.start_exchange(Buf::new()).unwrap(); + let win_pub = win_kx.pub_key().to_vec(); + + // wincrypto completes with dalek's public key + let mut win_shared = Buf::new(); + win_kx + .complete(dalek_pub.as_bytes(), &mut win_shared) + .unwrap(); + + // dalek completes with wincrypto's public key + let win_pub_bytes: [u8; 32] = win_pub.try_into().unwrap(); + let dalek_peer = PublicKey::from(win_pub_bytes); + let dalek_shared = dalek_secret.diffie_hellman(&dalek_peer); + + assert_eq!( + &win_shared[..], + dalek_shared.as_bytes(), + "wincrypto and x25519-dalek must produce the same shared secret" + ); + } +} diff --git a/crypto/wincrypto/src/dimpl_provider/mod.rs b/crypto/wincrypto/src/dimpl_provider/mod.rs new file mode 100644 index 000000000..0d4257d75 --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/mod.rs @@ -0,0 +1,65 @@ +//! Windows CNG cryptographic provider for dimpl. +//! +//! This module implements the dimpl crypto provider traits using +//! Windows CNG (Cryptography Next Generation) APIs. + +#![allow(unsafe_code)] + +mod cipher_suite; +mod hash; +mod hmac; +mod kx_group; +pub(crate) mod sign; + +use dimpl::crypto::{CryptoProvider, SecureRandom}; + +use crate::WinCryptoError; + +/// Get the Windows CNG based crypto provider for dimpl. +pub fn default_provider() -> CryptoProvider { + CryptoProvider { + cipher_suites: cipher_suite::ALL_CIPHER_SUITES, + dtls13_cipher_suites: cipher_suite::ALL_DTLS13_CIPHER_SUITES, + kx_groups: kx_group::ALL_KX_GROUPS, + signature_verification: &sign::SIGNATURE_VERIFIER, + key_provider: &sign::KEY_PROVIDER, + secure_random: &SECURE_RANDOM, + hash_provider: &hash::HASH_PROVIDER, + hmac_provider: &hmac::HMAC_PROVIDER, + } +} + +#[derive(Debug)] +struct WinCngSecureRandom; + +impl SecureRandom for WinCngSecureRandom { + fn fill(&self, buf: &mut [u8]) -> Result<(), String> { + use windows::Win32::Security::Cryptography::BCRYPT_USE_SYSTEM_PREFERRED_RNG; + use windows::Win32::Security::Cryptography::BCryptGenRandom; + // SAFETY: Microsoft Learn documents `BCryptGenRandom` as filling the + // caller-provided mutable buffer for the duration of the call; `buf` + // outlives this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom + unsafe { + WinCryptoError::from_ntstatus(BCryptGenRandom( + None, + buf, + BCRYPT_USE_SYSTEM_PREFERRED_RNG, + )) + .map_err(|e| format!("BCryptGenRandom failed: {e}"))?; + } + Ok(()) + } +} + +static SECURE_RANDOM: WinCngSecureRandom = WinCngSecureRandom; + +#[cfg(test)] +mod tests { + #[test] + fn validate_dimpl_provider() -> Result<(), String> { + super::default_provider() + .validate() + .map_err(|err| format!("{err:?}")) + } +} diff --git a/crypto/wincrypto/src/dimpl_provider/sign.rs b/crypto/wincrypto/src/dimpl_provider/sign.rs new file mode 100644 index 000000000..cb99f08d5 --- /dev/null +++ b/crypto/wincrypto/src/dimpl_provider/sign.rs @@ -0,0 +1,743 @@ +//! Signing and key loading implementations using Windows CNG BCrypt ECDSA. + +use std::str; + +use dimpl::crypto::Buf; +use dimpl::crypto::{HashAlgorithm, KeyProvider}; +use dimpl::crypto::{SignatureAlgorithm, SignatureVerifier, SigningKey as SigningKeyTrait}; + +use windows::Win32::Security::Cryptography::BCRYPT_ECCPRIVATE_BLOB; +use windows::Win32::Security::Cryptography::BCRYPT_ECCPUBLIC_BLOB; +use windows::Win32::Security::Cryptography::BCRYPT_ECDSA_P256_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_ECDSA_P384_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_FLAGS; +use windows::Win32::Security::Cryptography::BCRYPT_KEY_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_SHA256_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCRYPT_SHA384_ALG_HANDLE; +use windows::Win32::Security::Cryptography::BCryptHash; +use windows::Win32::Security::Cryptography::BCryptImportKeyPair; +use windows::Win32::Security::Cryptography::BCryptSignHash; +use windows::Win32::Security::Cryptography::BCryptVerifySignature; +use windows::core::Owned; + +use crate::WinCryptoError; + +// Well-known OID values (raw DER content bytes, without tag/length). +/// ecPublicKey: 1.2.840.10045.2.1 +const OID_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; +/// prime256v1 (P-256): 1.2.840.10045.3.1.7 +const OID_PRIME256V1: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; +/// secp384r1 (P-384): 1.3.132.0.34 +const OID_SECP384R1: &[u8] = &[0x2B, 0x81, 0x04, 0x00, 0x22]; + +#[derive(Clone, Copy, Debug)] +enum EcCurve { + P256, + P384, +} + +/// ECDSA signing key implementation using Windows CNG. +struct EcdsaSigningKey { + key_handle: Owned, + curve: EcCurve, +} + +// SAFETY: `BCRYPT_KEY_HANDLE` is an opaque CNG handle documented by Microsoft +// Learn for the BCrypt APIs; this wrapper never dereferences it directly and +// only passes it back to those APIs. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +unsafe impl Send for EcdsaSigningKey {} +// SAFETY: `BCRYPT_KEY_HANDLE` is an opaque CNG handle documented by Microsoft +// Learn for the BCrypt APIs; this wrapper never dereferences it directly and +// only passes it back to those APIs. +// Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/ +unsafe impl Sync for EcdsaSigningKey {} + +impl std::fmt::Debug for EcdsaSigningKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EcdsaSigningKey") + .field("curve", &self.curve) + .finish_non_exhaustive() + } +} + +impl SigningKeyTrait for EcdsaSigningKey { + fn sign(&mut self, data: &[u8], out: &mut Buf) -> Result<(), String> { + // Hash the data. + let (hash_alg, hash_len) = match self.curve { + EcCurve::P256 => (BCRYPT_SHA256_ALG_HANDLE, 32usize), + EcCurve::P384 => (BCRYPT_SHA384_ALG_HANDLE, 48usize), + }; + + let mut hash = vec![0u8; hash_len]; + // SAFETY: Microsoft Learn documents `BCryptHash` as borrowing the input + // and output buffers only for the duration of the call; both outlive + // this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcrypthash + unsafe { + WinCryptoError::from_ntstatus(BCryptHash(hash_alg, None, data, &mut hash)) + .map_err(|e| format!("Hash failed: {e}"))?; + } + + // Sign the hash. + let sig_size = match self.curve { + EcCurve::P256 => 64usize, // 32 + 32 + EcCurve::P384 => 96usize, // 48 + 48 + }; + let mut raw_sig = vec![0u8; sig_size]; + let mut sig_len = 0u32; + + // SAFETY: Microsoft Learn documents `BCryptSignHash` as borrowing the + // key handle, hash, and output buffer only for the duration of the + // call; all three outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptsignhash + unsafe { + WinCryptoError::from_ntstatus(BCryptSignHash( + *self.key_handle, + None, + &hash, + Some(&mut raw_sig), + &mut sig_len, + BCRYPT_FLAGS(0), + )) + .map_err(|e| format!("BCryptSignHash failed: {e}"))?; + } + + raw_sig.truncate(sig_len as usize); + + // Convert raw (r,s) to DER-encoded ECDSA-Sig-Value. + let der_sig = raw_rs_to_der(&raw_sig)?; + + out.clear(); + out.extend_from_slice(&der_sig); + Ok(()) + } + + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::ECDSA + } + + fn hash_algorithm(&self) -> HashAlgorithm { + match self.curve { + EcCurve::P256 => HashAlgorithm::SHA256, + EcCurve::P384 => HashAlgorithm::SHA384, + } + } +} + +/// ECDSA key provider using Windows CNG. +#[derive(Debug)] +pub(super) struct WinCngKeyProvider; + +impl KeyProvider for WinCngKeyProvider { + fn load_private_key(&self, key_der: &[u8]) -> Result, String> { + // Try PEM format first. + if let Ok(pem_str) = str::from_utf8(key_der) { + if pem_str.contains("-----BEGIN") { + if let Ok(decoded) = decode_pem(pem_str) { + return self.load_private_key(&decoded); + } + } + } + + // Try PKCS#8 unwrap, fall back to treating as raw SEC1. + let sec1_der = try_unwrap_pkcs8(key_der).unwrap_or_else(|| key_der.to_vec()); + + // Parse SEC1 ECPrivateKey. + let (curve_hint, private_key_bytes, public_key_opt) = parse_ec_private_key(&sec1_der)?; + + let curve = if let Some(c) = curve_hint { + c + } else if private_key_bytes.len() == 32 { + EcCurve::P256 + } else if private_key_bytes.len() == 48 { + EcCurve::P384 + } else { + return Err(format!( + "Could not determine EC curve from key length: {}", + private_key_bytes.len() + )); + }; + + let public_key_bytes = public_key_opt + .ok_or_else(|| "EC private key missing public key component".to_string())?; + + let coord_size = match curve { + EcCurve::P256 => 32usize, + EcCurve::P384 => 48usize, + }; + + let expected_pub_len = 1 + 2 * coord_size; + if public_key_bytes.len() != expected_pub_len || public_key_bytes[0] != 0x04 { + return Err(format!( + "Unexpected public key length: {} (expected {})", + public_key_bytes.len(), + expected_pub_len + )); + } + + let x = &public_key_bytes[1..1 + coord_size]; + let y = &public_key_bytes[1 + coord_size..]; + let d = &private_key_bytes; + + let (alg_handle, magic) = match curve { + EcCurve::P256 => ( + BCRYPT_ECDSA_P256_ALG_HANDLE, + 0x32534345u32, // BCRYPT_ECDSA_PRIVATE_P256_MAGIC + ), + EcCurve::P384 => ( + BCRYPT_ECDSA_P384_ALG_HANDLE, + 0x34534345u32, // BCRYPT_ECDSA_PRIVATE_P384_MAGIC + ), + }; + + // Build BCRYPT_ECCKEY_BLOB for private key. + let header_size = 8; + let mut blob = Vec::with_capacity(header_size + 3 * coord_size); + blob.extend_from_slice(&magic.to_le_bytes()); + blob.extend_from_slice(&(coord_size as u32).to_le_bytes()); + + // Pad/truncate X, Y, D to coord_size. + pad_to(&mut blob, x, coord_size); + pad_to(&mut blob, y, coord_size); + pad_to(&mut blob, d, coord_size); + + // SAFETY: Microsoft Learn documents `BCryptImportKeyPair` as borrowing + // the key blob and output handle only for the duration of the call; + // both outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptimportkeypair + let key_handle = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptImportKeyPair( + alg_handle, + None, + BCRYPT_ECCPRIVATE_BLOB, + &mut *key_handle, + &blob, + 0, + )) + .map_err(|e| format!("BCryptImportKeyPair failed: {e}"))?; + key_handle + }; + + Ok(Box::new(EcdsaSigningKey { key_handle, curve })) + } +} + +// Pad or truncate src to exactly `size` bytes, left-padded with zeros. +fn pad_to(dst: &mut Vec, src: &[u8], size: usize) { + if src.len() >= size { + dst.extend_from_slice(&src[src.len() - size..]); + } else { + let pad = size - src.len(); + dst.extend(std::iter::repeat_n(0u8, pad)); + dst.extend_from_slice(src); + } +} + +/// Signature verifier implementation using Windows CNG. +#[derive(Debug)] +pub(super) struct WinCngSignatureVerifier; + +impl SignatureVerifier for WinCngSignatureVerifier { + fn verify_signature( + &self, + cert_der: &[u8], + data: &[u8], + signature: &[u8], + hash_alg: HashAlgorithm, + sig_alg: SignatureAlgorithm, + ) -> Result<(), String> { + if sig_alg != SignatureAlgorithm::ECDSA { + return Err(format!("Unsupported signature algorithm: {sig_alg:?}")); + } + + let (alg_oid, pubkey_bytes) = extract_spki_from_cert(cert_der)?; + + if alg_oid != OID_EC_PUBLIC_KEY { + return Err("Unsupported public key algorithm OID".into()); + } + + // Verify uncompressed EC point format (0x04 prefix). + if pubkey_bytes.is_empty() || pubkey_bytes[0] != 0x04 { + return Err("EC public key is not in uncompressed format (0x04)".into()); + } + + // Determine key size from public key length. + let (alg_handle, coord_size, magic) = if pubkey_bytes.len() == 65 { + ( + BCRYPT_ECDSA_P256_ALG_HANDLE, + 32usize, + 0x31534345u32, // BCRYPT_ECDSA_PUBLIC_P256_MAGIC + ) + } else if pubkey_bytes.len() == 97 { + ( + BCRYPT_ECDSA_P384_ALG_HANDLE, + 48usize, + 0x33534345u32, // BCRYPT_ECDSA_PUBLIC_P384_MAGIC + ) + } else { + return Err(format!( + "Unsupported EC public key size: {} bytes", + pubkey_bytes.len() + )); + }; + + // Hash the data. + let (hash_bcrypt_alg, hash_len) = match hash_alg { + HashAlgorithm::SHA256 => (BCRYPT_SHA256_ALG_HANDLE, 32usize), + HashAlgorithm::SHA384 => (BCRYPT_SHA384_ALG_HANDLE, 48usize), + _ => { + return Err(format!( + "Unsupported hash algorithm for ECDSA: {hash_alg:?}" + )); + } + }; + + let mut hash = vec![0u8; hash_len]; + // SAFETY: Microsoft Learn documents `BCryptHash` as borrowing the input + // and output buffers only for the duration of the call; both outlive + // this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcrypthash + unsafe { + WinCryptoError::from_ntstatus(BCryptHash(hash_bcrypt_alg, None, data, &mut hash)) + .map_err(|e| format!("Hash failed: {e}"))?; + } + + // Import the public key. + let header_size = 8; + let mut blob = Vec::with_capacity(header_size + 2 * coord_size); + blob.extend_from_slice(&magic.to_le_bytes()); + blob.extend_from_slice(&(coord_size as u32).to_le_bytes()); + blob.extend_from_slice(&pubkey_bytes[1..]); // Skip 0x04 prefix + + // SAFETY: Microsoft Learn documents `BCryptImportKeyPair` as borrowing + // the key blob and output handle only for the duration of the call; + // both outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptimportkeypair + let key_handle = unsafe { + let mut key_handle = Owned::new(BCRYPT_KEY_HANDLE::default()); + WinCryptoError::from_ntstatus(BCryptImportKeyPair( + alg_handle, + None, + BCRYPT_ECCPUBLIC_BLOB, + &mut *key_handle, + &blob, + 0, + )) + .map_err(|e| format!("Import public key failed: {e}"))?; + key_handle + }; + + // Convert DER signature to raw (r,s) format. + let raw_sig = der_to_raw_rs(signature, coord_size)?; + + // Verify. + // SAFETY: Microsoft Learn documents `BCryptVerifySignature` as + // borrowing the key handle, hash, and signature buffers only for the + // duration of the call; all three outlive this block. + // Docs: https://learn.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptverifysignature + unsafe { + WinCryptoError::from_ntstatus(BCryptVerifySignature( + *key_handle, + None, + &hash, + &raw_sig, + BCRYPT_FLAGS(0), + )) + .map_err(|e| format!("ECDSA signature verification failed: {e}"))?; + } + + Ok(()) + } +} + +/// Convert raw (r || s) to DER-encoded ECDSA-Sig-Value. +pub(crate) fn raw_rs_to_der(raw: &[u8]) -> Result, String> { + if raw.len() % 2 != 0 { + return Err("Raw signature length must be even".into()); + } + let half = raw.len() / 2; + let r = &raw[..half]; + let s = &raw[half..]; + + let r_der = encode_der_integer(r); + let s_der = encode_der_integer(s); + + let mut content = Vec::new(); + content.extend_from_slice(&r_der); + content.extend_from_slice(&s_der); + + let mut result = vec![0x30]; // SEQUENCE tag + encode_der_length(content.len(), &mut result); + result.extend_from_slice(&content); + + Ok(result) +} + +/// Convert DER-encoded ECDSA-Sig-Value to raw (r || s). +fn der_to_raw_rs(der: &[u8], coord_size: usize) -> Result, String> { + // Parse outer SEQUENCE. + let (tag, seq_content, _) = parse_tlv(der)?; + if tag != 0x30 { + return Err("Invalid DER signature: not a SEQUENCE".into()); + } + + // Parse r INTEGER. + let (tag, r_bytes, rest) = parse_tlv(seq_content)?; + if tag != 0x02 { + return Err("Invalid DER signature: r not INTEGER".into()); + } + + // Parse s INTEGER. + let (tag, s_bytes, _) = parse_tlv(rest)?; + if tag != 0x02 { + return Err("Invalid DER signature: s not INTEGER".into()); + } + + // Convert to fixed-size (r || s). + let mut raw = vec![0u8; coord_size * 2]; + copy_integer_to_fixed(&mut raw[..coord_size], r_bytes)?; + copy_integer_to_fixed(&mut raw[coord_size..], s_bytes)?; + + Ok(raw) +} + +// Copy a DER integer (possibly with leading zero) into a fixed-size buffer, right-aligned. +fn copy_integer_to_fixed(dst: &mut [u8], src: &[u8]) -> Result<(), String> { + // Skip leading zeros in the DER integer. + let mut start = 0; + while start < src.len() && src[start] == 0 && (src.len() - start) > dst.len() { + start += 1; + } + let meaningful = &src[start..]; + if meaningful.len() <= dst.len() { + let offset = dst.len() - meaningful.len(); + dst[offset..].copy_from_slice(meaningful); + Ok(()) + } else { + Err(format!( + "DER integer too large: {} bytes for {}-byte field", + meaningful.len(), + dst.len() + )) + } +} + +fn encode_der_integer(value: &[u8]) -> Vec { + if value.is_empty() { + // Encode zero. + return vec![0x02, 0x01, 0x00]; + } + + // Skip leading zeros but keep at least one byte. + let mut start = 0; + while start < value.len() - 1 && value[start] == 0 { + start += 1; + } + let trimmed = &value[start..]; + + let mut result = vec![0x02]; // INTEGER tag + if trimmed[0] & 0x80 != 0 { + // Need leading zero. + encode_der_length(trimmed.len() + 1, &mut result); + result.push(0x00); + } else { + encode_der_length(trimmed.len(), &mut result); + } + result.extend_from_slice(trimmed); + result +} + +fn encode_der_length(len: usize, out: &mut Vec) { + if len < 128 { + out.push(len as u8); + } else if len < 256 { + out.push(0x81); + out.push(len as u8); + } else { + out.push(0x82); + out.push((len >> 8) as u8); + out.push(len as u8); + } +} + +fn parse_der_length(data: &[u8]) -> Result<(usize, &[u8]), String> { + if data.is_empty() { + return Err("DER: unexpected end of data".into()); + } + if data[0] < 128 { + Ok((data[0] as usize, &data[1..])) + } else if data[0] == 0x81 { + if data.len() < 2 { + return Err("DER: truncated length".into()); + } + Ok((data[1] as usize, &data[2..])) + } else if data[0] == 0x82 { + if data.len() < 3 { + return Err("DER: truncated length".into()); + } + Ok((((data[1] as usize) << 8) | (data[2] as usize), &data[3..])) + } else { + Err("DER: unsupported length encoding".into()) + } +} + +// ============================================================================ +// Minimal DER parsing helpers (replaces der/pkcs8/sec1/spki/x509-cert crates) +// ============================================================================ + +/// Parse a DER TLV (Tag-Length-Value). Returns (tag, value, remaining_bytes). +fn parse_tlv(data: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { + if data.is_empty() { + return Err("DER: unexpected end of data".into()); + } + let tag = data[0]; + let (len, after_len) = parse_der_length(&data[1..])?; + if after_len.len() < len { + return Err(format!( + "DER: truncated (need {len}, have {})", + after_len.len() + )); + } + Ok((tag, &after_len[..len], &after_len[len..])) +} + +/// Decode PEM to DER bytes. +fn decode_pem(pem: &str) -> Result, String> { + use windows::Win32::Security::Cryptography::{CRYPT_STRING_BASE64HEADER, CryptStringToBinaryA}; + + // CryptStringToBinaryA handles PEM headers/footers and base64 decoding. + let pem_bytes = pem.as_bytes(); + let mut der_len = 0u32; + + // First call: get required buffer size. + // SAFETY: Microsoft Learn documents `CryptStringToBinaryA` as allowing a + // null output buffer when querying the required decoded size. + // Docs: https://learn.microsoft.com/windows/win32/api/wincrypt/nf-wincrypt-cryptstringtobinarya + unsafe { + CryptStringToBinaryA( + pem_bytes, + CRYPT_STRING_BASE64HEADER, + None, + &mut der_len, + None, + None, + ) + .map_err(|e| format!("PEM decode (size query): {e}"))?; + } + + let mut der = vec![0u8; der_len as usize]; + + // Second call: decode into buffer. + // SAFETY: Microsoft Learn documents `CryptStringToBinaryA` as writing into + // the caller-provided output buffer for the duration of the call; `der` + // was sized from the preceding length query and outlives this block. + // Docs: https://learn.microsoft.com/windows/win32/api/wincrypt/nf-wincrypt-cryptstringtobinarya + unsafe { + CryptStringToBinaryA( + pem_bytes, + CRYPT_STRING_BASE64HEADER, + Some(der.as_mut_ptr()), + &mut der_len, + None, + None, + ) + .map_err(|e| format!("PEM decode: {e}"))?; + } + + der.truncate(der_len as usize); + Ok(der) +} + +/// Try to unwrap PKCS#8 PrivateKeyInfo, returning the inner private key DER. +/// Returns None if the data doesn't look like PKCS#8 for an EC key. +fn try_unwrap_pkcs8(der: &[u8]) -> Option> { + // SEQUENCE { version INTEGER, algorithm SEQUENCE, privateKey OCTET STRING } + let (tag, seq_content, _) = parse_tlv(der).ok()?; + if tag != 0x30 { + return None; + } + + // version INTEGER. + let (tag, _version, rest) = parse_tlv(seq_content).ok()?; + if tag != 0x02 { + return None; + } + + // algorithm SEQUENCE — verify it contains ecPublicKey OID. + let (tag, alg_content, rest) = parse_tlv(rest).ok()?; + if tag != 0x30 { + return None; + } + let (tag, alg_oid, _) = parse_tlv(alg_content).ok()?; + if tag != 0x06 || alg_oid != OID_EC_PUBLIC_KEY { + return None; + } + + // privateKey OCTET STRING. + let (tag, private_key, _) = parse_tlv(rest).ok()?; + if tag != 0x04 { + return None; + } + + Some(private_key.to_vec()) +} + +/// Parse SEC1 ECPrivateKey. Returns (curve, private_key, public_key). +#[allow(clippy::type_complexity)] +fn parse_ec_private_key(der: &[u8]) -> Result<(Option, Vec, Option>), String> { + let (tag, seq_content, _) = parse_tlv(der)?; + if tag != 0x30 { + return Err("ECPrivateKey: not a SEQUENCE".into()); + } + + // version INTEGER (should be 1). + let (tag, _version, rest) = parse_tlv(seq_content)?; + if tag != 0x02 { + return Err("ECPrivateKey: version not INTEGER".into()); + } + + // privateKey OCTET STRING. + let (tag, private_key, mut rest) = parse_tlv(rest)?; + if tag != 0x04 { + return Err("ECPrivateKey: privateKey not OCTET STRING".into()); + } + + let mut curve = None; + let mut public_key = None; + + // Optional context-tagged fields. + while !rest.is_empty() { + let (tag, content, remaining) = parse_tlv(rest)?; + match tag { + 0xA0 => { + // [0] parameters — should contain an OID. + let (tag, oid_bytes, _) = parse_tlv(content)?; + if tag == 0x06 { + curve = oid_to_curve(oid_bytes); + } + } + 0xA1 => { + // [1] publicKey — BIT STRING. + let (tag, bs_content, _) = parse_tlv(content)?; + if tag == 0x03 && bs_content.len() > 1 { + if bs_content[0] != 0 { + return Err("ECPrivateKey: BIT STRING has non-zero unused bits".into()); + } + public_key = Some(bs_content[1..].to_vec()); + } + } + _ => {} // Skip unknown. + } + rest = remaining; + } + + Ok((curve, private_key.to_vec(), public_key)) +} + +// Map raw OID value bytes to an EcCurve. +fn oid_to_curve(oid_bytes: &[u8]) -> Option { + if oid_bytes == OID_PRIME256V1 { + Some(EcCurve::P256) + } else if oid_bytes == OID_SECP384R1 { + Some(EcCurve::P384) + } else { + None + } +} + +/// Extract SubjectPublicKeyInfo from a DER-encoded X.509 certificate. +/// Returns (algorithm_oid_bytes, public_key_bytes) where public_key_bytes +/// is the BIT STRING content without the unused-bits byte. +fn extract_spki_from_cert(cert_der: &[u8]) -> Result<(&[u8], &[u8]), String> { + // Certificate SEQUENCE + let (tag, cert_content, _) = parse_tlv(cert_der)?; + if tag != 0x30 { + return Err("Certificate: not a SEQUENCE".into()); + } + + // TBSCertificate SEQUENCE + let (tag, tbs_content, _) = parse_tlv(cert_content)?; + if tag != 0x30 { + return Err("TBSCertificate: not a SEQUENCE".into()); + } + + let mut pos = tbs_content; + + // [0] version — optional context-specific tag + if !pos.is_empty() && pos[0] == 0xA0 { + let (_, _, rest) = parse_tlv(pos)?; + pos = rest; + } + + // serialNumber INTEGER + let (tag, _, rest) = parse_tlv(pos)?; + if tag != 0x02 { + return Err("TBS: serialNumber not INTEGER".into()); + } + pos = rest; + + // signature AlgorithmIdentifier SEQUENCE + let (tag, _, rest) = parse_tlv(pos)?; + if tag != 0x30 { + return Err("TBS: signature not SEQUENCE".into()); + } + pos = rest; + + // issuer Name SEQUENCE + let (tag, _, rest) = parse_tlv(pos)?; + if tag != 0x30 { + return Err("TBS: issuer not SEQUENCE".into()); + } + pos = rest; + + // validity SEQUENCE + let (tag, _, rest) = parse_tlv(pos)?; + if tag != 0x30 { + return Err("TBS: validity not SEQUENCE".into()); + } + pos = rest; + + // subject Name SEQUENCE + let (tag, _, rest) = parse_tlv(pos)?; + if tag != 0x30 { + return Err("TBS: subject not SEQUENCE".into()); + } + pos = rest; + + // subjectPublicKeyInfo SEQUENCE + let (tag, spki_content, _) = parse_tlv(pos)?; + if tag != 0x30 { + return Err("SPKI: not a SEQUENCE".into()); + } + + // algorithm AlgorithmIdentifier SEQUENCE + let (tag, alg_content, rest) = parse_tlv(spki_content)?; + if tag != 0x30 { + return Err("SPKI algorithm: not a SEQUENCE".into()); + } + + // Extract OID from algorithm + let (tag, oid_bytes, _) = parse_tlv(alg_content)?; + if tag != 0x06 { + return Err("SPKI: algorithm OID not found".into()); + } + + // subjectPublicKey BIT STRING + let (tag, bs_content, _) = parse_tlv(rest)?; + if tag != 0x03 { + return Err("SPKI: publicKey not BIT STRING".into()); + } + if bs_content.len() < 2 { + return Err("SPKI: BIT STRING too short".into()); + } + if bs_content[0] != 0 { + return Err("SPKI: BIT STRING has non-zero unused bits".into()); + } + + Ok((oid_bytes, &bs_content[1..])) +} + +pub(super) static KEY_PROVIDER: WinCngKeyProvider = WinCngKeyProvider; +pub(super) static SIGNATURE_VERIFIER: WinCngSignatureVerifier = WinCngSignatureVerifier; diff --git a/crypto/wincrypto/src/dtls_dimpl.rs b/crypto/wincrypto/src/dtls_dimpl.rs new file mode 100644 index 000000000..d485bc622 --- /dev/null +++ b/crypto/wincrypto/src/dtls_dimpl.rs @@ -0,0 +1,127 @@ +//! DTLS implementation using dimpl with Windows CNG as crypto backend. + +use std::sync::Arc; +use std::time::Instant; + +use str0m_proto::crypto::CryptoError; +use str0m_proto::crypto::DtlsVersion; +use str0m_proto::crypto::dtls::{DtlsCert, DtlsImplError, DtlsInstance, DtlsOutput, DtlsProvider}; + +use dimpl::{Config, Dtls, DtlsCertificate}; + +// ============================================================================ +// DTLS Provider Implementation +// ============================================================================ + +#[derive(Debug)] +pub(crate) struct WinCryptoDtlsProvider; + +impl DtlsProvider for WinCryptoDtlsProvider { + fn generate_certificate(&self) -> Option { + generate_certificate_impl().ok() + } + + fn new_dtls( + &self, + cert: &DtlsCert, + now: Instant, + dtls_version: DtlsVersion, + ) -> Result, CryptoError> { + let dimpl_cert = DtlsCertificate { + certificate: cert.certificate.clone(), + private_key: cert.private_key.clone(), + }; + + let mut builder = Config::builder(); + if self.is_test() { + builder = builder.dangerously_set_rng_seed(42); + } + + let config = builder + .with_crypto_provider(crate::dimpl_provider::default_provider()) + .build() + .map_err(|e| CryptoError::Other(format!("dimpl config creation failed: {e}")))?; + + let config = Arc::new(config); + let dtls = match dtls_version { + DtlsVersion::Dtls12 => Dtls::new_12(config, dimpl_cert, now), + DtlsVersion::Dtls13 => Dtls::new_13(config, dimpl_cert, now), + DtlsVersion::Auto => Dtls::new_auto(config, dimpl_cert, now), + _ => { + return Err(CryptoError::Other(format!( + "Unsupported DTLS version: {dtls_version}" + ))); + } + }; + + Ok(Box::new(WinCryptoDtlsInstance { dtls })) + } +} + +struct WinCryptoDtlsInstance { + dtls: Dtls, +} + +impl std::fmt::Debug for WinCryptoDtlsInstance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WinCryptoDtlsInstance").finish() + } +} + +impl DtlsInstance for WinCryptoDtlsInstance { + fn set_active(&mut self, active: bool) { + self.dtls.set_active(active); + } + + fn handle_packet(&mut self, packet: &[u8]) -> Result<(), DtlsImplError> { + self.dtls.handle_packet(packet) + } + + fn poll_output<'a>(&mut self, buf: &'a mut [u8]) -> DtlsOutput<'a> { + self.dtls.poll_output(buf) + } + + fn handle_timeout(&mut self, now: Instant) -> Result<(), DtlsImplError> { + self.dtls.handle_timeout(now) + } + + fn send_application_data(&mut self, data: &[u8]) -> Result<(), DtlsImplError> { + self.dtls.send_application_data(data) + } + + fn is_active(&self) -> bool { + self.dtls.is_active() + } +} + +// ============================================================================ +// Certificate Generation +// ============================================================================ + +fn generate_certificate_impl() -> Result { + let cert = crate::sys::Certificate::new_self_signed(true, "cn=WebRTC").map_err(|e| { + CryptoError::Other(format!("Failed to create self-signed certificate: {e}")) + })?; + + let certificate = cert + .get_der_bytes() + .map_err(|e| CryptoError::Other(format!("Failed to get certificate DER bytes: {e}")))?; + + let private_key_der = cert + .export_private_key_pkcs8_der() + .map_err(|e| CryptoError::Other(format!("Failed to export private key as PKCS#8: {e}")))?; + + Ok(DtlsCert { + certificate, + private_key: private_key_der, + }) +} + +#[cfg(test)] +mod tests { + #[test] + fn generate_self_signed_certificate() { + let cert = super::generate_certificate_impl().unwrap(); + assert_eq!(165, cert.private_key.len()); + } +} diff --git a/crypto/wincrypto/src/dtls.rs b/crypto/wincrypto/src/dtls_schannel.rs similarity index 97% rename from crypto/wincrypto/src/dtls.rs rename to crypto/wincrypto/src/dtls_schannel.rs index f40bc14f1..b2c7febe1 100644 --- a/crypto/wincrypto/src/dtls.rs +++ b/crypto/wincrypto/src/dtls_schannel.rs @@ -44,13 +44,14 @@ impl DtlsProvider for WinCryptoDtlsProvider { _now: Instant, dtls_version: DtlsVersion, ) -> Result, CryptoError> { - if dtls_version != DtlsVersion::Dtls12 { + if !matches!(dtls_version, DtlsVersion::Dtls12 | DtlsVersion::Auto) { return Err(CryptoError::Other( - "WinCrypto DTLS provider only supports DTLS 1.2. \ - Use aws-lc-rs or rust-crypto backend for DTLS 1.3/Auto." + "WinCrypto DTLS provider only supports DTLS 1.2 without prefer_dimpl. \ + Enable the prefer_dimpl feature for DTLS 1.3." .to_string(), )); } + // Look up the Windows certificate by its DER bytes let win_cert = CERT_CACHE .lock() diff --git a/crypto/wincrypto/src/lib.rs b/crypto/wincrypto/src/lib.rs index 4caae4d14..ff1d6f90e 100644 --- a/crypto/wincrypto/src/lib.rs +++ b/crypto/wincrypto/src/lib.rs @@ -1,6 +1,7 @@ //! Windows SChannel + CNG implementation of cryptographic functions. //! DTLS via Windows SChannel. +#[cfg(not(feature = "prefer_dimpl"))] #[macro_use] extern crate tracing; @@ -13,6 +14,10 @@ use sha1::WinCryptoSha1HmacProvider; mod sha256; use sha256::WinCryptoSha256Provider; +#[cfg(feature = "prefer_dimpl")] +mod dimpl_provider; +#[cfg_attr(feature = "prefer_dimpl", path = "dtls_dimpl.rs")] +#[cfg_attr(not(feature = "prefer_dimpl"), path = "dtls_schannel.rs")] mod dtls; use dtls::WinCryptoDtlsProvider; diff --git a/crypto/wincrypto/src/sys/cert.rs b/crypto/wincrypto/src/sys/cert.rs index 79cb419c6..d025f1b92 100644 --- a/crypto/wincrypto/src/sys/cert.rs +++ b/crypto/wincrypto/src/sys/cert.rs @@ -98,6 +98,24 @@ impl Certificate { CERT_KEY_SPEC(0), NCRYPT_SILENT_FLAG, )?; + + // Dimpl currently requires the Certificate and Private Key as + // DER-encoded bytes. This will allow plaintext export so the + // private key can be exported in PKCS#8 format after creation. + #[cfg(feature = "prefer_dimpl")] + { + use windows::Win32::Security::Cryptography::NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG; + use windows::Win32::Security::Cryptography::NCRYPT_EXPORT_POLICY_PROPERTY; + use windows::Win32::Security::Cryptography::NCryptSetProperty; + let export_policy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG; + NCryptSetProperty( + key_handle.into(), + NCRYPT_EXPORT_POLICY_PROPERTY, + &export_policy.to_le_bytes(), + NCRYPT_SILENT_FLAG, + )?; + } + NCryptFinalizeKey(key_handle, NCRYPT_FLAGS(0))?; let key_prov_info = CRYPT_KEY_PROV_INFO { @@ -175,9 +193,53 @@ impl Certificate { crate::sys::sha256(&der_bytes) } + #[cfg(any(test, not(feature = "prefer_dimpl")))] pub fn context(&self) -> *const CERT_CONTEXT { self.cert_context } + + /// Export the private key in PKCS#8 DER format. + /// + /// Only available when the certificate was created via `new_self_signed` + /// with a key handle (EC-DSA keys). + #[cfg(feature = "prefer_dimpl")] + pub fn export_private_key_pkcs8_der(&self) -> Result, WinCryptoError> { + use windows::Win32::Security::Cryptography::NCRYPT_PKCS8_PRIVATE_KEY_BLOB; + use windows::Win32::Security::Cryptography::NCryptExportKey; + let key_handle = self + .key_handle + .ok_or_else(|| WinCryptoError::Generic("No private key handle available".into()))?; + + // SAFETY: NCryptExportKey borrows the key handle and output buffer for + // the duration of each call; both outlive this block. + unsafe { + // Query the required buffer size. + let mut size = 0u32; + NCryptExportKey( + key_handle, + None, + NCRYPT_PKCS8_PRIVATE_KEY_BLOB, + None, + None, + &mut size, + NCRYPT_SILENT_FLAG, + )?; + + let mut buf = vec![0u8; size as usize]; + NCryptExportKey( + key_handle, + None, + NCRYPT_PKCS8_PRIVATE_KEY_BLOB, + None, + Some(&mut buf), + &mut size, + NCRYPT_SILENT_FLAG, + )?; + + buf.truncate(size as usize); + Ok(buf) + } + } } impl From<*const CERT_CONTEXT> for Certificate { @@ -330,3 +392,22 @@ mod tests { assert_eq!(fingerprint.len(), 32); } } + +#[cfg(all(test, feature = "prefer_dimpl"))] +mod dimpl_tests { + #[test] + fn export_pkcs8_ec_dsa() { + let cert = super::Certificate::new_self_signed(true, "cn=WebRTC-PKCS8").unwrap(); + let pkcs8 = cert.export_private_key_pkcs8_der().unwrap(); + // PKCS#8 DER starts with a SEQUENCE tag (0x30). + assert_eq!(pkcs8[0], 0x30); + assert!(!pkcs8.is_empty()); + } + + #[test] + fn export_pkcs8_no_key_handle() { + let cert = super::Certificate::new_self_signed(false, "cn=WebRTC-RSA").unwrap(); + // RSA path doesn't store a key_handle, so export should fail. + assert!(cert.export_private_key_pkcs8_der().is_err()); + } +} diff --git a/crypto/wincrypto/src/sys/mod.rs b/crypto/wincrypto/src/sys/mod.rs index 48aac38f8..1c48cce6c 100644 --- a/crypto/wincrypto/src/sys/mod.rs +++ b/crypto/wincrypto/src/sys/mod.rs @@ -15,7 +15,9 @@ pub use sha256::*; mod srtp; pub use srtp::*; +#[cfg(not(feature = "prefer_dimpl"))] mod dtls; +#[cfg(not(feature = "prefer_dimpl"))] pub use dtls::*; #[derive(Debug)] diff --git a/crypto/wincrypto/src/sys/provider.rs b/crypto/wincrypto/src/sys/provider.rs index 3df5bd756..1e6e49106 100644 --- a/crypto/wincrypto/src/sys/provider.rs +++ b/crypto/wincrypto/src/sys/provider.rs @@ -65,11 +65,11 @@ struct WinCryptoSha256Provider; impl Sha256Provider for WinCryptoSha256Provider { fn sha256(&self, data: &[u8]) -> [u8; 32] { - use windows::core::Owned; - use windows::Win32::Security::Cryptography::BCryptHashData; use windows::Win32::Security::Cryptography::BCRYPT_HASH_HANDLE; use windows::Win32::Security::Cryptography::BCRYPT_SHA256_ALG_HANDLE; + use windows::Win32::Security::Cryptography::BCryptHashData; use windows::Win32::Security::Cryptography::{BCryptCreateHash, BCryptFinishHash}; + use windows::core::Owned; let mut hash = [0u8; 32]; unsafe { @@ -306,8 +306,8 @@ impl DtlsProvider for WinCryptoDtlsProvider { ) -> Result, CryptoError> { if dtls_version != DtlsVersion::Dtls12 { return Err(CryptoError::Other( - "WinCrypto DTLS provider only supports DTLS 1.2. \ - Use aws-lc-rs or rust-crypto backend for DTLS 1.3/Auto." + "WinCrypto DTLS provider only supports DTLS 1.2/Auto. \ + Use aws-lc-rs or rust-crypto backend for DTLS 1.3." .to_string(), )); } diff --git a/proto/Cargo.toml b/proto/Cargo.toml index ccd8040c9..6fa3f973f 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -20,7 +20,8 @@ _internal_test_exports = [] [dependencies] # DTLS made for str0m (required dependency for types and traits) -dimpl = { version = "0.4.3", default-features = false } +#dimpl = { version = "0.4.3", default-features = false } +dimpl = {git = "https://github.com/algesten/dimpl.git", default-features = false } time = "0.3" base64ct = "1" diff --git a/tests/handshake-direct.rs b/tests/handshake-direct.rs index 8d9c39212..6008daaf1 100644 --- a/tests/handshake-direct.rs +++ b/tests/handshake-direct.rs @@ -82,18 +82,24 @@ fn run_handshake_test(client_dtls: DtlsVersion, server_dtls: DtlsVersion) -> Res let server_crypto_name = std::env::var("R_CRYPTO").unwrap_or_else(|_| default_crypto_name().into()); - // wincrypto and openssl only support DTLS 1.2 — skip tests requiring 1.3/Auto. + // wincrypto only support DTLS 1.2 — skip tests requiring 1.3/Auto. + // openssl only supports DTLS 1.2 unless prefer_dimpl is enabled. // Also skip Auto client → 1.2-only server: dimpl advertises X25519 in the hybrid // ClientHello but its DTLS 1.2 engine can't process X25519 in ServerKeyExchange. - let dtls12_only = |name: &str| matches!(name, "wincrypto" | "openssl"); - let needs_13 = |v: DtlsVersion| matches!(v, DtlsVersion::Auto | DtlsVersion::Dtls13); + let dtls12_only = |name: &str| match name { + #[cfg(not(feature = "prefer_dimpl"))] + "wincrypto" => true, + #[cfg(not(feature = "prefer_dimpl"))] + "openssl" => true, + _ => false, + }; + let needs_13 = |v: DtlsVersion| matches!(v, DtlsVersion::Dtls13); if (dtls12_only(&client_crypto_name) && needs_13(client_dtls)) || (dtls12_only(&server_crypto_name) && needs_13(server_dtls)) - || (matches!(client_dtls, DtlsVersion::Auto) && dtls12_only(&server_crypto_name)) { println!( - "\n=== SKIPPED: client={} ({}), server={} ({}) — DTLS 1.3/Auto not supported ===", + "\n=== SKIPPED: client={} ({}), server={} ({}) — DTLS 1.3 not supported ===", client_dtls, client_crypto_name, server_dtls, server_crypto_name ); return Ok(());