Skip to main content

pakery_spake2/
transcript.rs

1//! Key schedule and output per RFC 9382 §4.
2
3use alloc::vec::Vec;
4use subtle::ConstantTimeEq;
5use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
6
7use pakery_core::crypto::{Hash, Kdf, Mac};
8use pakery_core::SharedSecret;
9
10use crate::ciphersuite::Spake2Ciphersuite;
11use crate::error::Spake2Error;
12
13/// Output of a completed SPAKE2 protocol run.
14///
15/// Contains the session key and confirmation MACs.
16#[derive(Zeroize, ZeroizeOnDrop)]
17pub struct Spake2Output {
18    /// The session key (Ke, first half of the hash).
19    #[zeroize(skip)]
20    pub session_key: SharedSecret,
21    /// This party's confirmation MAC to send to the peer.
22    pub confirmation_mac: Vec<u8>,
23    /// The expected MAC from the peer.
24    expected_peer_mac: Vec<u8>,
25}
26
27impl Spake2Output {
28    /// Verify the peer's confirmation MAC in constant time.
29    pub fn verify_peer_confirmation(&self, peer_mac: &[u8]) -> Result<(), Spake2Error> {
30        if self.expected_peer_mac.ct_eq(peer_mac).into() {
31            Ok(())
32        } else {
33            Err(Spake2Error::ConfirmationFailed)
34        }
35    }
36}
37
38/// Derive the key schedule from transcript TT.
39///
40/// Per RFC 9382 §4:
41/// 1. `Ke || Ka = Hash(TT)` (first NH/2 = Ke, second NH/2 = Ka)
42/// 2. `PRK = KDF.extract(salt=[], ikm=Ka)`
43/// 3. `KcA || KcB = KDF.expand(PRK, "ConfirmationKeys" || AAD, NH)`
44/// 4. `cA = MAC(KcA, TT)`, `cB = MAC(KcB, TT)`
45pub fn derive_key_schedule<C: Spake2Ciphersuite>(
46    tt: &[u8],
47    aad: &[u8],
48    is_party_a: bool,
49) -> Result<Spake2Output, Spake2Error> {
50    // Step 1: Hash(TT) → Ke || Ka
51    const { assert!(<C::Hash as pakery_core::crypto::Hash>::OUTPUT_SIZE >= C::NH) };
52    let hash_tt = Zeroizing::new(C::Hash::digest(tt));
53    let half = C::NH / 2;
54    let ke = &hash_tt[..half];
55    let ka = &hash_tt[half..C::NH];
56
57    // Step 2: PRK = KDF.extract(salt=[], ikm=Ka)
58    let prk = C::Kdf::extract(&[], ka);
59
60    // Step 3: KcA || KcB = KDF.expand(PRK, "ConfirmationKeys" || AAD, NH)
61    let mut info = Vec::from(b"ConfirmationKeys" as &[u8]);
62    info.extend_from_slice(aad);
63    let kc = C::Kdf::expand(&prk, &info, C::NH)
64        .map_err(|_| Spake2Error::InternalError("KDF expand failed"))?;
65    let kc_a = &kc[..half];
66    let kc_b = &kc[half..C::NH];
67
68    // Step 4: cA = MAC(KcA, TT), cB = MAC(KcB, TT)
69    let mac_a =
70        C::Mac::mac(kc_a, tt).map_err(|_| Spake2Error::InternalError("MAC computation failed"))?;
71    let mac_b =
72        C::Mac::mac(kc_b, tt).map_err(|_| Spake2Error::InternalError("MAC computation failed"))?;
73
74    let session_key = SharedSecret::new(ke.to_vec());
75
76    if is_party_a {
77        Ok(Spake2Output {
78            session_key,
79            confirmation_mac: mac_a,
80            expected_peer_mac: mac_b,
81        })
82    } else {
83        Ok(Spake2Output {
84            session_key,
85            confirmation_mac: mac_b,
86            expected_peer_mac: mac_a,
87        })
88    }
89}