#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use aws_lc_rs::agreement::{self, EphemeralPrivateKey, PrivateKey, UnparsedPublicKey, X25519};
use aws_lc_rs::encoding::{AsBigEndian, Curve25519SeedBin};
use aws_lc_rs::agreement::{ECDH_P256, ECDH_P384, ECDH_P521};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub const X25519_KEY_SIZE: usize = 32;
pub const P256_PUBLIC_KEY_SIZE: usize = 65;
pub const P256_SHARED_SECRET_SIZE: usize = 32;
pub const P384_PUBLIC_KEY_SIZE: usize = 97;
pub const P384_SHARED_SECRET_SIZE: usize = 48;
pub const P521_PUBLIC_KEY_SIZE: usize = 133;
pub const P521_SHARED_SECRET_SIZE: usize = 66;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EcdhCurve {
X25519,
P256,
P384,
P521,
}
impl EcdhCurve {
#[must_use]
pub const fn public_key_size(self) -> usize {
match self {
Self::X25519 => X25519_KEY_SIZE,
Self::P256 => P256_PUBLIC_KEY_SIZE,
Self::P384 => P384_PUBLIC_KEY_SIZE,
Self::P521 => P521_PUBLIC_KEY_SIZE,
}
}
#[must_use]
pub const fn shared_secret_size(self) -> usize {
match self {
Self::X25519 => X25519_KEY_SIZE,
Self::P256 => P256_SHARED_SECRET_SIZE,
Self::P384 => P384_SHARED_SECRET_SIZE,
Self::P521 => P521_SHARED_SECRET_SIZE,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::X25519 => "X25519",
Self::P256 => "P-256",
Self::P384 => "P-384",
Self::P521 => "P-521",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum EcdhError {
#[error("ECDH key generation failed")]
KeyGenerationFailed,
#[error("ECDH shared secret derivation failed")]
SharedSecretDerivationFailed,
#[error("Invalid key size: expected {expected}, got {actual}")]
InvalidKeySize {
expected: usize,
actual: usize,
},
#[error("ECDH key agreement failed")]
AgreementFailed,
#[error("Invalid public key: point validation failed for curve {curve}")]
InvalidPublicKey {
curve: &'static str,
},
#[error("Invalid point format: expected {expected}, got {actual}")]
InvalidPointFormat {
expected: &'static str,
actual: &'static str,
},
#[error("Invalid key data")]
InvalidKeyData,
#[error("Invalid key material: {0}")]
InvalidKeyMaterial(String),
#[error("Curve mismatch: expected {expected}, got {actual}")]
CurveMismatch {
expected: &'static str,
actual: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct X25519PublicKey {
bytes: [u8; X25519_KEY_SIZE],
}
const X25519_LOW_ORDER_POINTS: [[u8; X25519_KEY_SIZE]; 7] = [
[0x00; 32],
[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
[
0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,
0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,
0xb8, 0x00,
],
[
0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,
0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,
0x11, 0x57,
],
[
0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x7f,
],
[
0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x7f,
],
[
0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x7f,
],
];
impl X25519PublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EcdhError> {
use subtle::ConstantTimeEq;
if bytes.len() != X25519_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: X25519_KEY_SIZE,
actual: bytes.len(),
});
}
let mut is_low_order = subtle::Choice::from(0u8);
for point in &X25519_LOW_ORDER_POINTS {
is_low_order |= bytes.ct_eq(point);
}
if bool::from(is_low_order) {
return Err(EcdhError::InvalidKeyMaterial(
"X25519 public key is a low-order point (RFC 7748 §6.1)".to_string(),
));
}
let mut key_bytes = [0u8; X25519_KEY_SIZE];
key_bytes.copy_from_slice(bytes);
Ok(Self { bytes: key_bytes })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; X25519_KEY_SIZE] {
&self.bytes
}
#[must_use]
pub fn to_vec(&self) -> Vec<u8> {
self.bytes.to_vec()
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct X25519SecretKey {
bytes: [u8; X25519_KEY_SIZE],
}
impl X25519SecretKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EcdhError> {
if bytes.len() != X25519_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: X25519_KEY_SIZE,
actual: bytes.len(),
});
}
let mut key_bytes = [0u8; X25519_KEY_SIZE];
key_bytes.copy_from_slice(bytes);
Ok(Self { bytes: key_bytes })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; X25519_KEY_SIZE] {
&self.bytes
}
}
impl std::fmt::Debug for X25519SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("X25519SecretKey").field("bytes", &"[REDACTED]").finish()
}
}
impl subtle::ConstantTimeEq for X25519SecretKey {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.bytes.ct_eq(&other.bytes)
}
}
pub struct X25519KeyPair {
private: EphemeralPrivateKey,
public_bytes: [u8; X25519_KEY_SIZE],
}
impl X25519KeyPair {
pub fn generate() -> Result<Self, EcdhError> {
let rng = aws_lc_rs::rand::SystemRandom::new();
let private = EphemeralPrivateKey::generate(&X25519, &rng)
.map_err(|_e| EcdhError::KeyGenerationFailed)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
let mut public_bytes = [0u8; X25519_KEY_SIZE];
public_bytes.copy_from_slice(public.as_ref());
Ok(Self { private, public_bytes })
}
#[must_use]
pub fn public_key_bytes(&self) -> &[u8; X25519_KEY_SIZE] {
&self.public_bytes
}
#[must_use]
pub fn public_key(&self) -> X25519PublicKey {
X25519PublicKey { bytes: self.public_bytes }
}
pub fn agree(
self,
peer_public_bytes: &[u8],
) -> Result<Zeroizing<[u8; X25519_KEY_SIZE]>, EcdhError> {
let _validated_peer = X25519PublicKey::from_bytes(peer_public_bytes)?;
let peer_public = UnparsedPublicKey::new(&X25519, peer_public_bytes);
agreement::agree_ephemeral(
self.private,
peer_public,
EcdhError::AgreementFailed,
|shared_secret| {
let mut result = [0u8; X25519_KEY_SIZE];
result.copy_from_slice(shared_secret);
Ok(Zeroizing::new(result))
},
)
}
}
impl std::fmt::Debug for X25519KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("X25519KeyPair")
.field("public_bytes", &self.public_bytes)
.field("private", &"[REDACTED]")
.finish()
}
}
pub struct X25519StaticKeyPair {
private: PrivateKey,
public_bytes: [u8; X25519_KEY_SIZE],
}
impl X25519StaticKeyPair {
pub fn generate() -> Result<Self, EcdhError> {
let private = PrivateKey::generate(&X25519).map_err(|_e| EcdhError::KeyGenerationFailed)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
let mut public_bytes = [0u8; X25519_KEY_SIZE];
public_bytes.copy_from_slice(public.as_ref());
Ok(Self { private, public_bytes })
}
#[must_use]
pub fn public_key_bytes(&self) -> &[u8; X25519_KEY_SIZE] {
&self.public_bytes
}
#[must_use]
pub fn public_key(&self) -> X25519PublicKey {
X25519PublicKey { bytes: self.public_bytes }
}
pub fn seed_bytes(&self) -> Result<Zeroizing<[u8; X25519_KEY_SIZE]>, EcdhError> {
let seed: Curve25519SeedBin<'_> =
self.private.as_be_bytes().map_err(|_e| EcdhError::KeyGenerationFailed)?;
let mut bytes = [0u8; X25519_KEY_SIZE];
bytes.copy_from_slice(seed.as_ref());
Ok(Zeroizing::new(bytes))
}
pub fn from_seed_bytes(seed: &[u8; X25519_KEY_SIZE]) -> Result<Self, EcdhError> {
let private =
PrivateKey::from_private_key(&X25519, seed).map_err(|_e| EcdhError::InvalidKeyData)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
let mut public_bytes = [0u8; X25519_KEY_SIZE];
public_bytes.copy_from_slice(public.as_ref());
Ok(Self { private, public_bytes })
}
pub fn agree(
&self,
peer_public_bytes: &[u8],
) -> Result<Zeroizing<[u8; X25519_KEY_SIZE]>, EcdhError> {
let _validated_peer = X25519PublicKey::from_bytes(peer_public_bytes)?;
let peer_public = UnparsedPublicKey::new(&X25519, peer_public_bytes);
agreement::agree(&self.private, peer_public, EcdhError::AgreementFailed, |shared_secret| {
let mut result = [0u8; X25519_KEY_SIZE];
result.copy_from_slice(shared_secret);
Ok(Zeroizing::new(result))
})
}
}
impl std::fmt::Debug for X25519StaticKeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("X25519StaticKeyPair")
.field("public_bytes", &self.public_bytes)
.field("private", &"[REDACTED]")
.finish()
}
}
pub fn validate_public_key(public_key: &X25519PublicKey) -> Result<(), EcdhError> {
if public_key.as_bytes().len() != X25519_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: X25519_KEY_SIZE,
actual: public_key.as_bytes().len(),
});
}
Ok(())
}
pub fn validate_secret_key(secret_key: &X25519SecretKey) -> Result<(), EcdhError> {
if secret_key.as_bytes().len() != X25519_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: X25519_KEY_SIZE,
actual: secret_key.as_bytes().len(),
});
}
Ok(())
}
pub fn agree_ephemeral(
peer_public_bytes: &[u8],
) -> Result<(Zeroizing<[u8; X25519_KEY_SIZE]>, [u8; X25519_KEY_SIZE]), EcdhError> {
let keypair = X25519KeyPair::generate()?;
let our_public = *keypair.public_key_bytes();
let shared_secret = keypair.agree(peer_public_bytes)?;
Ok((shared_secret, our_public))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcdhP256PublicKey {
bytes: Vec<u8>,
}
impl EcdhP256PublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EcdhError> {
if bytes.len() != P256_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P256_PUBLIC_KEY_SIZE,
actual: bytes.len(),
});
}
if bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(Self { bytes: bytes.to_vec() })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn to_vec(&self) -> Vec<u8> {
self.bytes.clone()
}
pub fn validate(&self) -> Result<(), EcdhError> {
if self.bytes.len() != P256_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P256_PUBLIC_KEY_SIZE,
actual: self.bytes.len(),
});
}
if self.bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(())
}
}
pub struct EcdhP256KeyPair {
private: EphemeralPrivateKey,
public_bytes: Vec<u8>,
}
impl EcdhP256KeyPair {
pub fn generate() -> Result<Self, EcdhError> {
let rng = aws_lc_rs::rand::SystemRandom::new();
let private = EphemeralPrivateKey::generate(&ECDH_P256, &rng)
.map_err(|_e| EcdhError::KeyGenerationFailed)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
Ok(Self { private, public_bytes: public.as_ref().to_vec() })
}
#[must_use]
pub fn public_key_bytes(&self) -> &[u8] {
&self.public_bytes
}
pub fn public_key(&self) -> Result<EcdhP256PublicKey, EcdhError> {
EcdhP256PublicKey::from_bytes(&self.public_bytes)
}
pub fn agree(self, peer_public_bytes: &[u8]) -> Result<Zeroizing<Vec<u8>>, EcdhError> {
let peer_public = UnparsedPublicKey::new(&ECDH_P256, peer_public_bytes);
agreement::agree_ephemeral(
self.private,
peer_public,
EcdhError::AgreementFailed,
|shared_secret| Ok(Zeroizing::new(shared_secret.to_vec())),
)
}
}
impl std::fmt::Debug for EcdhP256KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EcdhP256KeyPair")
.field("public_bytes", &self.public_bytes)
.field("private", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcdhP384PublicKey {
bytes: Vec<u8>,
}
impl EcdhP384PublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EcdhError> {
if bytes.len() != P384_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P384_PUBLIC_KEY_SIZE,
actual: bytes.len(),
});
}
if bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(Self { bytes: bytes.to_vec() })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn to_vec(&self) -> Vec<u8> {
self.bytes.clone()
}
pub fn validate(&self) -> Result<(), EcdhError> {
if self.bytes.len() != P384_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P384_PUBLIC_KEY_SIZE,
actual: self.bytes.len(),
});
}
if self.bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(())
}
}
pub struct EcdhP384KeyPair {
private: EphemeralPrivateKey,
public_bytes: Vec<u8>,
}
impl EcdhP384KeyPair {
pub fn generate() -> Result<Self, EcdhError> {
let rng = aws_lc_rs::rand::SystemRandom::new();
let private = EphemeralPrivateKey::generate(&ECDH_P384, &rng)
.map_err(|_e| EcdhError::KeyGenerationFailed)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
Ok(Self { private, public_bytes: public.as_ref().to_vec() })
}
#[must_use]
pub fn public_key_bytes(&self) -> &[u8] {
&self.public_bytes
}
pub fn public_key(&self) -> Result<EcdhP384PublicKey, EcdhError> {
EcdhP384PublicKey::from_bytes(&self.public_bytes)
}
pub fn agree(self, peer_public_bytes: &[u8]) -> Result<Zeroizing<Vec<u8>>, EcdhError> {
let peer_public = UnparsedPublicKey::new(&ECDH_P384, peer_public_bytes);
agreement::agree_ephemeral(
self.private,
peer_public,
EcdhError::AgreementFailed,
|shared_secret| Ok(Zeroizing::new(shared_secret.to_vec())),
)
}
}
impl std::fmt::Debug for EcdhP384KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EcdhP384KeyPair")
.field("public_bytes", &self.public_bytes)
.field("private", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EcdhP521PublicKey {
bytes: Vec<u8>,
}
impl EcdhP521PublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EcdhError> {
if bytes.len() != P521_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P521_PUBLIC_KEY_SIZE,
actual: bytes.len(),
});
}
if bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(Self { bytes: bytes.to_vec() })
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn to_vec(&self) -> Vec<u8> {
self.bytes.clone()
}
pub fn validate(&self) -> Result<(), EcdhError> {
if self.bytes.len() != P521_PUBLIC_KEY_SIZE {
return Err(EcdhError::InvalidKeySize {
expected: P521_PUBLIC_KEY_SIZE,
actual: self.bytes.len(),
});
}
if self.bytes.first() != Some(&0x04) {
return Err(EcdhError::InvalidPointFormat {
expected: "uncompressed (0x04 prefix)",
actual: "invalid prefix",
});
}
Ok(())
}
}
pub struct EcdhP521KeyPair {
private: EphemeralPrivateKey,
public_bytes: Vec<u8>,
}
impl EcdhP521KeyPair {
pub fn generate() -> Result<Self, EcdhError> {
let rng = aws_lc_rs::rand::SystemRandom::new();
let private = EphemeralPrivateKey::generate(&ECDH_P521, &rng)
.map_err(|_e| EcdhError::KeyGenerationFailed)?;
let public = private.compute_public_key().map_err(|_e| EcdhError::KeyGenerationFailed)?;
Ok(Self { private, public_bytes: public.as_ref().to_vec() })
}
#[must_use]
pub fn public_key_bytes(&self) -> &[u8] {
&self.public_bytes
}
pub fn public_key(&self) -> Result<EcdhP521PublicKey, EcdhError> {
EcdhP521PublicKey::from_bytes(&self.public_bytes)
}
pub fn agree(self, peer_public_bytes: &[u8]) -> Result<Zeroizing<Vec<u8>>, EcdhError> {
let peer_public = UnparsedPublicKey::new(&ECDH_P521, peer_public_bytes);
agreement::agree_ephemeral(
self.private,
peer_public,
EcdhError::AgreementFailed,
|shared_secret| Ok(Zeroizing::new(shared_secret.to_vec())),
)
}
}
impl std::fmt::Debug for EcdhP521KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EcdhP521KeyPair")
.field("public_bytes", &self.public_bytes)
.field("private", &"[REDACTED]")
.finish()
}
}
pub fn agree_ephemeral_p256(
peer_public_bytes: &[u8],
) -> Result<(Zeroizing<Vec<u8>>, Vec<u8>), EcdhError> {
let keypair = EcdhP256KeyPair::generate()?;
let our_public = keypair.public_key_bytes().to_vec();
let shared_secret = keypair.agree(peer_public_bytes)?;
Ok((shared_secret, our_public))
}
pub fn agree_ephemeral_p384(
peer_public_bytes: &[u8],
) -> Result<(Zeroizing<Vec<u8>>, Vec<u8>), EcdhError> {
let keypair = EcdhP384KeyPair::generate()?;
let our_public = keypair.public_key_bytes().to_vec();
let shared_secret = keypair.agree(peer_public_bytes)?;
Ok((shared_secret, our_public))
}
pub fn agree_ephemeral_p521(
peer_public_bytes: &[u8],
) -> Result<(Zeroizing<Vec<u8>>, Vec<u8>), EcdhError> {
let keypair = EcdhP521KeyPair::generate()?;
let our_public = keypair.public_key_bytes().to_vec();
let shared_secret = keypair.agree(peer_public_bytes)?;
Ok((shared_secret, our_public))
}
pub fn validate_p256_public_key(public_key_bytes: &[u8]) -> Result<(), EcdhError> {
let pk = EcdhP256PublicKey::from_bytes(public_key_bytes)?;
pk.validate()
}
pub fn validate_p384_public_key(public_key_bytes: &[u8]) -> Result<(), EcdhError> {
let pk = EcdhP384PublicKey::from_bytes(public_key_bytes)?;
pk.validate()
}
pub fn validate_p521_public_key(public_key_bytes: &[u8]) -> Result<(), EcdhError> {
let pk = EcdhP521PublicKey::from_bytes(public_key_bytes)?;
pk.validate()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn test_ecdh_keypair_generation_succeeds() {
let keypair = X25519KeyPair::generate();
assert!(keypair.is_ok());
let keypair = keypair.unwrap();
assert_eq!(keypair.public_key_bytes().len(), X25519_KEY_SIZE);
}
#[test]
fn test_all_low_order_points_rejected_fails() {
assert_eq!(
X25519_LOW_ORDER_POINTS.len(),
7,
"RFC 7748 §6.1 canonical list (libsodium) has exactly 7 points",
);
for (idx, point) in X25519_LOW_ORDER_POINTS.iter().enumerate() {
let from_bytes_err = X25519PublicKey::from_bytes(point);
assert!(
matches!(from_bytes_err, Err(EcdhError::InvalidKeyMaterial(_))),
"from_bytes must reject low-order point #{idx}",
);
let kp = X25519KeyPair::generate().unwrap();
let agree_err = kp.agree(point);
assert!(
matches!(agree_err, Err(EcdhError::InvalidKeyMaterial(_))),
"agree() must reject low-order point #{idx}",
);
}
}
#[test]
fn test_ecdh_key_exchange_roundtrip() {
let keypair1 = X25519KeyPair::generate().unwrap();
let keypair2 = X25519KeyPair::generate().unwrap();
let pk1 = *keypair1.public_key_bytes();
let pk2 = *keypair2.public_key_bytes();
let ss1 = keypair1.agree(&pk2).unwrap();
let ss2 = keypair2.agree(&pk1).unwrap();
assert_eq!(ss1, ss2);
}
#[test]
fn test_public_key_from_bytes_succeeds() {
let bytes = [0x42u8; X25519_KEY_SIZE];
let pk = X25519PublicKey::from_bytes(&bytes).unwrap();
assert_eq!(pk.as_bytes(), &bytes);
}
#[test]
fn test_public_key_invalid_size_fails() {
let bytes = [0x42u8; 16]; let result = X25519PublicKey::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn test_secret_key_from_bytes_succeeds() {
let bytes = [0x42u8; X25519_KEY_SIZE];
let sk = X25519SecretKey::from_bytes(&bytes).unwrap();
assert_eq!(sk.as_bytes(), &bytes);
}
#[test]
fn test_validate_public_key_succeeds() {
let bytes = [0x42u8; X25519_KEY_SIZE];
let pk = X25519PublicKey::from_bytes(&bytes).unwrap();
assert!(validate_public_key(&pk).is_ok());
}
#[test]
fn test_validate_secret_key_succeeds() {
let bytes = [0x42u8; X25519_KEY_SIZE];
let sk = X25519SecretKey::from_bytes(&bytes).unwrap();
assert!(validate_secret_key(&sk).is_ok());
}
#[test]
fn test_agree_ephemeral_roundtrip() {
let keypair = X25519KeyPair::generate().unwrap();
let peer_public = *keypair.public_key_bytes();
let result = agree_ephemeral(&peer_public);
assert!(result.is_ok());
let (shared_secret, our_public) = result.unwrap();
assert_eq!(shared_secret.len(), X25519_KEY_SIZE);
assert_eq!(our_public.len(), X25519_KEY_SIZE);
}
#[test]
fn test_static_keypair_generation_succeeds() {
let kp = X25519StaticKeyPair::generate().unwrap();
assert_eq!(kp.public_key_bytes().len(), X25519_KEY_SIZE);
assert!(!kp.public_key_bytes().iter().all(|&b| b == 0));
}
#[test]
fn test_static_keypair_commutativity_roundtrip() {
let alice = X25519StaticKeyPair::generate().unwrap();
let bob = X25519StaticKeyPair::generate().unwrap();
let ss_ab = alice.agree(bob.public_key_bytes()).unwrap();
let ss_ba = bob.agree(alice.public_key_bytes()).unwrap();
assert_eq!(ss_ab, ss_ba, "DH commutativity must hold");
assert!(!ss_ab.iter().all(|&b| b == 0), "Shared secret must not be all zeros");
}
#[test]
fn test_static_keypair_reusable_succeeds() {
let alice = X25519StaticKeyPair::generate().unwrap();
let bob = X25519StaticKeyPair::generate().unwrap();
let carol = X25519StaticKeyPair::generate().unwrap();
let ss1 = alice.agree(bob.public_key_bytes()).unwrap();
let ss2 = alice.agree(carol.public_key_bytes()).unwrap();
let ss3 = alice.agree(bob.public_key_bytes()).unwrap();
assert_eq!(ss1, ss3, "agree() with same peer must produce same result");
assert_ne!(ss1, ss2, "Different peers must produce different shared secrets");
}
#[test]
fn test_static_keypair_public_key_succeeds() {
let kp = X25519StaticKeyPair::generate().unwrap();
let pk = kp.public_key();
assert_eq!(pk.as_bytes(), kp.public_key_bytes());
}
#[test]
fn test_static_keypair_different_keys_succeeds() {
let kp1 = X25519StaticKeyPair::generate().unwrap();
let kp2 = X25519StaticKeyPair::generate().unwrap();
assert_ne!(kp1.public_key_bytes(), kp2.public_key_bytes());
}
#[test]
fn test_static_keypair_seed_roundtrip_succeeds() {
let original = X25519StaticKeyPair::generate().unwrap();
let seed = original.seed_bytes().unwrap();
let restored = X25519StaticKeyPair::from_seed_bytes(&seed).unwrap();
assert_eq!(original.public_key_bytes(), restored.public_key_bytes());
}
#[test]
fn test_static_keypair_seed_roundtrip_agree_roundtrip() {
let alice = X25519StaticKeyPair::generate().unwrap();
let bob = X25519StaticKeyPair::generate().unwrap();
let ss_original = alice.agree(bob.public_key_bytes()).unwrap();
let seed = alice.seed_bytes().unwrap();
let alice_restored = X25519StaticKeyPair::from_seed_bytes(&seed).unwrap();
let ss_restored = alice_restored.agree(bob.public_key_bytes()).unwrap();
assert_eq!(ss_original, ss_restored);
}
#[test]
fn test_static_keypair_seed_not_zero_succeeds() {
let kp = X25519StaticKeyPair::generate().unwrap();
let seed = kp.seed_bytes().unwrap();
assert!(!seed.iter().all(|&b| b == 0));
}
#[test]
fn test_static_keypair_from_invalid_seed_fails() {
let zero_seed = [0u8; X25519_KEY_SIZE];
let result = X25519StaticKeyPair::from_seed_bytes(&zero_seed);
assert!(result.is_ok());
}
#[test]
fn test_ecdh_curve_public_key_size_passes_validation() {
assert_eq!(EcdhCurve::X25519.public_key_size(), X25519_KEY_SIZE);
assert_eq!(EcdhCurve::P256.public_key_size(), P256_PUBLIC_KEY_SIZE);
assert_eq!(EcdhCurve::P384.public_key_size(), P384_PUBLIC_KEY_SIZE);
assert_eq!(EcdhCurve::P521.public_key_size(), P521_PUBLIC_KEY_SIZE);
}
#[test]
fn test_ecdh_curve_shared_secret_size_passes_validation() {
assert_eq!(EcdhCurve::X25519.shared_secret_size(), X25519_KEY_SIZE);
assert_eq!(EcdhCurve::P256.shared_secret_size(), P256_SHARED_SECRET_SIZE);
assert_eq!(EcdhCurve::P384.shared_secret_size(), P384_SHARED_SECRET_SIZE);
assert_eq!(EcdhCurve::P521.shared_secret_size(), P521_SHARED_SECRET_SIZE);
}
#[test]
fn test_ecdh_curve_name_passes_validation() {
assert_eq!(EcdhCurve::X25519.name(), "X25519");
assert_eq!(EcdhCurve::P256.name(), "P-256");
assert_eq!(EcdhCurve::P384.name(), "P-384");
assert_eq!(EcdhCurve::P521.name(), "P-521");
}
#[test]
fn test_ecdh_curve_clone_eq_passes_validation() {
let c1 = EcdhCurve::P256;
let c2 = c1;
assert_eq!(c1, c2);
assert_ne!(EcdhCurve::P256, EcdhCurve::P384);
}
#[test]
fn test_x25519_public_key_to_vec_succeeds() {
let bytes = [0x42u8; X25519_KEY_SIZE];
let pk = X25519PublicKey::from_bytes(&bytes).unwrap();
assert_eq!(pk.to_vec(), bytes.to_vec());
}
#[test]
fn test_x25519_secret_key_invalid_size_fails() {
let result = X25519SecretKey::from_bytes(&[0u8; 16]);
assert!(matches!(result, Err(EcdhError::InvalidKeySize { expected: 32, actual: 16 })));
}
#[test]
fn test_x25519_secret_key_debug_redacted_passes_validation() {
let sk = X25519SecretKey::from_bytes(&[0xAA; X25519_KEY_SIZE]).unwrap();
let debug = format!("{:?}", sk);
assert!(debug.contains("REDACTED"));
assert!(!debug.contains("0xaa"));
}
#[test]
fn test_x25519_keypair_debug_passes_validation() {
let kp = X25519KeyPair::generate().unwrap();
let debug = format!("{:?}", kp);
assert!(debug.contains("X25519KeyPair"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_x25519_keypair_public_key_succeeds() {
let kp = X25519KeyPair::generate().unwrap();
let pk = kp.public_key();
assert_eq!(pk.as_bytes().len(), X25519_KEY_SIZE);
}
#[test]
fn test_x25519_static_keypair_debug_passes_validation() {
let kp = X25519StaticKeyPair::generate().unwrap();
let debug = format!("{:?}", kp);
assert!(debug.contains("X25519StaticKeyPair"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_p256_keypair_generation_succeeds() {
let kp = EcdhP256KeyPair::generate().unwrap();
assert_eq!(kp.public_key_bytes().len(), P256_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p256_keypair_public_key_succeeds() {
let kp = EcdhP256KeyPair::generate().unwrap();
let pk = kp.public_key().unwrap();
assert_eq!(pk.as_bytes().len(), P256_PUBLIC_KEY_SIZE);
assert_eq!(pk.to_vec().len(), P256_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p256_key_exchange_roundtrip() {
let alice = EcdhP256KeyPair::generate().unwrap();
let bob = EcdhP256KeyPair::generate().unwrap();
let alice_pk = alice.public_key_bytes().to_vec();
let bob_pk = bob.public_key_bytes().to_vec();
let ss_alice = alice.agree(&bob_pk).unwrap();
let ss_bob = bob.agree(&alice_pk).unwrap();
assert_eq!(ss_alice, ss_bob);
assert_eq!(ss_alice.len(), P256_SHARED_SECRET_SIZE);
}
#[test]
fn test_p256_public_key_from_bytes_valid_succeeds() {
let kp = EcdhP256KeyPair::generate().unwrap();
let pk_bytes = kp.public_key_bytes().to_vec();
let pk = EcdhP256PublicKey::from_bytes(&pk_bytes).unwrap();
assert_eq!(pk.as_bytes(), &pk_bytes);
assert!(pk.validate().is_ok());
}
#[test]
fn test_p256_public_key_wrong_size_fails() {
let result = EcdhP256PublicKey::from_bytes(&[0x04; 32]);
assert!(matches!(result, Err(EcdhError::InvalidKeySize { .. })));
}
#[test]
fn test_p256_public_key_wrong_prefix_fails() {
let mut bytes = vec![0x05; P256_PUBLIC_KEY_SIZE];
bytes[0] = 0x05; let result = EcdhP256PublicKey::from_bytes(&bytes);
assert!(matches!(result, Err(EcdhError::InvalidPointFormat { .. })));
}
#[test]
fn test_p256_keypair_debug_passes_validation() {
let kp = EcdhP256KeyPair::generate().unwrap();
let debug = format!("{:?}", kp);
assert!(debug.contains("EcdhP256KeyPair"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_agree_ephemeral_p256_roundtrip() {
let bob = EcdhP256KeyPair::generate().unwrap();
let bob_pk = bob.public_key_bytes().to_vec();
let (shared_secret, our_pk) = agree_ephemeral_p256(&bob_pk).unwrap();
assert_eq!(shared_secret.len(), P256_SHARED_SECRET_SIZE);
assert_eq!(our_pk.len(), P256_PUBLIC_KEY_SIZE);
}
#[test]
fn test_validate_p256_public_key_valid_succeeds() {
let kp = EcdhP256KeyPair::generate().unwrap();
assert!(validate_p256_public_key(kp.public_key_bytes()).is_ok());
}
#[test]
fn test_validate_p256_public_key_invalid_fails() {
assert!(validate_p256_public_key(&[0u8; 10]).is_err());
}
#[test]
fn test_p384_keypair_generation_succeeds() {
let kp = EcdhP384KeyPair::generate().unwrap();
assert_eq!(kp.public_key_bytes().len(), P384_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p384_keypair_public_key_succeeds() {
let kp = EcdhP384KeyPair::generate().unwrap();
let pk = kp.public_key().unwrap();
assert_eq!(pk.as_bytes().len(), P384_PUBLIC_KEY_SIZE);
assert_eq!(pk.to_vec().len(), P384_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p384_key_exchange_roundtrip() {
let alice = EcdhP384KeyPair::generate().unwrap();
let bob = EcdhP384KeyPair::generate().unwrap();
let alice_pk = alice.public_key_bytes().to_vec();
let bob_pk = bob.public_key_bytes().to_vec();
let ss_alice = alice.agree(&bob_pk).unwrap();
let ss_bob = bob.agree(&alice_pk).unwrap();
assert_eq!(ss_alice, ss_bob);
assert_eq!(ss_alice.len(), P384_SHARED_SECRET_SIZE);
}
#[test]
fn test_p384_public_key_from_bytes_valid_succeeds() {
let kp = EcdhP384KeyPair::generate().unwrap();
let pk_bytes = kp.public_key_bytes().to_vec();
let pk = EcdhP384PublicKey::from_bytes(&pk_bytes).unwrap();
assert_eq!(pk.as_bytes(), &pk_bytes);
assert!(pk.validate().is_ok());
}
#[test]
fn test_p384_public_key_wrong_size_fails() {
let result = EcdhP384PublicKey::from_bytes(&[0x04; 32]);
assert!(matches!(result, Err(EcdhError::InvalidKeySize { .. })));
}
#[test]
fn test_p384_public_key_wrong_prefix_fails() {
let mut bytes = vec![0x03; P384_PUBLIC_KEY_SIZE];
bytes[0] = 0x03;
let result = EcdhP384PublicKey::from_bytes(&bytes);
assert!(matches!(result, Err(EcdhError::InvalidPointFormat { .. })));
}
#[test]
fn test_p384_keypair_debug_passes_validation() {
let kp = EcdhP384KeyPair::generate().unwrap();
let debug = format!("{:?}", kp);
assert!(debug.contains("EcdhP384KeyPair"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_agree_ephemeral_p384_roundtrip() {
let bob = EcdhP384KeyPair::generate().unwrap();
let bob_pk = bob.public_key_bytes().to_vec();
let (shared_secret, our_pk) = agree_ephemeral_p384(&bob_pk).unwrap();
assert_eq!(shared_secret.len(), P384_SHARED_SECRET_SIZE);
assert_eq!(our_pk.len(), P384_PUBLIC_KEY_SIZE);
}
#[test]
fn test_validate_p384_public_key_valid_succeeds() {
let kp = EcdhP384KeyPair::generate().unwrap();
assert!(validate_p384_public_key(kp.public_key_bytes()).is_ok());
}
#[test]
fn test_validate_p384_public_key_invalid_fails() {
assert!(validate_p384_public_key(&[0u8; 10]).is_err());
}
#[test]
fn test_p521_keypair_generation_succeeds() {
let kp = EcdhP521KeyPair::generate().unwrap();
assert_eq!(kp.public_key_bytes().len(), P521_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p521_keypair_public_key_succeeds() {
let kp = EcdhP521KeyPair::generate().unwrap();
let pk = kp.public_key().unwrap();
assert_eq!(pk.as_bytes().len(), P521_PUBLIC_KEY_SIZE);
assert_eq!(pk.to_vec().len(), P521_PUBLIC_KEY_SIZE);
}
#[test]
fn test_p521_key_exchange_roundtrip() {
let alice = EcdhP521KeyPair::generate().unwrap();
let bob = EcdhP521KeyPair::generate().unwrap();
let alice_pk = alice.public_key_bytes().to_vec();
let bob_pk = bob.public_key_bytes().to_vec();
let ss_alice = alice.agree(&bob_pk).unwrap();
let ss_bob = bob.agree(&alice_pk).unwrap();
assert_eq!(ss_alice, ss_bob);
assert_eq!(ss_alice.len(), P521_SHARED_SECRET_SIZE);
}
#[test]
fn test_p521_public_key_from_bytes_valid_succeeds() {
let kp = EcdhP521KeyPair::generate().unwrap();
let pk_bytes = kp.public_key_bytes().to_vec();
let pk = EcdhP521PublicKey::from_bytes(&pk_bytes).unwrap();
assert_eq!(pk.as_bytes(), &pk_bytes);
assert!(pk.validate().is_ok());
}
#[test]
fn test_p521_public_key_wrong_size_fails() {
let result = EcdhP521PublicKey::from_bytes(&[0x04; 32]);
assert!(matches!(result, Err(EcdhError::InvalidKeySize { .. })));
}
#[test]
fn test_p521_public_key_wrong_prefix_fails() {
let mut bytes = vec![0x02; P521_PUBLIC_KEY_SIZE];
bytes[0] = 0x02;
let result = EcdhP521PublicKey::from_bytes(&bytes);
assert!(matches!(result, Err(EcdhError::InvalidPointFormat { .. })));
}
#[test]
fn test_p521_keypair_debug_passes_validation() {
let kp = EcdhP521KeyPair::generate().unwrap();
let debug = format!("{:?}", kp);
assert!(debug.contains("EcdhP521KeyPair"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_agree_ephemeral_p521_roundtrip() {
let bob = EcdhP521KeyPair::generate().unwrap();
let bob_pk = bob.public_key_bytes().to_vec();
let (shared_secret, our_pk) = agree_ephemeral_p521(&bob_pk).unwrap();
assert_eq!(shared_secret.len(), P521_SHARED_SECRET_SIZE);
assert_eq!(our_pk.len(), P521_PUBLIC_KEY_SIZE);
}
#[test]
fn test_validate_p521_public_key_valid_succeeds() {
let kp = EcdhP521KeyPair::generate().unwrap();
assert!(validate_p521_public_key(kp.public_key_bytes()).is_ok());
}
#[test]
fn test_validate_p521_public_key_invalid_fails() {
assert!(validate_p521_public_key(&[0u8; 10]).is_err());
}
#[test]
fn test_ecdh_error_display_passes_validation() {
let e = EcdhError::KeyGenerationFailed;
assert!(e.to_string().contains("key generation"));
let e = EcdhError::SharedSecretDerivationFailed;
assert!(e.to_string().contains("shared secret"));
let e = EcdhError::InvalidKeySize { expected: 32, actual: 16 };
assert!(e.to_string().contains("32"));
assert!(e.to_string().contains("16"));
let e = EcdhError::AgreementFailed;
assert!(e.to_string().contains("agreement"));
let e = EcdhError::InvalidPublicKey { curve: "P-256" };
assert!(e.to_string().contains("P-256"));
let e = EcdhError::InvalidPointFormat { expected: "uncompressed", actual: "compressed" };
assert!(e.to_string().contains("uncompressed"));
let e = EcdhError::InvalidKeyData;
assert!(e.to_string().contains("Invalid key data"));
let e = EcdhError::CurveMismatch { expected: "P-256", actual: "P-384" };
assert!(e.to_string().contains("P-256"));
assert!(e.to_string().contains("P-384"));
}
}