#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
use alloc::vec::Vec;
use ring::agreement::{self, ECDH_P256, EphemeralPrivateKey, PublicKey, X25519};
use ring::hkdf;
use ring::rand::SystemRandom;
use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KxSuite {
X25519,
EcdhP256,
}
impl Default for KxSuite {
fn default() -> Self {
Self::X25519
}
}
impl KxSuite {
#[must_use]
pub const fn public_key_len(self) -> usize {
match self {
Self::X25519 => 32,
Self::EcdhP256 => 65,
}
}
fn algorithm(self) -> &'static agreement::Algorithm {
match self {
Self::X25519 => &X25519,
Self::EcdhP256 => &ECDH_P256,
}
}
}
pub struct KeyExchange {
suite: KxSuite,
private: EphemeralPrivateKey,
public: PublicKey,
}
impl KeyExchange {
pub fn new() -> SecurityResult<Self> {
Self::with_suite(KxSuite::X25519)
}
pub fn with_suite(suite: KxSuite) -> SecurityResult<Self> {
let rng = SystemRandom::new();
let private = EphemeralPrivateKey::generate(suite.algorithm(), &rng).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"keyexchange: ephemeral-key generation failed",
)
})?;
let public = private.compute_public_key().map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"keyexchange: public-key derivation failed",
)
})?;
Ok(Self {
suite,
private,
public,
})
}
#[must_use]
pub fn suite(&self) -> KxSuite {
self.suite
}
#[must_use]
pub fn public_key(&self) -> &[u8] {
self.public.as_ref()
}
pub fn derive_shared_secret(self, remote_public_key: &[u8]) -> SecurityResult<Vec<u8>> {
if remote_public_key.len() != self.suite.public_key_len() {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
alloc::format!(
"keyexchange: {:?} public-key muss {} byte sein",
self.suite,
self.suite.public_key_len()
),
));
}
let peer = agreement::UnparsedPublicKey::new(self.suite.algorithm(), remote_public_key);
agreement::agree_ephemeral(self.private, &peer, |raw_dh| {
let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, b"zerodds-security-v1/shared-secret");
let prk = salt.extract(raw_dh);
let info_parts = [b"DDS:Auth:PKI-DH:secret".as_slice()];
let okm = prk.expand(&info_parts, hkdf::HKDF_SHA256).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"keyexchange: HKDF expand failed",
)
})?;
let mut out = [0u8; 32];
okm.fill(&mut out).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"keyexchange: HKDF fill failed",
)
})?;
Ok(out.to_vec())
})
.map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"keyexchange: DH agreement rejected (invalid peer key?)",
)
})
.and_then(|r| r)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn public_key_is_32_bytes() {
let kx = KeyExchange::new().unwrap();
assert_eq!(kx.public_key().len(), 32);
}
#[test]
fn two_parties_derive_identical_secret() {
let alice = KeyExchange::new().unwrap();
let bob = KeyExchange::new().unwrap();
let a_pub = alice.public_key().to_vec();
let b_pub = bob.public_key().to_vec();
let s1 = alice.derive_shared_secret(&b_pub).unwrap();
let s2 = bob.derive_shared_secret(&a_pub).unwrap();
assert_eq!(s1.len(), 32);
assert_eq!(s1, s2, "alice + bob muessen identisches secret ableiten");
}
#[test]
fn different_pairs_produce_different_secrets() {
let alice = KeyExchange::new().unwrap();
let bob = KeyExchange::new().unwrap();
let b_pub = bob.public_key().to_vec();
let s1 = alice.derive_shared_secret(&b_pub).unwrap();
let alice2 = KeyExchange::new().unwrap();
let bob2 = KeyExchange::new().unwrap();
let b2_pub = bob2.public_key().to_vec();
let s2 = alice2.derive_shared_secret(&b2_pub).unwrap();
assert_ne!(s1, s2, "andere ephemerals → anderes secret (PFS)");
}
#[test]
fn wrong_length_public_key_rejected() {
let alice = KeyExchange::new().unwrap();
let err = alice.derive_shared_secret(&[0u8; 16]).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::BadArgument);
}
#[test]
fn zero_public_key_rejected_by_ring() {
let alice = KeyExchange::new().unwrap();
let err = alice.derive_shared_secret(&[0u8; 32]).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn default_suite_is_x25519() {
let kx = KeyExchange::new().unwrap();
assert_eq!(kx.suite(), KxSuite::X25519);
assert_eq!(kx.public_key().len(), 32);
}
#[test]
fn p256_public_key_is_65_bytes() {
let kx = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
assert_eq!(kx.suite(), KxSuite::EcdhP256);
assert_eq!(kx.public_key().len(), 65);
assert_eq!(kx.public_key()[0], 0x04);
}
#[test]
fn p256_two_parties_derive_identical_secret() {
let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
let bob = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
let a_pub = alice.public_key().to_vec();
let b_pub = bob.public_key().to_vec();
let s1 = alice.derive_shared_secret(&b_pub).unwrap();
let s2 = bob.derive_shared_secret(&a_pub).unwrap();
assert_eq!(s1.len(), 32);
assert_eq!(s1, s2);
}
#[test]
fn p256_rejects_wrong_length_public_key() {
let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
let err = alice.derive_shared_secret(&[0u8; 32]).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::BadArgument);
}
#[test]
fn p256_rejects_off_curve_point() {
let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
let mut bogus = [0u8; 65];
bogus[0] = 0x04;
let err = alice.derive_shared_secret(&bogus).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn x25519_and_p256_produce_different_public_key_lengths() {
let a = KeyExchange::new().unwrap();
let b = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
assert_ne!(a.public_key().len(), b.public_key().len());
}
}