Skip to main content

phantom_protocol/crypto/
kdf.rs

1//! Shared key-derivation helpers (Phase 4.1).
2//!
3//! Functions here are deterministic and side-agnostic: the client and the
4//! server feed identical inputs and obtain identical outputs. They live in
5//! `crypto/` rather than `transport/` so both the server handshake path
6//! (`transport::handshake`) and the client API path (`api::session`) can
7//! call them without a circular module dependency.
8
9use hkdf::Hkdf;
10use sha2::Sha256;
11
12/// 32-byte key derivation that matches `blake3::derive_key`'s API
13/// shape (label string + IKM bytes → `[u8; 32]`) and dispatches per
14/// the active build:
15///
16/// - Default: `blake3::derive_key(label, ikm)` — pure-Rust, infallible.
17/// - `--features fips`: `HKDF-SHA256` with empty salt, `info = label
18///   bytes`. FIPS 140-3 §C-approved (SP 800-108) so the same
19///   call-sites stay valid under the fips swap.
20///
21/// PANIC-SAFETY: HKDF-SHA256 `expand` only errors when the requested
22/// output length exceeds 255 * HashLen (255 * 32 = 8160 bytes for
23/// SHA-256). 32 bytes is well within that ceiling, so the
24/// `expect_used` is statically safe.
25#[inline]
26pub fn derive_key_32(label: &str, ikm: &[u8]) -> [u8; 32] {
27    #[cfg(not(feature = "fips"))]
28    {
29        blake3::derive_key(label, ikm)
30    }
31    #[cfg(feature = "fips")]
32    {
33        let hk = Hkdf::<Sha256>::new(None, ikm);
34        let mut out = [0u8; 32];
35        #[allow(clippy::expect_used)]
36        // PANIC-SAFETY: 32 bytes is far below HKDF-SHA256's 255 *
37        // HashLen output ceiling.
38        hk.expand(label.as_bytes(), &mut out)
39            .expect("HKDF-SHA256 expand: 32 bytes is within the SHA-256 output bound");
40        out
41    }
42}
43
44/// HKDF `info` label for the 0-RTT early-data AEAD key.
45const EARLY_DATA_KEY_INFO: &[u8] = b"phantom-early-data-key-v3";
46/// HKDF `info` label for the 0-RTT early-data AEAD nonce.
47const EARLY_DATA_NONCE_INFO: &[u8] = b"phantom-early-data-nonce-v3";
48
49/// Derive the AEAD `(key, nonce)` pair that protects 0-RTT early-data
50/// carried inside a V3 `ClientHello`.
51///
52/// Both peers hold the two inputs:
53/// - `resumption_secret` — the 32-byte secret a prior handshake produced;
54///   the server keeps it in its `SessionCache`, the client gets it from
55///   `Session::resumption_hint()`.
56/// - `client_nonce` — the fresh 32-byte nonce in *this* connect's
57///   `ClientHello`, visible to both sides.
58///
59/// Construction: HKDF-SHA256 with `client_nonce` as the salt and
60/// `resumption_secret` as the IKM, then two `expand` calls with
61/// distinct `info` labels — one for the 32-byte key, one for the
62/// 12-byte AEAD nonce. HKDF-SHA256 (not BLAKE3) keeps this path
63/// FIPS-eligible.
64///
65/// The output is single-use: the key is bound to one `client_nonce`,
66/// which is itself one-shot (the server consumes the resumption ticket
67/// on first sight — see `SessionCache::try_resume`). So a fixed,
68/// deterministically-derived nonce is safe — the `(key, nonce)` pair is
69/// never reused for a second encryption.
70pub fn derive_early_data_keying(
71    resumption_secret: &[u8; 32],
72    client_nonce: &[u8; 32],
73) -> ([u8; 32], [u8; 12]) {
74    let hk = Hkdf::<Sha256>::new(Some(client_nonce), resumption_secret);
75    let mut key = [0u8; 32];
76    let mut nonce = [0u8; 12];
77    // HKDF-Expand only fails when the requested length exceeds
78    // 255 * HashLen (255 * 32 = 8160 bytes for SHA-256). 32 and 12 are
79    // both far below that ceiling, so these expansions are infallible.
80    // PANIC-SAFETY: see above — the length precondition is a compile-time
81    // constant well within the HKDF bound.
82    #[allow(clippy::expect_used)]
83    hk.expand(EARLY_DATA_KEY_INFO, &mut key)
84        .expect("HKDF expand: 32 bytes is within the SHA-256 output bound");
85    #[allow(clippy::expect_used)]
86    hk.expand(EARLY_DATA_NONCE_INFO, &mut nonce)
87        .expect("HKDF expand: 12 bytes is within the SHA-256 output bound");
88    (key, nonce)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn deterministic_same_inputs_same_outputs() {
97        // The whole point: client and server, given identical inputs,
98        // derive byte-identical keying material.
99        let secret = [0x11u8; 32];
100        let nonce = [0x22u8; 32];
101        let a = derive_early_data_keying(&secret, &nonce);
102        let b = derive_early_data_keying(&secret, &nonce);
103        assert_eq!(a.0, b.0, "key must be deterministic");
104        assert_eq!(a.1, b.1, "nonce must be deterministic");
105    }
106
107    #[test]
108    fn distinct_client_nonce_yields_distinct_keying() {
109        let secret = [0x11u8; 32];
110        let (k1, n1) = derive_early_data_keying(&secret, &[0x01u8; 32]);
111        let (k2, n2) = derive_early_data_keying(&secret, &[0x02u8; 32]);
112        assert_ne!(k1, k2, "different client_nonce must change the key");
113        assert_ne!(n1, n2, "different client_nonce must change the nonce");
114    }
115
116    #[test]
117    fn distinct_resumption_secret_yields_distinct_keying() {
118        let nonce = [0x22u8; 32];
119        let (k1, _) = derive_early_data_keying(&[0xAAu8; 32], &nonce);
120        let (k2, _) = derive_early_data_keying(&[0xBBu8; 32], &nonce);
121        assert_ne!(k1, k2, "different resumption_secret must change the key");
122    }
123
124    #[test]
125    fn key_and_nonce_are_independent() {
126        // The two HKDF expansions use distinct info labels, so the key
127        // bytes and nonce bytes are not a prefix/suffix of one another.
128        let (key, nonce) = derive_early_data_keying(&[0x33u8; 32], &[0x44u8; 32]);
129        assert_ne!(
130            &key[..12],
131            &nonce[..],
132            "key prefix must not equal the nonce"
133        );
134    }
135
136    #[test]
137    fn derive_key_32_is_deterministic() {
138        let a = derive_key_32("phantom-self-test", b"some-ikm-bytes");
139        let b = derive_key_32("phantom-self-test", b"some-ikm-bytes");
140        assert_eq!(a, b, "derive_key_32 must be deterministic across calls");
141    }
142
143    #[test]
144    fn derive_key_32_label_changes_output() {
145        let a = derive_key_32("phantom-label-a", b"ikm");
146        let b = derive_key_32("phantom-label-b", b"ikm");
147        assert_ne!(a, b, "different labels must produce different keys");
148    }
149
150    /// fips-only KAT: locks the HKDF-SHA256 construction used by
151    /// `derive_key_32`. A mismatch on a clean build means the
152    /// underlying `hkdf` / `sha2` crates changed behavior or that
153    /// the cfg dispatch in `derive_key_32` is broken.
154    #[cfg(feature = "fips")]
155    #[test]
156    fn derive_key_32_fips_kat() {
157        let out = derive_key_32("phantom-rekey-v1", &[0x11u8; 32]);
158        // KAT computed from `Hkdf::<Sha256>::new(None, &[0x11; 32])`
159        // then `expand(b"phantom-rekey-v1", &mut [0u8; 32])`. Matches
160        // the bytes baked into `crypto::self_tests::test_hkdf_sha256`.
161        const KAT: [u8; 32] = [
162            0x41, 0x90, 0x72, 0xe4, 0xca, 0x1b, 0xa9, 0xca, 0xdc, 0x1b, 0x02, 0xd3, 0x75, 0xb0,
163            0xf8, 0x84, 0x70, 0xa7, 0x0f, 0xe9, 0x57, 0x13, 0x1d, 0x7b, 0x5b, 0x35, 0xe5, 0x74,
164            0x14, 0x34, 0xe4, 0x10,
165        ];
166        assert_eq!(
167            out, KAT,
168            "derive_key_32 fips path must match HKDF-SHA256 KAT"
169        );
170    }
171}