Skip to main content

oxicrypto_kex/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Pure Rust key-exchange implementations for the OxiCrypto stack.
4//!
5//! Provides a [`KeyAgreement`]-trait wrapper for X25519 Diffie-Hellman,
6//! X448 Diffie-Hellman, ECDH over NIST P-256, P-384, and P-521, plus key
7//! generation helpers.
8//!
9//! # Key generation
10//!
11//! All `generate_keypair` functions accept any RNG implementing
12//! [`rand_core::TryCryptoRng`] (rand_core 0.10+).
13//!
14//! ## Shared-secret rejection
15//!
16//! Every `agree()` implementation rejects all-zero shared secrets via
17//! constant-time comparison.  An all-zero output indicates a low-order
18//! (small subgroup) public key attack; callers will receive
19//! [`CryptoError::Kex`] in that case.
20
21use oxicrypto_core::{CryptoError, KeyAgreement, SecretKey, SecretVec};
22use p256::elliptic_curve::Generate;
23use x25519_dalek::{PublicKey, StaticSecret};
24
25pub mod hpke;
26
27// ── Type-safe public key wrappers ─────────────────────────────────────────────
28
29/// A type-safe 32-byte X25519 public key.
30///
31/// Wraps the raw Montgomery-form u-coordinate used by the X25519 function
32/// (RFC 7748 §6.1).  Use [`as_bytes`](X25519PublicKey::as_bytes) to access
33/// the inner bytes for [`KeyAgreement::agree`].
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct X25519PublicKey(pub [u8; 32]);
36
37impl X25519PublicKey {
38    /// Borrow the 32-byte public key.
39    #[must_use]
40    pub fn as_bytes(&self) -> &[u8; 32] {
41        &self.0
42    }
43
44    /// Consume the wrapper and return the inner 32-byte array.
45    #[must_use]
46    pub fn to_bytes(self) -> [u8; 32] {
47        self.0
48    }
49}
50
51impl From<[u8; 32]> for X25519PublicKey {
52    fn from(bytes: [u8; 32]) -> Self {
53        Self(bytes)
54    }
55}
56
57impl AsRef<[u8]> for X25519PublicKey {
58    fn as_ref(&self) -> &[u8] {
59        &self.0
60    }
61}
62
63/// A type-safe 56-byte X448 public key.
64///
65/// Wraps the raw Montgomery-form u-coordinate used by the X448 function
66/// (RFC 7748 §6.2).  Use [`as_bytes`](X448PublicKey::as_bytes) to access
67/// the inner bytes for [`KeyAgreement::agree`].
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct X448PublicKey(pub [u8; 56]);
70
71impl X448PublicKey {
72    /// Borrow the 56-byte public key.
73    #[must_use]
74    pub fn as_bytes(&self) -> &[u8; 56] {
75        &self.0
76    }
77
78    /// Consume the wrapper and return the inner 56-byte array.
79    #[must_use]
80    pub fn to_bytes(self) -> [u8; 56] {
81        self.0
82    }
83}
84
85impl From<[u8; 56]> for X448PublicKey {
86    fn from(bytes: [u8; 56]) -> Self {
87        Self(bytes)
88    }
89}
90
91impl AsRef<[u8]> for X448PublicKey {
92    fn as_ref(&self) -> &[u8] {
93        &self.0
94    }
95}
96
97// ── X25519 ────────────────────────────────────────────────────────────────────
98
99/// X25519 Diffie-Hellman key agreement.
100///
101/// `agree(my_secret_bytes, their_public_bytes, shared_out)`:
102/// - `my_secret_bytes` — 32-byte static secret scalar
103/// - `their_public_bytes` — 32-byte public point
104/// - `shared_out` — must be at least 32 bytes; receives the shared secret
105#[derive(Debug, Default, Clone, Copy)]
106pub struct X25519;
107
108impl KeyAgreement for X25519 {
109    fn name(&self) -> &'static str {
110        "X25519"
111    }
112    fn scalar_len(&self) -> usize {
113        32
114    }
115    fn point_len(&self) -> usize {
116        32
117    }
118    fn agree(
119        &self,
120        my_secret: &[u8],
121        their_public: &[u8],
122        shared_out: &mut [u8],
123    ) -> Result<(), CryptoError> {
124        if shared_out.len() < 32 {
125            return Err(CryptoError::BufferTooSmall);
126        }
127        let secret_bytes: [u8; 32] = my_secret.try_into().map_err(|_| CryptoError::InvalidKey)?;
128        let public_bytes: [u8; 32] = their_public
129            .try_into()
130            .map_err(|_| CryptoError::InvalidKey)?;
131
132        let secret = StaticSecret::from(secret_bytes);
133        let public = PublicKey::from(public_bytes);
134        let shared = secret.diffie_hellman(&public);
135        // Reject all-zero shared secret (low-order point attack).
136        if oxicrypto_core::ct_is_zero(shared.as_bytes()) {
137            return Err(CryptoError::Kex);
138        }
139        shared_out[..32].copy_from_slice(shared.as_bytes());
140        Ok(())
141    }
142}
143
144// ── ECDH P-256 ───────────────────────────────────────────────────────────────
145
146/// ECDH key agreement over NIST P-256 (secp256r1).
147///
148/// - `my_secret`: 32-byte raw scalar
149/// - `their_public`: SEC1-encoded public key (compressed 33 bytes or uncompressed 65 bytes)
150/// - `shared_out`: receives the 32-byte x-coordinate of the shared point
151#[derive(Debug, Default, Clone, Copy)]
152pub struct EcdhP256;
153
154impl KeyAgreement for EcdhP256 {
155    fn name(&self) -> &'static str {
156        "ECDH-P256"
157    }
158    fn scalar_len(&self) -> usize {
159        32
160    }
161    fn point_len(&self) -> usize {
162        33 // compressed SEC1
163    }
164    fn agree(
165        &self,
166        my_secret: &[u8],
167        their_public: &[u8],
168        shared_out: &mut [u8],
169    ) -> Result<(), CryptoError> {
170        if shared_out.len() < 32 {
171            return Err(CryptoError::BufferTooSmall);
172        }
173        let sk = p256::SecretKey::from_slice(my_secret).map_err(|_| CryptoError::InvalidKey)?;
174        let pk =
175            p256::PublicKey::from_sec1_bytes(their_public).map_err(|_| CryptoError::InvalidKey)?;
176
177        let shared_secret = p256::ecdh::diffie_hellman(sk.to_nonzero_scalar(), pk.as_affine());
178        let raw = shared_secret.raw_secret_bytes();
179        // Reject all-zero shared secret (low-order point attack).
180        if oxicrypto_core::ct_is_zero(raw) {
181            return Err(CryptoError::Kex);
182        }
183        shared_out[..32].copy_from_slice(raw);
184        Ok(())
185    }
186}
187
188// ── ECDH P-384 ───────────────────────────────────────────────────────────────
189
190/// ECDH key agreement over NIST P-384 (secp384r1).
191///
192/// - `my_secret`: 48-byte raw scalar
193/// - `their_public`: SEC1-encoded public key (compressed 49 bytes or uncompressed 97 bytes)
194/// - `shared_out`: receives the 48-byte x-coordinate of the shared point
195#[derive(Debug, Default, Clone, Copy)]
196pub struct EcdhP384;
197
198impl KeyAgreement for EcdhP384 {
199    fn name(&self) -> &'static str {
200        "ECDH-P384"
201    }
202    fn scalar_len(&self) -> usize {
203        48
204    }
205    fn point_len(&self) -> usize {
206        49 // compressed SEC1
207    }
208    fn agree(
209        &self,
210        my_secret: &[u8],
211        their_public: &[u8],
212        shared_out: &mut [u8],
213    ) -> Result<(), CryptoError> {
214        if shared_out.len() < 48 {
215            return Err(CryptoError::BufferTooSmall);
216        }
217        let sk = p384::SecretKey::from_slice(my_secret).map_err(|_| CryptoError::InvalidKey)?;
218        let pk =
219            p384::PublicKey::from_sec1_bytes(their_public).map_err(|_| CryptoError::InvalidKey)?;
220
221        let shared_secret = p384::ecdh::diffie_hellman(sk.to_nonzero_scalar(), pk.as_affine());
222        let raw = shared_secret.raw_secret_bytes();
223        if oxicrypto_core::ct_is_zero(raw) {
224            return Err(CryptoError::Kex);
225        }
226        shared_out[..48].copy_from_slice(raw);
227        Ok(())
228    }
229}
230
231// ── ECDH P-521 ───────────────────────────────────────────────────────────────
232
233/// ECDH key agreement over NIST P-521 (secp521r1).
234///
235/// - `my_secret`: 66-byte raw scalar
236/// - `their_public`: SEC1-encoded public key (uncompressed 133 bytes or compressed 67 bytes)
237/// - `shared_out`: receives the 66-byte x-coordinate of the shared point
238///
239/// Note: NIST P-521 keys generated by this crate use **uncompressed** SEC1
240/// encoding (133 bytes) because `p521` sets `COMPRESS_POINTS = false`.
241/// Both compressed (67 bytes) and uncompressed (133 bytes) inputs are accepted
242/// by `agree()`.
243#[derive(Debug, Default, Clone, Copy)]
244pub struct EcdhP521;
245
246impl KeyAgreement for EcdhP521 {
247    fn name(&self) -> &'static str {
248        "ECDH-P521"
249    }
250    fn scalar_len(&self) -> usize {
251        66
252    }
253    fn point_len(&self) -> usize {
254        133 // uncompressed SEC1: 0x04 prefix + 2 × 66 coordinate bytes
255    }
256    fn agree(
257        &self,
258        my_secret: &[u8],
259        their_public: &[u8],
260        shared_out: &mut [u8],
261    ) -> Result<(), CryptoError> {
262        if shared_out.len() < 66 {
263            return Err(CryptoError::BufferTooSmall);
264        }
265        let sk = p521::SecretKey::from_slice(my_secret).map_err(|_| CryptoError::InvalidKey)?;
266        let pk =
267            p521::PublicKey::from_sec1_bytes(their_public).map_err(|_| CryptoError::InvalidKey)?;
268        let shared_secret = p521::ecdh::diffie_hellman(sk.to_nonzero_scalar(), pk.as_affine());
269        let raw = shared_secret.raw_secret_bytes();
270        if oxicrypto_core::ct_is_zero(raw) {
271            return Err(CryptoError::Kex);
272        }
273        shared_out[..66].copy_from_slice(raw);
274        Ok(())
275    }
276}
277
278// ── Key generation helpers ────────────────────────────────────────────────────
279
280/// Generate an X25519 key pair.
281///
282/// Returns `(SecretKey<32>, [u8; 32])` — the 32-byte static secret scalar and
283/// the 32-byte public point.
284///
285/// # Errors
286///
287/// Returns [`CryptoError::Rng`] if the RNG fails to produce random bytes.
288#[must_use = "result must be checked"]
289pub fn x25519_generate_keypair<R>(rng: &mut R) -> Result<(SecretKey<32>, [u8; 32]), CryptoError>
290where
291    R: rand_core::TryCryptoRng + ?Sized,
292{
293    let mut seed = [0u8; 32];
294    rng.try_fill_bytes(&mut seed)
295        .map_err(|_| CryptoError::Rng)?;
296    let secret = StaticSecret::from(seed);
297    let public = PublicKey::from(&secret);
298    Ok((SecretKey::new(seed), *public.as_bytes()))
299}
300
301/// Generate an ECDH P-256 key pair.
302///
303/// Returns `(SecretVec, Vec<u8>)` — the 32-byte raw scalar wrapped in a
304/// zeroize-on-drop container, and the SEC1-encoded public key (compressed,
305/// 33 bytes for P-256).
306///
307/// # Errors
308///
309/// Returns [`CryptoError::Rng`] if the RNG fails.
310#[must_use = "result must be checked"]
311pub fn ecdh_p256_generate_keypair<R>(rng: &mut R) -> Result<(SecretVec, Vec<u8>), CryptoError>
312where
313    R: rand_core::TryCryptoRng + ?Sized,
314{
315    let secret_key = p256::SecretKey::try_generate_from_rng(rng).map_err(|_| CryptoError::Rng)?;
316    let public_key = secret_key.public_key();
317    let sk_bytes = SecretVec::from_slice(secret_key.to_bytes().as_slice());
318    let pk_bytes = public_key.to_sec1_bytes().to_vec();
319    Ok((sk_bytes, pk_bytes))
320}
321
322/// Generate an ECDH P-384 key pair.
323///
324/// Returns `(SecretVec, Vec<u8>)` — the 48-byte raw scalar and the
325/// SEC1-encoded public key (compressed, 49 bytes for P-384).
326///
327/// # Errors
328///
329/// Returns [`CryptoError::Rng`] if the RNG fails.
330#[must_use = "result must be checked"]
331pub fn ecdh_p384_generate_keypair<R>(rng: &mut R) -> Result<(SecretVec, Vec<u8>), CryptoError>
332where
333    R: rand_core::TryCryptoRng + ?Sized,
334{
335    let secret_key = p384::SecretKey::try_generate_from_rng(rng).map_err(|_| CryptoError::Rng)?;
336    let public_key = secret_key.public_key();
337    let sk_bytes = SecretVec::from_slice(secret_key.to_bytes().as_slice());
338    let pk_bytes = public_key.to_sec1_bytes().to_vec();
339    Ok((sk_bytes, pk_bytes))
340}
341
342/// Generate an ECDH P-521 key pair.
343///
344/// Returns `(SecretVec, Vec<u8>)` — the 66-byte raw scalar and the
345/// SEC1-encoded public key (uncompressed, 133 bytes for P-521).
346///
347/// Note: P-521 uses uncompressed SEC1 encoding by default.
348///
349/// # Errors
350///
351/// Returns [`CryptoError::Rng`] if the RNG fails.
352#[must_use = "result must be checked"]
353pub fn ecdh_p521_generate_keypair<R>(rng: &mut R) -> Result<(SecretVec, Vec<u8>), CryptoError>
354where
355    R: rand_core::TryCryptoRng + ?Sized,
356{
357    let secret_key = p521::SecretKey::try_generate_from_rng(rng).map_err(|_| CryptoError::Rng)?;
358    let public_key = secret_key.public_key();
359    let sk_bytes = SecretVec::from_slice(secret_key.to_bytes().as_slice());
360    let pk_bytes = public_key.to_sec1_bytes().to_vec();
361    Ok((sk_bytes, pk_bytes))
362}
363
364// ── X448 ─────────────────────────────────────────────────────────────────────
365
366/// X448 Diffie-Hellman key agreement (RFC 7748 §5).
367///
368/// - `my_secret`: 56-byte scalar (clamped per RFC 7748)
369/// - `their_public`: 56-byte Montgomery-form public point
370/// - `shared_out`: must be at least 56 bytes; receives the shared secret
371///
372/// Low-order public keys are rejected with [`CryptoError::Kex`].
373#[derive(Debug, Default, Clone, Copy)]
374pub struct X448;
375
376impl KeyAgreement for X448 {
377    fn name(&self) -> &'static str {
378        "X448"
379    }
380    fn scalar_len(&self) -> usize {
381        56
382    }
383    fn point_len(&self) -> usize {
384        56
385    }
386    /// Perform X448 DH and write the 56-byte shared secret into `shared_out`.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`CryptoError::InvalidKey`] if either `my_secret` or
391    /// `their_public` is not exactly 56 bytes, and [`CryptoError::Kex`] if
392    /// `their_public` is a low-order point or the resulting shared secret is
393    /// all-zero.
394    fn agree(
395        &self,
396        my_secret: &[u8],
397        their_public: &[u8],
398        shared_out: &mut [u8],
399    ) -> Result<(), CryptoError> {
400        if shared_out.len() < 56 {
401            return Err(CryptoError::BufferTooSmall);
402        }
403        // Length validation: both inputs must be exactly 56 bytes.
404        let scalar: [u8; 56] = my_secret.try_into().map_err(|_| CryptoError::InvalidKey)?;
405        let point: [u8; 56] = their_public
406            .try_into()
407            .map_err(|_| CryptoError::InvalidKey)?;
408        // x448() applies RFC 7748 clamping and rejects low-order points.
409        let shared = x448::x448(scalar, point).ok_or(CryptoError::Kex)?;
410        // Reject all-zero shared secret (low-order point attack defence in depth).
411        if oxicrypto_core::ct_is_zero(&shared) {
412            return Err(CryptoError::Kex);
413        }
414        shared_out[..56].copy_from_slice(&shared);
415        Ok(())
416    }
417}
418
419/// Generate an X448 key pair from raw random bytes.
420///
421/// Fills 56 bytes from `rng`, applies RFC 7748 clamping, and derives the
422/// public key via base-point multiplication.
423///
424/// Returns `(SecretKey<56>, [u8; 56])` — the clamped secret scalar and the
425/// 56-byte public point.
426///
427/// # Errors
428///
429/// Returns [`CryptoError::Rng`] if the RNG fails to produce random bytes.
430#[must_use = "result must be checked"]
431pub fn x448_generate_keypair<R>(rng: &mut R) -> Result<(SecretKey<56>, [u8; 56]), CryptoError>
432where
433    R: rand_core::TryCryptoRng + ?Sized,
434{
435    let mut seed = [0u8; 56];
436    rng.try_fill_bytes(&mut seed)
437        .map_err(|_| CryptoError::Rng)?;
438    // Apply RFC 7748 §5 clamping: clear bits 0-1 of byte 0, set bit 7 of byte 55.
439    seed[0] &= 252;
440    seed[55] |= 128;
441    // Derive the public key: X448(seed, base_point). x448() re-clamps the scalar;
442    // clamping is idempotent so the round-trip is safe.
443    let public = x448::x448(seed, x448::X448_BASEPOINT_BYTES).ok_or(CryptoError::Rng)?;
444    Ok((SecretKey::new(seed), public))
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use rand_chacha::ChaCha20Rng;
451    use rand_core::SeedableRng;
452
453    const ALICE_SECRET: [u8; 32] = [0xaau8; 32];
454    const BOB_SECRET: [u8; 32] = [0xbbu8; 32];
455
456    fn public_from_secret(secret_bytes: &[u8; 32]) -> [u8; 32] {
457        let secret = StaticSecret::from(*secret_bytes);
458        let public = PublicKey::from(&secret);
459        *public.as_bytes()
460    }
461
462    fn test_rng() -> ChaCha20Rng {
463        ChaCha20Rng::from_seed([42u8; 32])
464    }
465
466    // ── X25519 basic tests ────────────────────────────────────────────────────
467
468    #[test]
469    fn x25519_both_parties_agree() {
470        let kex = X25519;
471
472        let alice_pub = public_from_secret(&ALICE_SECRET);
473        let bob_pub = public_from_secret(&BOB_SECRET);
474
475        let mut alice_shared = [0u8; 32];
476        kex.agree(&ALICE_SECRET, &bob_pub, &mut alice_shared)
477            .expect("Alice agree failed");
478
479        let mut bob_shared = [0u8; 32];
480        kex.agree(&BOB_SECRET, &alice_pub, &mut bob_shared)
481            .expect("Bob agree failed");
482
483        assert_eq!(
484            alice_shared, bob_shared,
485            "Alice and Bob must derive the same shared secret"
486        );
487    }
488
489    #[test]
490    fn x25519_shared_is_32_bytes() {
491        let kex = X25519;
492        assert_eq!(kex.scalar_len(), 32);
493        assert_eq!(kex.point_len(), 32);
494
495        let bob_pub = public_from_secret(&BOB_SECRET);
496        let mut shared = [0u8; 32];
497        kex.agree(&ALICE_SECRET, &bob_pub, &mut shared)
498            .expect("X25519 agree failed");
499        assert_ne!(shared, [0u8; 32], "Shared secret should not be all zeros");
500    }
501
502    #[test]
503    fn x25519_invalid_key_length() {
504        let kex = X25519;
505        let mut shared = [0u8; 32];
506        let result = kex.agree(&[0u8; 16], &[0u8; 32], &mut shared);
507        assert_eq!(result, Err(CryptoError::InvalidKey));
508    }
509
510    #[test]
511    fn x25519_buffer_too_small() {
512        let kex = X25519;
513        let bob_pub = public_from_secret(&BOB_SECRET);
514        let mut shared = [0u8; 16];
515        let result = kex.agree(&ALICE_SECRET, &bob_pub, &mut shared);
516        assert_eq!(result, Err(CryptoError::BufferTooSmall));
517    }
518
519    /// Verify that X25519 with the all-zero public key (a known low-order point
520    /// on Curve25519) returns an error instead of the all-zero shared secret.
521    #[test]
522    fn x25519_zero_rejection() {
523        let kex = X25519;
524        let zero_pk = [0u8; 32]; // all-zero is a low-order point on Curve25519
525        let mut shared = [0u8; 32];
526        let result = kex.agree(&ALICE_SECRET, &zero_pk, &mut shared);
527        assert_eq!(
528            result,
529            Err(CryptoError::Kex),
530            "X25519 must reject all-zero shared secret from low-order public key"
531        );
532    }
533
534    // ── X25519 key generation ─────────────────────────────────────────────────
535
536    /// Generate two X25519 keypairs from an RNG, run DH in both directions,
537    /// and verify the shared secrets match.
538    #[test]
539    fn x25519_keygen_then_agree() {
540        let mut rng = test_rng();
541        let (alice_sk, alice_pk) = x25519_generate_keypair(&mut rng).expect("Alice keygen");
542        let (bob_sk, bob_pk) = x25519_generate_keypair(&mut rng).expect("Bob keygen");
543
544        let kex = X25519;
545        let mut alice_shared = [0u8; 32];
546        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)
547            .expect("Alice agree");
548
549        let mut bob_shared = [0u8; 32];
550        kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)
551            .expect("Bob agree");
552
553        assert_eq!(
554            alice_shared, bob_shared,
555            "x25519_keygen: Alice and Bob must derive the same shared secret"
556        );
557        assert_ne!(alice_shared, [0u8; 32]);
558    }
559
560    // ── ECDH P-256 tests ─────────────────────────────────────────────────────
561
562    fn p256_keypair(scalar_bytes: &[u8; 32]) -> (Vec<u8>, Vec<u8>) {
563        let sk = p256::SecretKey::from_slice(scalar_bytes).expect("valid P-256 scalar");
564        let pk = sk.public_key();
565        (scalar_bytes.to_vec(), pk.to_sec1_bytes().to_vec())
566    }
567
568    #[test]
569    fn ecdh_p256_both_parties_agree() {
570        // Two deterministic scalars (must be valid non-zero mod n).
571        let alice_scalar: [u8; 32] = {
572            let mut s = [0u8; 32];
573            s[0] = 0x01;
574            s[31] = 0x01;
575            s
576        };
577        let bob_scalar: [u8; 32] = {
578            let mut s = [0u8; 32];
579            s[0] = 0x02;
580            s[31] = 0x02;
581            s
582        };
583        let (_alice_sk, alice_pk) = p256_keypair(&alice_scalar);
584        let (_bob_sk, bob_pk) = p256_keypair(&bob_scalar);
585
586        let kex = EcdhP256;
587        let mut alice_shared = [0u8; 32];
588        kex.agree(&alice_scalar, &bob_pk, &mut alice_shared)
589            .expect("Alice ECDH-P256 agree failed");
590
591        let mut bob_shared = [0u8; 32];
592        kex.agree(&bob_scalar, &alice_pk, &mut bob_shared)
593            .expect("Bob ECDH-P256 agree failed");
594
595        assert_eq!(
596            alice_shared, bob_shared,
597            "ECDH-P256: Alice and Bob must derive the same shared secret"
598        );
599        assert_ne!(alice_shared, [0u8; 32]);
600    }
601
602    #[test]
603    fn ecdh_p256_buffer_too_small() {
604        let kex = EcdhP256;
605        let scalar: [u8; 32] = {
606            let mut s = [0u8; 32];
607            s[0] = 0x01;
608            s[31] = 0x01;
609            s
610        };
611        let (_, pk) = p256_keypair(&scalar);
612        let mut shared = [0u8; 16];
613        let result = kex.agree(&scalar, &pk, &mut shared);
614        assert_eq!(result, Err(CryptoError::BufferTooSmall));
615    }
616
617    /// Generate a P-256 keypair, run ECDH in both directions, verify secrets match.
618    #[test]
619    fn ecdh_p256_keygen_agree() {
620        let mut rng = test_rng();
621        let (alice_sk, alice_pk) =
622            ecdh_p256_generate_keypair(&mut rng).expect("Alice P-256 keygen");
623        let (bob_sk, bob_pk) = ecdh_p256_generate_keypair(&mut rng).expect("Bob P-256 keygen");
624
625        let kex = EcdhP256;
626        let mut alice_shared = [0u8; 32];
627        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)
628            .expect("Alice P-256 agree");
629
630        let mut bob_shared = [0u8; 32];
631        kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)
632            .expect("Bob P-256 agree");
633
634        assert_eq!(
635            alice_shared, bob_shared,
636            "ecdh_p256_keygen_agree: secrets must match"
637        );
638        assert_ne!(alice_shared, [0u8; 32]);
639    }
640
641    // ── ECDH P-384 tests ─────────────────────────────────────────────────────
642
643    fn p384_keypair(scalar_bytes: &[u8; 48]) -> (Vec<u8>, Vec<u8>) {
644        let sk = p384::SecretKey::from_slice(scalar_bytes).expect("valid P-384 scalar");
645        let pk = sk.public_key();
646        (scalar_bytes.to_vec(), pk.to_sec1_bytes().to_vec())
647    }
648
649    #[test]
650    fn ecdh_p384_both_parties_agree() {
651        let alice_scalar: [u8; 48] = {
652            let mut s = [0u8; 48];
653            s[0] = 0x01;
654            s[47] = 0x01;
655            s
656        };
657        let bob_scalar: [u8; 48] = {
658            let mut s = [0u8; 48];
659            s[0] = 0x02;
660            s[47] = 0x02;
661            s
662        };
663        let (_alice_sk, alice_pk) = p384_keypair(&alice_scalar);
664        let (_bob_sk, bob_pk) = p384_keypair(&bob_scalar);
665
666        let kex = EcdhP384;
667        let mut alice_shared = [0u8; 48];
668        kex.agree(&alice_scalar, &bob_pk, &mut alice_shared)
669            .expect("Alice ECDH-P384 agree failed");
670
671        let mut bob_shared = [0u8; 48];
672        kex.agree(&bob_scalar, &alice_pk, &mut bob_shared)
673            .expect("Bob ECDH-P384 agree failed");
674
675        assert_eq!(
676            alice_shared, bob_shared,
677            "ECDH-P384: Alice and Bob must derive the same shared secret"
678        );
679        assert_ne!(alice_shared, [0u8; 48]);
680    }
681
682    #[test]
683    fn ecdh_p384_invalid_key() {
684        let kex = EcdhP384;
685        let mut shared = [0u8; 48];
686        let result = kex.agree(&[0u8; 16], &[0u8; 49], &mut shared);
687        assert_eq!(result, Err(CryptoError::InvalidKey));
688    }
689
690    /// Generate a P-384 keypair, run ECDH in both directions, verify secrets match.
691    #[test]
692    fn ecdh_p384_keygen_agree() {
693        let mut rng = test_rng();
694        let (alice_sk, alice_pk) =
695            ecdh_p384_generate_keypair(&mut rng).expect("Alice P-384 keygen");
696        let (bob_sk, bob_pk) = ecdh_p384_generate_keypair(&mut rng).expect("Bob P-384 keygen");
697
698        let kex = EcdhP384;
699        let mut alice_shared = [0u8; 48];
700        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)
701            .expect("Alice P-384 agree");
702
703        let mut bob_shared = [0u8; 48];
704        kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)
705            .expect("Bob P-384 agree");
706
707        assert_eq!(
708            alice_shared, bob_shared,
709            "ecdh_p384_keygen_agree: secrets must match"
710        );
711        assert_ne!(alice_shared, [0u8; 48]);
712    }
713
714    // ── ECDH P-521 tests ─────────────────────────────────────────────────────
715
716    fn p521_keypair_from_scalar(scalar_bytes: &[u8; 66]) -> (Vec<u8>, Vec<u8>) {
717        let sk = p521::SecretKey::from_slice(scalar_bytes).expect("valid P-521 scalar");
718        let pk = sk.public_key();
719        (scalar_bytes.to_vec(), pk.to_sec1_bytes().to_vec())
720    }
721
722    /// Verify that two P-521 parties performing ECDH derive the same shared secret.
723    #[test]
724    fn ecdh_p521_both_parties_agree() {
725        // Construct valid P-521 scalars: non-zero, well under the group order.
726        // P-521 order ≈ 2^521; first byte 0x01 makes the value ≈ 2^520, valid.
727        // We keep the first byte at 0x00 and use bytes further right so the
728        // scalar is unambiguously small relative to the order.
729        let alice_scalar: [u8; 66] = {
730            let mut s = [0u8; 66];
731            s[63] = 0xAB;
732            s[64] = 0xCD;
733            s[65] = 0x01;
734            s
735        };
736        let bob_scalar: [u8; 66] = {
737            let mut s = [0u8; 66];
738            s[63] = 0x12;
739            s[64] = 0x34;
740            s[65] = 0x56;
741            s
742        };
743        let (_alice_sk, alice_pk) = p521_keypair_from_scalar(&alice_scalar);
744        let (_bob_sk, bob_pk) = p521_keypair_from_scalar(&bob_scalar);
745
746        let kex = EcdhP521;
747        let mut alice_shared = [0u8; 66];
748        kex.agree(&alice_scalar, &bob_pk, &mut alice_shared)
749            .expect("Alice ECDH-P521 agree failed");
750
751        let mut bob_shared = [0u8; 66];
752        kex.agree(&bob_scalar, &alice_pk, &mut bob_shared)
753            .expect("Bob ECDH-P521 agree failed");
754
755        assert_eq!(
756            alice_shared, bob_shared,
757            "ECDH-P521: Alice and Bob must derive the same shared secret"
758        );
759        assert_ne!(alice_shared, [0u8; 66]);
760    }
761
762    /// Verify that agree() returns BufferTooSmall when the output buffer is insufficient.
763    #[test]
764    fn ecdh_p521_buffer_too_small() {
765        let alice_scalar: [u8; 66] = {
766            let mut s = [0u8; 66];
767            s[63] = 0xAB;
768            s[64] = 0xCD;
769            s[65] = 0x01;
770            s
771        };
772        let (_alice_sk, alice_pk) = p521_keypair_from_scalar(&alice_scalar);
773        let kex = EcdhP521;
774        let mut shared = [0u8; 32]; // too small (need 66)
775        let result = kex.agree(&alice_scalar, &alice_pk, &mut shared);
776        assert_eq!(result, Err(CryptoError::BufferTooSmall));
777    }
778
779    /// Generate a P-521 keypair, run ECDH in both directions, verify secrets match.
780    #[test]
781    fn ecdh_p521_keygen_agree() {
782        let mut rng = test_rng();
783        let (alice_sk, alice_pk) =
784            ecdh_p521_generate_keypair(&mut rng).expect("Alice P-521 keygen");
785        let (bob_sk, bob_pk) = ecdh_p521_generate_keypair(&mut rng).expect("Bob P-521 keygen");
786
787        let kex = EcdhP521;
788        let mut alice_shared = [0u8; 66];
789        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)
790            .expect("Alice P-521 agree");
791
792        let mut bob_shared = [0u8; 66];
793        kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)
794            .expect("Bob P-521 agree");
795
796        assert_eq!(
797            alice_shared, bob_shared,
798            "ecdh_p521_keygen_agree: secrets must match"
799        );
800        assert_ne!(alice_shared, [0u8; 66]);
801    }
802
803    // ── X448 tests ────────────────────────────────────────────────────────────
804
805    /// Verify that two X448 parties using DH agree on the same shared secret.
806    #[test]
807    fn x448_both_parties_agree() {
808        let kex = X448;
809        let mut rng = test_rng();
810        let (alice_sk, alice_pk) = x448_generate_keypair(&mut rng).expect("Alice X448 keygen");
811        let (bob_sk, bob_pk) = x448_generate_keypair(&mut rng).expect("Bob X448 keygen");
812
813        let mut alice_shared = [0u8; 56];
814        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut alice_shared)
815            .expect("Alice X448 agree");
816
817        let mut bob_shared = [0u8; 56];
818        kex.agree(bob_sk.as_bytes(), &alice_pk, &mut bob_shared)
819            .expect("Bob X448 agree");
820
821        assert_eq!(
822            alice_shared, bob_shared,
823            "X448: Alice and Bob must derive the same shared secret"
824        );
825        assert_ne!(alice_shared, [0u8; 56]);
826    }
827
828    /// Verify X448 trait metadata.
829    #[test]
830    fn x448_metadata() {
831        let kex = X448;
832        assert_eq!(kex.name(), "X448");
833        assert_eq!(kex.scalar_len(), 56);
834        assert_eq!(kex.point_len(), 56);
835        assert_eq!(kex.shared_secret_len(), 56);
836    }
837
838    /// Verify X448 agree_to_vec works correctly.
839    #[test]
840    fn x448_agree_to_vec_matches_agree() {
841        let kex = X448;
842        let mut rng = test_rng();
843        let (alice_sk, _) = x448_generate_keypair(&mut rng).expect("Alice X448 keygen");
844        let (_, bob_pk) = x448_generate_keypair(&mut rng).expect("Bob X448 keygen");
845
846        let mut shared_fixed = [0u8; 56];
847        kex.agree(alice_sk.as_bytes(), &bob_pk, &mut shared_fixed)
848            .expect("agree failed");
849
850        let shared_vec = kex
851            .agree_to_vec(alice_sk.as_bytes(), &bob_pk)
852            .expect("agree_to_vec failed");
853
854        assert_eq!(shared_fixed.as_slice(), shared_vec.as_slice());
855        assert_eq!(shared_vec.len(), 56);
856    }
857
858    /// Verify X448 rejects an invalid-length secret.
859    #[test]
860    fn x448_invalid_secret_length() {
861        let kex = X448;
862        let mut shared = [0u8; 56];
863        let result = kex.agree(&[0u8; 32], &[5u8; 56], &mut shared);
864        assert_eq!(result, Err(CryptoError::InvalidKey));
865    }
866
867    /// Verify X448 rejects a public key of invalid length.
868    #[test]
869    fn x448_invalid_public_length() {
870        let kex = X448;
871        let mut shared = [0u8; 56];
872        // 32-byte public key is wrong length → InvalidKey (not Kex)
873        let result = kex.agree(&[0u8; 56], &[5u8; 32], &mut shared);
874        assert_eq!(result, Err(CryptoError::InvalidKey));
875    }
876
877    /// Verify X448 rejects a buffer that is too small.
878    #[test]
879    fn x448_buffer_too_small() {
880        let kex = X448;
881        let mut rng = test_rng();
882        let (alice_sk, _) = x448_generate_keypair(&mut rng).expect("keygen");
883        let (_, bob_pk) = x448_generate_keypair(&mut rng).expect("keygen");
884        let mut shared = [0u8; 32];
885        let result = kex.agree(alice_sk.as_bytes(), &bob_pk, &mut shared);
886        assert_eq!(result, Err(CryptoError::BufferTooSmall));
887    }
888
889    /// Verify X448 rejects the all-zero low-order public key.
890    #[test]
891    fn x448_zero_public_key_rejection() {
892        let kex = X448;
893        let mut rng = test_rng();
894        let (alice_sk, _) = x448_generate_keypair(&mut rng).expect("keygen");
895        let zero_pk = [0u8; 56];
896        let mut shared = [0u8; 56];
897        let result = kex.agree(alice_sk.as_bytes(), &zero_pk, &mut shared);
898        assert_eq!(
899            result,
900            Err(CryptoError::Kex),
901            "X448 must reject all-zero (low-order) public key"
902        );
903    }
904}