From 49cc32b79d6091fa88e6230f8c705cb604af3a73 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 15 Nov 2023 12:02:39 -0500 Subject: [PATCH] Add domain restricted sign/verify to ed25519 interface, modeled after p384 version. --- src/p384.rs | 23 +++++++++++++---------- src/x25519.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/p384.rs b/src/p384.rs index edbe086..d3f3763 100644 --- a/src/p384.rs +++ b/src/p384.rs @@ -70,7 +70,7 @@ pub struct P384PublicKey { unsafe impl Send for P384PublicKey {} unsafe impl Sync for P384PublicKey {} -fn create_digest(domain: &[u8], data: &[&[u8]]) -> [u8; SHA384_HASH_SIZE] { +fn create_domain_restricted_digest(domain: &[u8], data: &[&[u8]]) -> [u8; SHA384_HASH_SIZE] { debug_assert!(domain.len() <= u16::MAX as usize); let mut hasher = SHA384::new(); for msg in data { @@ -149,7 +149,7 @@ impl P384PublicKey { mem::forget(r); mem::forget(s); // Digest the message. - let digest = create_digest(domain, data); + let digest = create_domain_restricted_digest(domain, data); let key = self.key.lock().unwrap(); // Actually perform the verification. @@ -301,7 +301,7 @@ impl P384KeyPair { /// purpose the signer did not intend. #[allow(unused_assignments)] pub fn sign_all(&self, domain: &[u8], data: &[&[u8]]) -> [u8; P384_ECDSA_SIGNATURE_SIZE] { - let digest = create_digest(domain, data); + let digest = create_domain_restricted_digest(domain, data); unsafe { let keypair = self.pair.lock().unwrap(); // Actually create the signature with ECDSA. @@ -355,13 +355,16 @@ impl P384KeyPair { let other_key = other_public.key.lock().unwrap(); unsafe { // Ask OpenSSL to perform DH between the keypair and the other key's public key object. - assert_eq!(ECDH_compute_key( - output.as_mut_ptr(), - P384_ECDH_SHARED_SECRET_SIZE as c_ulong, - ffi::EC_KEY_get0_public_key(other_key.0), - keypair.0, - ptr::null(), - ), P384_ECDH_SHARED_SECRET_SIZE as c_int) + assert_eq!( + ECDH_compute_key( + output.as_mut_ptr(), + P384_ECDH_SHARED_SECRET_SIZE as c_ulong, + ffi::EC_KEY_get0_public_key(other_key.0), + keypair.0, + ptr::null(), + ), + P384_ECDH_SHARED_SECRET_SIZE as c_int + ) } } } diff --git a/src/x25519.rs b/src/x25519.rs index 8d30a1e..d8ab156 100644 --- a/src/x25519.rs +++ b/src/x25519.rs @@ -19,6 +19,18 @@ pub const ED25519_PUBLIC_KEY_SIZE: usize = 32; pub const ED25519_SECRET_KEY_SIZE: usize = 32; pub const ED25519_SIGNATURE_SIZE: usize = 64; +fn create_domain_restricted_digest(domain: &[u8], data: &[u8]) -> ed25519_dalek::Sha512 { + debug_assert!(domain.len() <= u16::MAX as usize); + let mut hasher = ed25519_dalek::Sha512::new(); + hasher.update(data); + // We hash the domain last to mitigate some of the weaknesses of merkle-damgard. + if domain.len() > 0 { + hasher.update(domain); + hasher.update(&(domain.len() as u16).to_be_bytes()); + } + hasher +} + /// Curve25519 key pair for ECDH key agreement. #[derive(Clone)] pub struct X25519KeyPair(x25519_dalek::StaticSecret, x25519_dalek::PublicKey); @@ -115,14 +127,23 @@ impl Ed25519KeyPair { *output = self.0.to_bytes(); } + /// Sign a hash of the message (no domain parameter). pub fn sign(&self, msg: &[u8]) -> [u8; ED25519_SIGNATURE_SIZE] { let mut h = ed25519_dalek::Sha512::new(); let _ = h.update(msg); - self.0.sign_prehashed(h.clone(), None).unwrap().to_bytes() + self.0.sign_prehashed(h, None).unwrap().to_bytes() + } + + /// Sign a hash of the message that includes a domain parameter. + pub fn sign_domain_restricted(&self, domain: &[u8], msg: &[u8]) -> [u8; ED25519_SIGNATURE_SIZE] { + self.0 + .sign_prehashed(create_domain_restricted_digest(domain, msg), None) + .unwrap() + .to_bytes() } /// Create a signature with the first 32 bytes of the SHA512 hash appended. - /// ZeroTier does this for legacy reasons, but it's ignored in newer versions. + /// This is for legacy ZeroTier V1 compatibility requirements. It's not used in newer code. pub fn sign_zt(&self, msg: &[u8]) -> [u8; 96] { let mut h = ed25519_dalek::Sha512::new(); let _ = h.update(msg); @@ -136,6 +157,7 @@ impl Ed25519KeyPair { } } +/// Verify a non-domain-restricted signature. #[must_use] pub fn ed25519_verify(public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], signature: &[u8], msg: &[u8]) -> bool { if signature.len() >= 64 { @@ -150,3 +172,26 @@ pub fn ed25519_verify(public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], signature: &[u false } } + +/// Verify a domain restricted signature. +#[must_use] +pub fn ed25519_verify_domain_restricted( + public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], + signature: &[u8], + domain: &[u8], + msg: &[u8], +) -> bool { + if signature.len() >= 64 { + ed25519_dalek::VerifyingKey::from_bytes(public_key.try_into().unwrap()).map_or(false, |pk| { + let sig: [u8; 64] = signature[0..64].try_into().unwrap(); + pk.verify_prehashed( + create_domain_restricted_digest(domain, msg), + None, + &ed25519_dalek::Signature::from(sig), + ) + .is_ok() + }) + } else { + false + } +}