use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use zeroize::Zeroizing;
pub const SEED_LEN: usize = 32;
pub const PUBLIC_KEY_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SignError {
#[error("invalid seed length: expected {expected} bytes, got {actual}")]
BadSeedLength {
expected: usize,
actual: usize,
},
#[error("invalid {what} length: expected {expected} bytes, got {actual}")]
BadFieldLength {
what: &'static str,
expected: usize,
actual: usize,
},
}
pub fn seed_from_slice(bytes: &[u8]) -> Result<Zeroizing<[u8; SEED_LEN]>, SignError> {
let seed: [u8; SEED_LEN] = bytes.try_into().map_err(|_| SignError::BadSeedLength {
expected: SEED_LEN,
actual: bytes.len(),
})?;
Ok(Zeroizing::new(seed))
}
fn signing_key(seed: &Zeroizing<[u8; SEED_LEN]>) -> SigningKey {
SigningKey::from_bytes(seed)
}
#[must_use]
pub fn sign(seed: &Zeroizing<[u8; SEED_LEN]>, message: &[u8]) -> [u8; SIGNATURE_LEN] {
let key = signing_key(seed);
key.sign(message).to_bytes()
}
#[must_use]
pub fn public_from_seed(seed: &Zeroizing<[u8; SEED_LEN]>) -> [u8; PUBLIC_KEY_LEN] {
signing_key(seed).verifying_key().to_bytes()
}
pub fn public_from_slice(bytes: &[u8]) -> Result<[u8; PUBLIC_KEY_LEN], SignError> {
bytes.try_into().map_err(|_| SignError::BadFieldLength {
what: "public key",
expected: PUBLIC_KEY_LEN,
actual: bytes.len(),
})
}
pub fn verify(
public: &[u8; PUBLIC_KEY_LEN],
message: &[u8],
signature: &[u8],
) -> Result<bool, SignError> {
let sig_bytes: [u8; SIGNATURE_LEN] =
signature
.try_into()
.map_err(|_| SignError::BadFieldLength {
what: "signature",
expected: SIGNATURE_LEN,
actual: signature.len(),
})?;
let Ok(verifying_key) = VerifyingKey::from_bytes(public) else {
return Ok(false);
};
let sig = Signature::from_bytes(&sig_bytes);
Ok(verifying_key.verify(message, &sig).is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
const RFC8032_SEED: [u8; 32] = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c,
0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae,
0x7f, 0x60,
];
const RFC8032_PUBLIC: [u8; 32] = [
0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07,
0x3a, 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07,
0x51, 0x1a,
];
const RFC8032_SIG: [u8; 64] = [
0xe5, 0x56, 0x43, 0x00, 0xc3, 0x60, 0xac, 0x72, 0x90, 0x86, 0xe2, 0xcc, 0x80, 0x6e, 0x82,
0x8a, 0x84, 0x87, 0x7f, 0x1e, 0xb8, 0xe5, 0xd9, 0x74, 0xd8, 0x73, 0xe0, 0x65, 0x22, 0x49,
0x01, 0x55, 0x5f, 0xb8, 0x82, 0x15, 0x90, 0xa3, 0x3b, 0xac, 0xc6, 0x1e, 0x39, 0x70, 0x1c,
0xf9, 0xb4, 0x6b, 0xd2, 0x5b, 0xf5, 0xf0, 0x59, 0x5b, 0xbe, 0x24, 0x65, 0x51, 0x41, 0x43,
0x8e, 0x7a, 0x10, 0x0b,
];
#[test]
fn sign_matches_rfc8032_known_answer() {
let seed = Zeroizing::new(RFC8032_SEED);
assert_eq!(public_from_seed(&seed), RFC8032_PUBLIC);
assert_eq!(sign(&seed, b""), RFC8032_SIG);
}
#[test]
fn sign_then_verify_round_trips() {
let seed = Zeroizing::new([7u8; 32]);
let public = public_from_seed(&seed);
let message = b"materialize-to-sign payload";
let sig = sign(&seed, message);
assert_eq!(verify(&public, message, &sig), Ok(true));
}
#[test]
fn sign_is_deterministic() {
let seed = Zeroizing::new([3u8; 32]);
assert_eq!(sign(&seed, b"m"), sign(&seed, b"m"));
}
#[test]
fn public_from_seed_is_deterministic() {
let seed = Zeroizing::new([5u8; 32]);
assert_eq!(public_from_seed(&seed), public_from_seed(&seed));
}
#[test]
fn verify_rejects_wrong_key() {
let seed = Zeroizing::new([7u8; 32]);
let other_public = public_from_seed(&Zeroizing::new([42u8; 32]));
let sig = sign(&seed, b"payload");
assert_eq!(verify(&other_public, b"payload", &sig), Ok(false));
}
#[test]
fn verify_rejects_tampered_signature() {
let seed = Zeroizing::new([7u8; 32]);
let public = public_from_seed(&seed);
let mut sig = sign(&seed, b"payload");
sig[0] ^= 0xFF;
assert_eq!(verify(&public, b"payload", &sig), Ok(false));
}
#[test]
fn verify_rejects_tampered_message() {
let seed = Zeroizing::new([7u8; 32]);
let public = public_from_seed(&seed);
let sig = sign(&seed, b"payload");
assert_eq!(verify(&public, b"payload-tampered", &sig), Ok(false));
}
#[test]
fn verify_rejects_wrong_length_signature() {
let seed = Zeroizing::new([7u8; 32]);
let public = public_from_seed(&seed);
assert!(matches!(
verify(&public, b"m", &[0u8; 63]),
Err(SignError::BadFieldLength {
what: "signature",
..
})
));
assert!(matches!(
verify(&public, b"m", &[0u8; 65]),
Err(SignError::BadFieldLength { .. })
));
}
#[test]
fn verify_treats_bad_public_key_as_failed_not_an_oracle() {
let seed = Zeroizing::new([7u8; 32]);
let sig = sign(&seed, b"m");
assert_eq!(verify(&[0xFFu8; 32], b"m", &sig), Ok(false));
}
#[test]
fn seed_from_slice_rejects_wrong_length() {
assert!(matches!(
seed_from_slice(&[0u8; 31]),
Err(SignError::BadSeedLength { .. })
));
assert!(matches!(
seed_from_slice(&[0u8; 33]),
Err(SignError::BadSeedLength { .. })
));
assert!(seed_from_slice(&[0u8; 32]).is_ok());
}
#[test]
fn public_from_slice_validates_length_and_verifies() {
assert!(matches!(
public_from_slice(&[0u8; 31]),
Err(SignError::BadFieldLength {
what: "public key",
..
})
));
assert!(matches!(
public_from_slice(&[0u8; 33]),
Err(SignError::BadFieldLength { .. })
));
let seed = Zeroizing::new([7u8; 32]);
let public = public_from_seed(&seed);
let from_slice = public_from_slice(&public).expect("32-byte public");
assert_eq!(from_slice, public);
let sig = sign(&seed, b"from-out-of-band-public");
assert_eq!(
verify(&from_slice, b"from-out-of-band-public", &sig),
Ok(true)
);
}
#[test]
fn seed_from_slice_round_trips_through_sign() {
let stored = vec![0x11u8; 32];
let seed = seed_from_slice(&stored).expect("valid seed");
let public = public_from_seed(&seed);
let sig = sign(&seed, b"from-kv");
assert_eq!(verify(&public, b"from-kv", &sig), Ok(true));
}
}