//! Ed25519 signatures (RFC 8032, FIPS 186-5). //! //! Native Ed25519 signing/verification via `ed25519-dalek`. Unlike XEdDSA, //! this uses a dedicated Ed25519 keypair — the X25519 key inside X-Wing is //! not involved in signing. use crate::error::{Error, Result}; use zeroize::Zeroize; /// Ed25519 signature size: 64 bytes. pub const SIGNATURE_SIZE: usize = 64; /// Ed25519 public key size: 32 bytes. pub const PUBLIC_KEY_SIZE: usize = 32; /// Ed25519 secret key size: 32 bytes. pub const SECRET_KEY_SIZE: usize = 32; /// Generate an Ed25519 keypair from the OS CSPRNG. /// /// Generates 32 random bytes via `getrandom` and constructs the keypair /// deterministically from that seed. The `SigningKey` implements /// `ZeroizeOnDrop` — no manual cleanup needed. #[must_use = "dropping the keypair loses secret key material without zeroization"] pub fn keygen() -> (ed25519_dalek::VerifyingKey, ed25519_dalek::SigningKey) { // Generate 32 random bytes and use them as the Ed25519 secret key seed. // This matches ed25519-dalek's SigningKey::generate() but uses our // getrandom-based CSPRNG instead of requiring a rand_core::CryptoRngCore. let mut seed: [u8; 32] = super::random::random_array(); let sk = ed25519_dalek::SigningKey::from_bytes(&seed); let vk = sk.verifying_key(); // seed is [u8; 32] (Copy) — SigningKey::from_bytes copies it into the // SigningKey (which is ZeroizeOnDrop). Zeroize the original stack slot. seed.zeroize(); (vk, sk) } /// Sign a message with an Ed25519 secret key (RFC 8032). /// /// Deterministic: the same (sk, message) always produces the same signature /// (no random nonce, unlike XEdDSA). The nonce is derived from SHA-512 of the /// secret key's prefix and the message, per RFC 8032 §5.1.6. #[must_use = "signature must not be discarded"] pub fn sign(sk: &ed25519_dalek::SigningKey, message: &[u8]) -> [u8; 64] { use ed25519_dalek::Signer; sk.sign(message).to_bytes() } /// Verify an Ed25519 signature (RFC 8032, strict mode). /// /// Uses `verify_strict` which rejects non-canonical signatures and small-order /// public keys, preventing malleability attacks. pub fn verify(vk: &ed25519_dalek::VerifyingKey, message: &[u8], sig: &[u8; 64]) -> Result<()> { let signature = ed25519_dalek::Signature::from_bytes(sig); vk.verify_strict(message, &signature) .map_err(|_| Error::VerificationFailed) } #[cfg(test)] mod tests { use super::*; use crate::error::Error; use proptest::prelude::*; #[test] fn sign_verify_round_trip() { let (vk, sk) = keygen(); let msg = b"hello ed25519"; let sig = sign(&sk, msg); assert!(verify(&vk, msg, &sig).is_ok()); } #[test] fn sign_verify_different_messages() { let (vk, sk) = keygen(); let sig = sign(&sk, b"message one"); assert!(matches!( verify(&vk, b"message two", &sig), Err(Error::VerificationFailed) )); } #[test] fn sign_verify_different_keys() { let (_vk1, sk1) = keygen(); let (vk2, _sk2) = keygen(); let sig = sign(&sk1, b"test message"); assert!(matches!( verify(&vk2, b"test message", &sig), Err(Error::VerificationFailed) )); } #[test] fn signature_is_64_bytes() { let (_vk, sk) = keygen(); let sig = sign(&sk, b"size check"); assert!(sig.iter().any(|&b| b != 0)); } #[test] fn sign_is_deterministic() { let (_vk, sk) = keygen(); let msg = b"same message both times"; let sig1 = sign(&sk, msg); let sig2 = sign(&sk, msg); // Ed25519 is deterministic (RFC 8032) — unlike XEdDSA which uses random Z. assert_eq!(sig1, sig2); } #[test] fn verify_empty_message() { let (vk, sk) = keygen(); let msg: &[u8] = &[]; let sig = sign(&sk, msg); assert!(verify(&vk, msg, &sig).is_ok()); } #[test] fn verify_large_message() { let (vk, sk) = keygen(); let msg = vec![0xABu8; 65536]; // 64 KB let sig = sign(&sk, &msg); assert!(verify(&vk, &msg, &sig).is_ok()); } #[test] fn verify_rejects_non_canonical_s() { let (vk, sk) = keygen(); let msg = b"non-canonical S test"; let mut sig = sign(&sk, msg); assert!(verify(&vk, msg, &sig).is_ok()); // Replace S (bytes 32..64) with L, the Ed25519 curve order. let l_bytes: [u8; 32] = [ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ]; sig[32..64].copy_from_slice(&l_bytes); assert!(matches!( verify(&vk, msg, &sig), Err(Error::VerificationFailed) )); } #[test] fn regression_vector_rfc8032() { // RFC 8032 §7.1 Test Vector 1 (empty message). use hex_literal::hex; let sk_bytes = hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"); let pk_bytes = hex!("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); let expected_sig = hex!( "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155" "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" ); let sk = ed25519_dalek::SigningKey::from_bytes(&sk_bytes); let vk = sk.verifying_key(); // Verify the derived public key matches the RFC 8032 test vector. assert_eq!(vk.as_bytes(), &pk_bytes); let sig = sign(&sk, b""); assert_eq!(sig, expected_sig); assert!(verify(&vk, b"", &sig).is_ok()); } proptest! { #![proptest_config(proptest::prelude::ProptestConfig::with_cases(1000))] #[test] #[allow(clippy::cast_possible_truncation)] fn proptest_round_trip( sk_bytes in prop::array::uniform32(any::()), msg_len in 0..4096usize, flip_byte in 0..64usize, ) { let sk = ed25519_dalek::SigningKey::from_bytes(&sk_bytes); let vk = sk.verifying_key(); let msg: Vec = (0..msg_len).map(|i| (i % 256) as u8).collect(); let sig = sign(&sk, &msg); prop_assert!(verify(&vk, &msg, &sig).is_ok()); // Forgery rejection: mutated signature must fail verification. let mut bad_sig = sig; bad_sig[flip_byte] ^= 0x01; prop_assert!(verify(&vk, &msg, &bad_sig).is_err()); // Forgery rejection: mutated message must fail verification. if !msg.is_empty() { let mut bad_msg = msg.clone(); bad_msg[0] ^= 0x01; prop_assert!(verify(&vk, &bad_msg, &sig).is_err()); } } } }