libsoliton 0.1.3

Core cryptographic library for the LO protocol — hybrid post-quantum key exchange, signatures, ratchet, and storage encryption
Documentation
//! 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::<u8>()),
            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<u8> = (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());
            }
        }
    }
}