//! E2EE voice call key derivation. //! //! Derives call encryption key material for encrypted voice calls from the ratchet root //! key and an ephemeral X-Wing KEM shared secret exchanged during call //! signaling. The ephemeral KEM provides forward secrecy independent of //! the ratchet — if the root key is later compromised (before the next KEM //! ratchet step), the call content remains confidential. //! //! ## Key Derivation //! //! ```text //! HKDF(salt=rk, ikm=kem_ss ‖ call_id, info="lo-call-v1" ‖ fp_lo ‖ fp_hi, L=96) //! → key_a (32) | key_b (32) | chain_key (32) //! ``` //! //! ## Role Assignment //! //! The party with the lexicographically lower identity fingerprint uses //! `key_a` as their send key and `key_b` as their recv key; the other //! party reverses the assignment. //! //! ## Intra-Call Rekeying //! //! `CallKeys::advance()` ratchets the internal chain key forward: //! //! ```text //! key_a' = HMAC-SHA3-256(chain_key, 0x04) //! key_b' = HMAC-SHA3-256(chain_key, 0x05) //! chain_key' = HMAC-SHA3-256(chain_key, 0x06) //! ``` //! //! The old chain key and call encryption keys are zeroized. This provides forward //! secrecy within the call — compromise of a later call encryption key does not //! reveal earlier media segments. use crate::constants; use crate::error::{Error, Result}; use crate::primitives::{hkdf, hmac}; use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Maximum number of `advance()` calls before the chain is exhausted. /// 2^24 (16,777,216) steps — far beyond any realistic call duration even with /// aggressive per-second rekeying. const MAX_CALL_ADVANCE: u32 = 1 << 24; /// Call encryption key state for an active E2EE voice/video call. /// /// Holds the current send key, receive key, and chain key for intra-call /// rekeying (§6.12). Role assignment (which key is send vs recv) is /// determined by fingerprint ordering at construction time. /// /// ## Lifecycle /// /// 1. Obtain via `derive_call_keys` after the ephemeral KEM exchange. /// 2. Read `send_key()` and `recv_key()` to configure the call encryption session. /// 3. Periodically call `advance()` to rekey — `send_key()` and `recv_key()` /// then return the new keys. /// /// # Thread Safety /// /// `CallKeys` auto-derives `Send + Sync` but is not designed for concurrent /// access. `advance` requires `&mut self`. The CAPI layer adds a runtime /// reentrancy guard for FFI callers. #[derive(Zeroize, ZeroizeOnDrop)] pub struct CallKeys { /// Current send key (32 bytes). send_key: [u8; 32], /// Current recv key (32 bytes). recv_key: [u8; 32], /// Chain key for intra-call rekeying. chain_key: [u8; 32], /// `true` if the local party has the lexicographically lower fingerprint, /// determining which HMAC output becomes the send vs recv key on each /// `advance()` call. #[zeroize(skip)] lower_role: bool, /// Number of `advance()` calls so far. Monotonically increasing; /// `advance()` returns `ChainExhausted` at `MAX_CALL_ADVANCE`. #[zeroize(skip)] step_count: u32, } impl CallKeys { /// Return the current send key (32 bytes). pub fn send_key(&self) -> &[u8; 32] { &self.send_key } /// Return the current recv key (32 bytes). pub fn recv_key(&self) -> &[u8; 32] { &self.recv_key } /// Return the current chain key bytes (test-utils only). #[cfg(all(feature = "test-utils", debug_assertions))] #[deprecated(note = "test-utils only — do not call in production code")] pub fn chain_key_bytes(&self) -> &[u8; 32] { &self.chain_key } /// Advance the call chain, replacing the current call encryption keys with fresh /// key material. /// /// Derives two new call encryption keys and a new chain key from the current chain /// key via HMAC-SHA3-256. The old chain key and call encryption keys are zeroized /// before replacement. /// /// After this call, [`send_key`](Self::send_key) and /// [`recv_key`](Self::recv_key) return the new keys. Role assignment /// (which HMAC output is send vs recv) is preserved from the original /// [`derive_call_keys`] call. /// /// # Errors /// /// Returns `ChainExhausted` after `MAX_CALL_ADVANCE` (2^24) calls. The /// caller must establish a new call with a fresh KEM exchange rather than /// continuing to rekey the exhausted chain. pub fn advance(&mut self) -> Result<()> { if self.step_count >= MAX_CALL_ADVANCE { // Zeroize all key material on exhaustion — prevents callers that // ignore the error from continuing with stale keys. self.send_key.zeroize(); self.recv_key.zeroize(); self.chain_key.zeroize(); return Err(Error::ChainExhausted); } let mut key_a = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_KEY_A_BYTE); let mut key_b = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_KEY_B_BYTE); let mut next_chain = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_CHAIN_ADV_BYTE); // [u8; 32] is Copy — field assignment copies new values without // dropping the old ones, so explicit zeroization prevents the // previous keys from lingering on the stack. self.chain_key.zeroize(); self.send_key.zeroize(); self.recv_key.zeroize(); self.chain_key = next_chain; if self.lower_role { self.send_key = key_a; self.recv_key = key_b; } else { self.send_key = key_b; self.recv_key = key_a; } // [u8; 32] is Copy — all assignments above created bitwise copies. // Zeroize the stack originals. key_a.zeroize(); key_b.zeroize(); next_chain.zeroize(); // Cannot overflow: MAX_CALL_ADVANCE (2^24) << u32::MAX, and the // >= MAX_CALL_ADVANCE guard above bounds step_count before this point. self.step_count += 1; Ok(()) } } /// Derive call keys from the ratchet root key, an ephemeral KEM shared /// secret, and a random call identifier. /// /// Both parties must call this function with identical `root_key`, `kem_ss`, /// and `call_id` values. The `local_fp`/`remote_fp` parameters determine /// role assignment (which half becomes the send vs recv key) and are /// naturally swapped between the two parties. /// /// ## Protocol /// /// 1. Call initiator (Alice) generates `call_id` (16 random bytes) and an /// ephemeral X-Wing keypair `(ek_pub, ek_sk)`. Sends `call_id` + `ek_pub` /// in a `CallOffer` message (encrypted via the ratchet). /// 2. Responder (Bob) encapsulates to Alice's ephemeral public key: /// `(ct, kem_ss) = XWing.Encaps(ek_pub)`. Sends `CallAnswer { ct }` /// (encrypted via the ratchet). /// 3. Alice decapsulates: `kem_ss = XWing.Decaps(ek_sk, ct)`. Zeroizes /// `ek_sk` immediately. /// 4. Both parties call this function with the same inputs to derive matching /// call encryption keys. /// /// ## Role Assignment /// /// The party with the lexicographically lower identity fingerprint uses /// `key_a` (first HKDF output half) as their send key and `key_b` (second /// half) as their recv key. The other party reverses the assignment. Both /// parties arrive at the correct assignment because `local_fp`/`remote_fp` /// are swapped between them. /// /// # Security /// /// The ephemeral KEM exchange binds call security to a fresh shared secret /// independent of the ratchet state. Even if `root_key` is later compromised /// (before the next ratchet KEM step), the call remains confidential — the /// ephemeral KEM secret is zeroized after key derivation. /// /// `root_key` in the HKDF salt provides defense-in-depth: if the ephemeral /// KEM is broken (e.g., by a future quantum computer), the root key's /// accumulated post-quantum security still protects the call. /// /// ## `call_id` Uniqueness /// /// `call_id` (16 random bytes, generated by the call initiator) is /// concatenated with `kem_ss` to form the HKDF IKM. Because a fresh /// ephemeral KEM exchange already guarantees a unique `kem_ss` per call, /// `call_id` is not the primary domain-separation mechanism — it provides /// defense-in-depth against KEM randomness failure. The caller (application /// layer) is responsible for generating `call_id` via a CSPRNG and must /// never reuse a `call_id` for a different call under the same root key. /// `call_id` is not bound into the HKDF info label because it is already /// mixed into the IKM; binding it to both would be redundant. /// /// # Errors /// /// Returns `Err(InvalidData)` if: /// - `root_key` is all-zeros (indicates uninitialized ratchet state) /// - `kem_ss` is all-zeros (indicates KEM failure or uninitialized buffer) /// - `call_id` is all-zeros (indicates uninitialized buffer) /// - `local_fp == remote_fp` (equal fingerprints collapse send/recv role separation) pub fn derive_call_keys( root_key: &[u8; 32], kem_ss: &[u8; 32], call_id: &[u8; 16], local_fp: &[u8; 32], remote_fp: &[u8; 32], ) -> Result { // All-zeros root_key indicates an uninitialized or reset ratchet. // Call keys derived from a zero root key provide no binding to the // ratchet session — an attacker who knows kem_ss could derive them. // Constant-time: root_key is secret material. if bool::from(root_key.ct_eq(&[0u8; 32])) { return Err(Error::InvalidData); } // All-zeros kem_ss indicates a KEM failure or uninitialized buffer. // Ephemeral forward secrecy would be entirely lost — the call keys would // depend only on root_key and call_id, both potentially attacker-known. // Constant-time: kem_ss is secret material (X-Wing shared secret). if bool::from(kem_ss.ct_eq(&[0u8; 32])) { return Err(Error::InvalidData); } // All-zeros call_id indicates an uninitialized buffer from the caller. // call_id is mixed into the IKM for domain separation — a zero value // still produces unique keys (from kem_ss), but indicates a programming // error rather than a legitimate random identifier. if call_id == &[0u8; 16] { return Err(Error::InvalidData); } // Equal fingerprints give both parties the same role (lower_role = false // for both since `<` is strict), silently collapsing send/recv separation. if local_fp == remote_fp { return Err(Error::InvalidData); } // Concatenate kem_ss ‖ call_id as HKDF input keying material. // Both components are fixed-size (&[u8; 32] and &[u8; 16]), so the // concatenation is unambiguous — no length-prefix needed. let mut ikm = [0u8; 48]; ikm[..32].copy_from_slice(kem_ss); ikm[32..].copy_from_slice(call_id); // HKDF info: label ‖ lower_fp ‖ higher_fp. Fingerprints are sorted into // canonical (lower-first) order so both parties produce identical info // regardless of which is local vs remote. This binds session identity // into the key derivation — defense-in-depth against KEX-level collisions // producing identical root_key for different identity pairs. let (fp_lo, fp_hi) = if local_fp < remote_fp { (local_fp, remote_fp) } else { (remote_fp, local_fp) }; let mut info = [0u8; 10 + 32 + 32]; // CALL_HKDF_INFO (10) + 2 × fingerprint (32) info[..10].copy_from_slice(constants::CALL_HKDF_INFO); info[10..42].copy_from_slice(fp_lo); info[42..74].copy_from_slice(fp_hi); // HKDF output: 96 bytes → key_a (32) | key_b (32) | chain_key (32). let mut output = [0u8; 96]; // Output size (96) is compile-time constant — InvalidLength structurally unreachable. hkdf::hkdf_sha3_256(root_key, &ikm, &info, &mut output)?; // IKM contains kem_ss (secret) — zeroize immediately after HKDF. ikm.zeroize(); // Role assignment: lexicographically lower fingerprint gets key_a as // send key. Both parties compute the same assignment because they swap // local_fp and remote_fp. Variable-time comparison is acceptable — // fingerprints are public identity material, not secret. let lower_role = local_fp < remote_fp; let (send_off, recv_off) = if lower_role { (0, 32) } else { (32, 0) }; let mut send_key = [0u8; 32]; let mut recv_key = [0u8; 32]; let mut chain_key = [0u8; 32]; send_key.copy_from_slice(&output[send_off..send_off + 32]); recv_key.copy_from_slice(&output[recv_off..recv_off + 32]); chain_key.copy_from_slice(&output[64..96]); // HKDF output contains all derived key material — zeroize before // leaving scope. output.zeroize(); let keys = CallKeys { send_key, recv_key, chain_key, lower_role, step_count: 0, }; // [u8; 32] is Copy — struct initialization created bitwise copies; // zeroize the stack originals before returning. send_key.zeroize(); recv_key.zeroize(); chain_key.zeroize(); Ok(keys) } #[cfg(test)] mod tests { use super::*; use crate::primitives::{hkdf, hmac}; use std::collections::HashSet; // Fixed test inputs. const RK: [u8; 32] = [0x01u8; 32]; const SS: [u8; 32] = [0x02u8; 32]; const CALL_ID: [u8; 16] = [0x03u8; 16]; const FP_LO: [u8; 32] = [0x00u8; 32]; // lexicographically lower const FP_HI: [u8; 32] = [0xFFu8; 32]; // lexicographically higher /// Independently compute the HKDF output for known inputs. /// /// Deliberately uses literal `b"lo-call-v1"` instead of `constants::CALL_HKDF_INFO` /// so that a constant change is caught by `derive_hkdf_kat`. fn reference_hkdf(rk: &[u8; 32], ss: &[u8; 32], call_id: &[u8; 16]) -> [u8; 96] { let mut ikm = [0u8; 48]; ikm[..32].copy_from_slice(ss); ikm[32..].copy_from_slice(call_id); // Info: label ‖ lower_fp ‖ higher_fp (canonical order). let mut info = [0u8; 10 + 32 + 32]; info[..10].copy_from_slice(b"lo-call-v1"); info[10..42].copy_from_slice(&FP_LO); // FP_LO < FP_HI info[42..74].copy_from_slice(&FP_HI); let mut output = [0u8; 96]; hkdf::hkdf_sha3_256(rk, &ikm, &info, &mut output).unwrap(); output } #[test] fn derive_round_trip_matching() { let alice = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); let bob = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap(); assert_eq!(alice.send_key(), bob.recv_key()); assert_eq!(alice.recv_key(), bob.send_key()); } #[test] fn derive_hkdf_kat() { let output = reference_hkdf(&RK, &SS, &CALL_ID); let key_a = &output[..32]; let key_b = &output[32..64]; // FP_LO < FP_HI → lower_role=true → send=key_a, recv=key_b let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert_eq!(keys.send_key().as_slice(), key_a); assert_eq!(keys.recv_key().as_slice(), key_b); } #[test] fn advance_hmac_kat() { let output = reference_hkdf(&RK, &SS, &CALL_ID); let chain_key = &output[64..96]; // First advance: chain_key → HMAC(ck, 0x04) / HMAC(ck, 0x05). let expected_a = hmac::hmac_sha3_256(chain_key, constants::CALL_KEY_A_BYTE); let expected_b = hmac::hmac_sha3_256(chain_key, constants::CALL_KEY_B_BYTE); // Derive expected next chain key via advance byte, then verify the second advance. let expected_chain2 = hmac::hmac_sha3_256(chain_key, constants::CALL_CHAIN_ADV_BYTE); let expected_a2 = hmac::hmac_sha3_256(&expected_chain2, constants::CALL_KEY_A_BYTE); let expected_b2 = hmac::hmac_sha3_256(&expected_chain2, constants::CALL_KEY_B_BYTE); let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); keys.advance().unwrap(); // lower_role=true → send=key_a, recv=key_b assert_eq!(*keys.send_key(), expected_a); assert_eq!(*keys.recv_key(), expected_b); // Second advance confirms CALL_CHAIN_ADV_BYTE drives the chain: the new // keys must match HMAC(expected_chain2, CALL_KEY_A/B_BYTE). keys.advance().unwrap(); assert_eq!(*keys.send_key(), expected_a2); assert_eq!(*keys.recv_key(), expected_b2); } #[test] fn derive_different_call_ids() { let k1 = derive_call_keys(&RK, &SS, &[0x01u8; 16], &FP_LO, &FP_HI).unwrap(); let k2 = derive_call_keys(&RK, &SS, &[0x02u8; 16], &FP_LO, &FP_HI).unwrap(); assert_ne!(k1.send_key(), k2.send_key()); } #[test] fn derive_different_kem_ss() { let k1 = derive_call_keys(&RK, &[0xAAu8; 32], &CALL_ID, &FP_LO, &FP_HI).unwrap(); let k2 = derive_call_keys(&RK, &[0xBBu8; 32], &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert_ne!(k1.send_key(), k2.send_key()); } #[test] fn derive_different_root_keys() { let k1 = derive_call_keys(&[0xAAu8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); let k2 = derive_call_keys(&[0xBBu8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert_ne!(k1.send_key(), k2.send_key()); } #[test] fn role_assignment_lower_fp_is_send() { let output = reference_hkdf(&RK, &SS, &CALL_ID); let key_a = &output[..32]; let key_b = &output[32..64]; // Lower fp → send=key_a let lower = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert_eq!(lower.send_key().as_slice(), key_a); assert_eq!(lower.recv_key().as_slice(), key_b); // Higher fp → send=key_b let higher = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap(); assert_eq!(higher.send_key().as_slice(), key_b); assert_eq!(higher.recv_key().as_slice(), key_a); } #[test] fn advance_changes_keys() { let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); let send_before = *keys.send_key(); let recv_before = *keys.recv_key(); keys.advance().unwrap(); assert_ne!(*keys.send_key(), send_before); assert_ne!(*keys.recv_key(), recv_before); } #[test] fn advance_preserves_role() { let mut alice = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); let mut bob = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap(); alice.advance().unwrap(); bob.advance().unwrap(); assert_eq!(alice.send_key(), bob.recv_key()); assert_eq!(alice.recv_key(), bob.send_key()); } #[test] fn advance_produces_distinct_keys() { let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); keys.advance().unwrap(); let after_first = *keys.send_key(); keys.advance().unwrap(); let after_second = *keys.send_key(); assert_ne!(after_first, after_second); } #[test] fn advance_keys_independent_of_chain_key() { // Intra-call forward secrecy invariant: knowing the exposed message keys // (send_key, recv_key) must not reveal the chain key that generates all // future keys. Verified by asserting send_key != chain_key and // recv_key != chain_key after each advance step. let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); for _ in 0..10 { keys.advance().unwrap(); #[allow(deprecated)] let ck = *keys.chain_key_bytes(); assert_ne!(*keys.send_key(), ck, "send_key == chain_key after advance"); assert_ne!(*keys.recv_key(), ck, "recv_key == chain_key after advance"); } } #[test] fn advance_multiple_steps() { // Inserts send_key before each advance: collects the initial key plus // keys after advances 1-99 (100 values across 99 steps), all unique. let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); let mut seen = HashSet::new(); for _ in 0..100 { seen.insert(*keys.send_key()); keys.advance().unwrap(); } assert_eq!(seen.len(), 100); } #[test] fn send_recv_keys_differ() { let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert_ne!(keys.send_key(), keys.recv_key()); } #[test] fn keys_are_32_bytes() { let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); assert!(keys.send_key().iter().any(|&b| b != 0)); assert!(keys.recv_key().iter().any(|&b| b != 0)); } #[test] fn zero_kem_ss_rejected() { assert!(matches!( derive_call_keys(&RK, &[0u8; 32], &CALL_ID, &FP_LO, &FP_HI), Err(crate::error::Error::InvalidData) )); } #[test] fn zero_root_key_rejected() { assert!(matches!( derive_call_keys(&[0u8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI), Err(crate::error::Error::InvalidData) )); } #[test] fn zero_call_id_rejected() { assert!(matches!( derive_call_keys(&RK, &SS, &[0u8; 16], &FP_LO, &FP_HI), Err(crate::error::Error::InvalidData) )); } #[test] fn advance_exhaustion_returns_chain_exhausted() { let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); // Simulate being at the limit by advancing MAX_CALL_ADVANCE - 1 times // would be too slow. Instead, directly set step_count near the limit. keys.step_count = MAX_CALL_ADVANCE - 1; // One more advance should succeed (step_count == MAX_CALL_ADVANCE - 1 < MAX_CALL_ADVANCE). assert!(keys.advance().is_ok()); // Now step_count == MAX_CALL_ADVANCE, next advance must fail. assert!(matches!(keys.advance(), Err(Error::ChainExhausted))); } #[test] fn advance_exhaustion_zeroizes_keys() { let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap(); keys.step_count = MAX_CALL_ADVANCE - 1; keys.advance().unwrap(); // Exhaust the chain. assert!(matches!(keys.advance(), Err(Error::ChainExhausted))); // All keys must be zeroed after exhaustion. assert_eq!( *keys.send_key(), [0u8; 32], "send_key not zeroed after ChainExhausted" ); assert_eq!( *keys.recv_key(), [0u8; 32], "recv_key not zeroed after ChainExhausted" ); #[allow(deprecated)] { assert_eq!( *keys.chain_key_bytes(), [0u8; 32], "chain_key not zeroed after ChainExhausted" ); } } #[test] fn equal_fingerprints_rejected() { assert!(matches!( derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_LO), Err(crate::error::Error::InvalidData) )); } #[test] fn kdf_call_spec_kat() { // Specification.md Appendix F.15 — KDF_Call reference vector. use hex_literal::hex; let root_key: [u8; 32] = [0xAAu8; 32]; let kem_ss: [u8; 32] = [0xBBu8; 32]; let call_id: [u8; 16] = [0xCCu8; 16]; let local_fp: [u8; 32] = [0x11u8; 32]; // < 0x22 → lower_role let remote_fp: [u8; 32] = [0x22u8; 32]; let expected_key_a = hex!("ed75d812373c9b3bf6bddd394a631950520503f103b492fb908621eb712b5970"); let expected_key_b = hex!("c3e5171534e0d1f922ea4ebf318357b990eafb0fff45d8cf430639a1fe2bb1e4"); let expected_chain_key = hex!("1427dde311aaa195b116cc98c870753179297981446d3b53e00a4a92a0d34aeb"); let keys = derive_call_keys(&root_key, &kem_ss, &call_id, &local_fp, &remote_fp).unwrap(); // local_fp < remote_fp → lower_role=true → send=key_a, recv=key_b. assert_eq!( keys.send_key().as_slice(), &expected_key_a, "send_key (key_a) does not match F.15 vector" ); assert_eq!( keys.recv_key().as_slice(), &expected_key_b, "recv_key (key_b) does not match F.15 vector" ); #[allow(deprecated)] { assert_eq!( keys.chain_key_bytes().as_slice(), &expected_chain_key, "chain_key does not match F.15 vector" ); } // Reversed-order coverage: swap fingerprints → same HKDF output, // reversed role assignment. let keys_rev = derive_call_keys(&root_key, &kem_ss, &call_id, &remote_fp, &local_fp).unwrap(); assert_eq!( keys_rev.send_key().as_slice(), &expected_key_b, "reversed: send_key should be key_b" ); assert_eq!( keys_rev.recv_key().as_slice(), &expected_key_a, "reversed: recv_key should be key_a" ); } #[test] fn advance_call_chain_spec_kat() { // Specification.md Appendix F.16 — AdvanceCallChain reference vector. // Uses chain_key from F.15 as starting point. use hex_literal::hex; let root_key: [u8; 32] = [0xAAu8; 32]; let kem_ss: [u8; 32] = [0xBBu8; 32]; let call_id: [u8; 16] = [0xCCu8; 16]; let local_fp: [u8; 32] = [0x11u8; 32]; let remote_fp: [u8; 32] = [0x22u8; 32]; let expected_key_a_prime = hex!("9cf3129c6bb7ad86cb12ffc534517a4c06a472fbcddbe295a501c79aa49800e1"); let expected_key_b_prime = hex!("f24cd7822fd611159a6e6d809c6ac148fd7b9bad65d8b4f85745869634b2dd1e"); let expected_chain_key_prime = hex!("d3ae610c39cd9f7f8dce990b5c91634092ad0621fc01b44b24b2cb9f3638d0f2"); let mut keys = derive_call_keys(&root_key, &kem_ss, &call_id, &local_fp, &remote_fp).unwrap(); keys.advance().unwrap(); // lower_role=true → send=key_a', recv=key_b'. assert_eq!( keys.send_key().as_slice(), &expected_key_a_prime, "send_key (key_a') does not match F.16 vector" ); assert_eq!( keys.recv_key().as_slice(), &expected_key_b_prime, "recv_key (key_b') does not match F.16 vector" ); #[allow(deprecated)] { assert_eq!( keys.chain_key_bytes().as_slice(), &expected_chain_key_prime, "chain_key' does not match F.16 vector" ); } } proptest::proptest! { #[test] fn proptest_derive_round_trip( rk in proptest::array::uniform32(proptest::prelude::any::()), ss in proptest::array::uniform32(proptest::prelude::any::()), call_id in proptest::array::uniform16(proptest::prelude::any::()), fp_a in proptest::array::uniform32(proptest::prelude::any::()), fp_b in proptest::array::uniform32(proptest::prelude::any::()), ) { // Filter out inputs rejected by derive_call_keys validation. proptest::prop_assume!(fp_a != fp_b); proptest::prop_assume!(rk != [0u8; 32]); proptest::prop_assume!(ss != [0u8; 32]); proptest::prop_assume!(call_id != [0u8; 16]); let alice = derive_call_keys(&rk, &ss, &call_id, &fp_a, &fp_b).unwrap(); let bob = derive_call_keys(&rk, &ss, &call_id, &fp_b, &fp_a).unwrap(); proptest::prop_assert_eq!(alice.send_key(), bob.recv_key()); proptest::prop_assert_eq!(alice.recv_key(), bob.send_key()); } } }