krypteia-arcana 0.1.0

Pure-Rust classical cryptographic primitives: RSA (PKCS#1 v1.5, OAEP), ECC (NIST P-256/384/521, secp256k1), ECDSA, EdDSA (Ed25519), X25519, AES (128/192/256, GCM/CBC), DES/3DES, SHA-1/2/3, HMAC. Side-channel-aware (Montgomery ladder, branchless point_add_ct). Targets embedded (no_std), STM32 M0/M4/M33, ESP32-C3 RISC-V. Zero runtime dependencies.
Documentation
//! Elliptic Curve Diffie-Hellman (ECDH) on short Weierstrass curves.
//!
//! ECDH is exposed as a method on the [`Curve`](super::curves::Curve) trait,
//! alongside ECDSA sign / verify and keygen. The same seven unit structs cover
//! all curve operations:
//!
//! ```ignore
//! use arcana::ecc::curves::{Curve, P256};
//!
//! let mut rng = OsRng;
//! let (alice_pk, alice_sk) = P256::keygen(&mut rng);
//! let (bob_pk,   bob_sk)   = P256::keygen(&mut rng);
//!
//! let secret_a = P256::ecdh(&alice_sk, &bob_pk).expect("alice ecdh");
//! let secret_b = P256::ecdh(&bob_sk,   &alice_pk).expect("bob ecdh");
//! assert_eq!(secret_a, secret_b);
//! ```
//!
//! # Supported curves
//!
//! All seven [`Curve`](super::curves::Curve) implementors:
//! [`P256`](super::curves::P256), [`P384`](super::curves::P384),
//! [`P521`](super::curves::P521),
//! [`Secp256k1`](super::curves::Secp256k1),
//! [`BrainpoolP256r1`](super::curves::BrainpoolP256r1),
//! [`BrainpoolP384r1`](super::curves::BrainpoolP384r1),
//! [`BrainpoolP512r1`](super::curves::BrainpoolP512r1).
//!
//! # Output format
//!
//! [`Curve::ecdh`](super::curves::Curve::ecdh) returns the **raw X coordinate**
//! of the shared point, encoded as `LIMBS * 8` big-endian bytes. This matches
//! NIST SP 800-56A §5.7.1.2 ("ECC CDH Primitive") and the TLS / IKE
//! conventions. Higher-level KDFs (HKDF, X9.63 KDF, ...) are out of scope
//! for this layer.
//!
//! # Public key validation (mandatory)
//!
//! Before multiplying the secret scalar by a peer's public key, `ecdh`
//! validates that the peer's point is actually on the curve. **Skipping
//! this check enables invalid-curve attacks** that recover bits of the
//! secret key one chosen point at a time, so it is non-negotiable here.
//! See [`super::curve::is_on_curve`]. The same internal entry point
//! is used by [`Curve::verify`](super::curves::Curve::verify), so the
//! validation rules cannot drift between ECDH and ECDSA verify.
//!
//! Validation performed on every `ecdh` call:
//! 1. SEC1 uncompressed format: `pk.bytes.len() == 1 + 2*LIMBS*8`
//! 2. Format byte: `pk.bytes[0] == 0x04`
//! 3. Coordinates are field elements in `[0, p)` (implicit via decoding)
//! 4. Point satisfies `y² = x³ + a·x + b mod p`
//! 5. Resulting shared point is not the point at infinity (small-subgroup
//!    defence in depth)
//!
//! # Test-only file
//!
//! The actual implementation lives next to the other LIMBS-generic curve
//! helpers in [`super::ecdsa`] (`ecdh_internal<LIMBS>`) and is dispatched
//! through the [`Curve`](super::curves::Curve) trait in [`super::curves`].
//! This file exists to host the ECDH-specific documentation and the
//! integration tests for ECDH; there is no public API defined here.

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::super::curves::{
        BrainpoolP256r1, BrainpoolP384r1, BrainpoolP512r1, CryptoRng, Curve, P256, P384, PublicKey, Secp256k1,
        SecretKey,
    };

    /// Tiny deterministic xorshift64 RNG, for tests only. NOT cryptographic.
    struct TestRng {
        state: u64,
    }

    impl TestRng {
        fn new(seed: u64) -> Self {
            Self {
                state: if seed == 0 { 0xdeadbeefcafef00d } else { seed },
            }
        }
    }

    impl CryptoRng for TestRng {
        fn fill_bytes(&mut self, dest: &mut [u8]) {
            for chunk in dest.chunks_mut(8) {
                let mut x = self.state;
                x ^= x << 13;
                x ^= x >> 7;
                x ^= x << 17;
                self.state = x;
                for (i, b) in chunk.iter_mut().enumerate() {
                    *b = (x >> (8 * i)) as u8;
                }
            }
        }
    }

    /// Generic round-trip on any curve: generate Alice and Bob keypairs,
    /// each derives the shared secret, the two outputs must match exactly.
    ///
    /// Generic over `C: Curve` so the same body is reused for all six
    /// curves. Tests just call `roundtrip::<P256>()` etc.
    fn roundtrip<C: Curve>() {
        let mut rng_a = TestRng::new(0xA11CE);
        let mut rng_b = TestRng::new(0xB0B);
        let (pk_a, sk_a) = C::keygen(&mut rng_a);
        let (pk_b, sk_b) = C::keygen(&mut rng_b);

        let s_ab = C::ecdh(&sk_a, &pk_b).expect("alice ecdh ok");
        let s_ba = C::ecdh(&sk_b, &pk_a).expect("bob ecdh ok");

        assert_eq!(s_ab, s_ba, "ECDH round-trip mismatch");
        // Sanity: shared secret is non-zero (the chance of a true zero is
        // negligible, so a zero output here means a bug).
        assert!(s_ab.iter().any(|&b| b != 0), "shared secret is all-zero");
    }

    #[test]
    fn ecdh_p256_roundtrip() {
        roundtrip::<P256>();
    }

    #[test]
    fn ecdh_p384_roundtrip() {
        roundtrip::<P384>();
    }

    #[test]
    fn ecdh_secp256k1_roundtrip() {
        roundtrip::<Secp256k1>();
    }

    #[test]
    fn ecdh_brainpoolp256r1_roundtrip() {
        roundtrip::<BrainpoolP256r1>();
    }

    #[test]
    fn ecdh_brainpoolp384r1_roundtrip() {
        roundtrip::<BrainpoolP384r1>();
    }

    #[test]
    fn ecdh_brainpoolp512r1_roundtrip() {
        roundtrip::<BrainpoolP512r1>();
    }

    /// The shared secret has the expected width (LIMBS*8 BE bytes).
    #[test]
    fn ecdh_shared_secret_widths() {
        let mut rng = TestRng::new(1);
        let (pk_a, _sk_a) = P256::keygen(&mut rng);
        let (_pk_b, sk_b) = P256::keygen(&mut rng);
        let s = P256::ecdh(&sk_b, &pk_a).unwrap();
        assert_eq!(s.len(), 32);

        let (pk_a, _sk_a) = P384::keygen(&mut rng);
        let (_pk_b, sk_b) = P384::keygen(&mut rng);
        let s = P384::ecdh(&sk_b, &pk_a).unwrap();
        assert_eq!(s.len(), 48);

        let (pk_a, _sk_a) = BrainpoolP512r1::keygen(&mut rng);
        let (_pk_b, sk_b) = BrainpoolP512r1::keygen(&mut rng);
        let s = BrainpoolP512r1::ecdh(&sk_b, &pk_a).unwrap();
        assert_eq!(s.len(), 64);
    }

    /// ECDH and ECDSA share key pairs: a key produced by `P256::keygen`
    /// works as input to both `P256::ecdh` and `P256::sign_rfc6979` /
    /// `P256::verify`. This test locks in that interchangeability so a
    /// future change cannot accidentally split the key types.
    #[test]
    fn ecdh_and_ecdsa_share_keys() {
        use crate::hash::sha256::Sha256;

        let mut rng = TestRng::new(2);
        let (alice_pk, alice_sk) = P256::keygen(&mut rng);
        let (bob_pk, bob_sk) = P256::keygen(&mut rng);

        // ECDH on the shared key pair.
        let shared_a = P256::ecdh(&alice_sk, &bob_pk).expect("alice ecdh");
        let shared_b = P256::ecdh(&bob_sk, &alice_pk).expect("bob ecdh");
        assert_eq!(shared_a, shared_b);

        // Sign with the *same* sk that just did ECDH, verify with the *same*
        // pk. If the key types had drifted between ECDH and ECDSA, one of
        // the calls below would not type-check.
        let msg = b"keys are the same";
        let sig = P256::sign_rfc6979_msg::<Sha256>(&alice_sk, msg);
        assert!(P256::verify_msg::<Sha256>(&alice_pk, msg, &sig));
    }

    /// `ecdh` must reject a malformed (wrong length) public key.
    #[test]
    fn ecdh_rejects_wrong_length_pubkey() {
        let mut rng = TestRng::new(7);
        let (_pk, sk) = P256::keygen(&mut rng);
        let bad = PublicKey { bytes: vec![0x04; 64] }; // 64 instead of 65
        assert!(P256::ecdh(&sk, &bad).is_none());
    }

    /// `ecdh` must reject a public key with the wrong tag byte.
    #[test]
    fn ecdh_rejects_wrong_tag_pubkey() {
        let mut rng = TestRng::new(8);
        let (mut pk, sk) = P256::keygen(&mut rng);
        pk.bytes[0] = 0x02; // compressed-format tag, we only support 0x04
        assert!(P256::ecdh(&sk, &pk).is_none());
    }

    /// `ecdh` must reject an off-curve public key (the invalid-curve attack
    /// defence). We construct one by flipping a single byte of Y.
    #[test]
    fn ecdh_rejects_off_curve_pubkey() {
        let mut rng = TestRng::new(9);
        let (mut pk, sk) = P256::keygen(&mut rng);
        let len = pk.bytes.len();
        pk.bytes[len - 1] ^= 0x01;
        assert!(
            P256::ecdh(&sk, &pk).is_none(),
            "off-curve point must be rejected by ECDH"
        );
    }

    /// `ecdh` must reject the encoded point at infinity (encoded as
    /// `0x04 || 0...0 || 0...0`). It is off-curve since `0 != 0 + 0 + b`
    /// for non-zero `b`.
    #[test]
    fn ecdh_rejects_infinity_pubkey() {
        let mut rng = TestRng::new(10);
        let (_pk, sk) = P256::keygen(&mut rng);
        let mut bytes = vec![0u8; 65];
        bytes[0] = 0x04;
        let pk = PublicKey { bytes };
        assert!(P256::ecdh(&sk, &pk).is_none());
    }

    /// `ecdh` must reject a syntactically valid PK whose secret scalar is
    /// invalid (e.g. our SecretKey is all zeros, which encodes d=0).
    #[test]
    fn ecdh_rejects_zero_secret() {
        let mut rng = TestRng::new(11);
        let (pk, _sk_real) = P256::keygen(&mut rng);
        let sk_zero = SecretKey { bytes: vec![0u8; 32] };
        assert!(P256::ecdh(&sk_zero, &pk).is_none());
    }
}