Skip to main content

pakery_spake2plus/
verifier.rs

1//! SPAKE2+ Verifier (server) state machine.
2//!
3//! The Verifier stores `(w0, L)` where `L = w1*G`. It does not know
4//! the password or `w1` directly.
5
6use alloc::vec::Vec;
7use rand_core::CryptoRng;
8use subtle::ConstantTimeEq;
9use zeroize::{Zeroize, Zeroizing};
10
11use pakery_core::crypto::CpaceGroup;
12use pakery_core::SharedSecret;
13
14use crate::ciphersuite::Spake2PlusCiphersuite;
15use crate::encoding::build_transcript;
16use crate::error::Spake2PlusError;
17use crate::transcript::{derive_key_schedule, Spake2PlusOutput};
18
19/// State held by the Verifier between sending (shareV, confirmV) and receiving confirmP.
20pub struct VerifierState {
21    expected_confirm_p: Vec<u8>,
22    session_key: SharedSecret,
23}
24
25impl Zeroize for VerifierState {
26    fn zeroize(&mut self) {
27        self.expected_confirm_p.zeroize();
28        // SharedSecret also zeroizes on its own drop; clearing it here keeps
29        // `zeroize()` exhaustive over every secret field.
30        self.session_key.zeroize();
31    }
32}
33
34impl Drop for VerifierState {
35    fn drop(&mut self) {
36        self.zeroize();
37    }
38}
39
40impl VerifierState {
41    /// Finish the SPAKE2+ protocol by verifying the Prover's confirmation MAC.
42    pub fn finish(mut self, confirm_p: &[u8]) -> Result<Spake2PlusOutput, Spake2PlusError> {
43        // ctgrind: the verification outcome is a public accept/reject
44        // decision; the comparison itself stays constant-time.
45        if !pakery_core::ct::declassify_choice(self.expected_confirm_p.ct_eq(confirm_p)) {
46            return Err(Spake2PlusError::ConfirmationFailed);
47        }
48
49        // Move session_key out; the placeholder empty secret is dropped
50        // (and zeroized) when `self` drops.
51        let session_key =
52            core::mem::replace(&mut self.session_key, SharedSecret::new(alloc::vec![]));
53        Ok(Spake2PlusOutput { session_key })
54    }
55}
56
57/// SPAKE2+ Verifier: processes the Prover's first message and generates the response.
58pub struct Verifier<C: Spake2PlusCiphersuite>(core::marker::PhantomData<C>);
59
60impl<C: Spake2PlusCiphersuite> Verifier<C> {
61    /// Start the SPAKE2+ protocol as the Verifier.
62    ///
63    /// `w0` is the password-derived scalar stored during registration.
64    /// `l_bytes` is the verifier point `L = w1*G` stored during registration.
65    ///
66    /// Returns `(shareV_bytes, confirmV, state)` where `shareV_bytes` and `confirmV`
67    /// are sent to the Prover.
68    pub fn start(
69        share_p_bytes: &[u8],
70        w0: &<C::Group as CpaceGroup>::Scalar,
71        l_bytes: &[u8],
72        context: &[u8],
73        id_prover: &[u8],
74        id_verifier: &[u8],
75        rng: &mut impl CryptoRng,
76    ) -> Result<(Vec<u8>, Vec<u8>, VerifierState), Spake2PlusError> {
77        let y = C::Group::random_scalar(rng);
78        Self::start_inner(
79            share_p_bytes,
80            w0,
81            l_bytes,
82            &y,
83            context,
84            id_prover,
85            id_verifier,
86        )
87    }
88
89    /// Start with a deterministic scalar (for testing).
90    ///
91    /// # Security
92    ///
93    /// Using a non-random scalar completely breaks security.
94    /// This method is gated behind the `test-utils` feature and must
95    /// only be used for RFC test vector validation.
96    #[cfg(feature = "test-utils")]
97    pub fn start_with_scalar(
98        share_p_bytes: &[u8],
99        w0: &<C::Group as CpaceGroup>::Scalar,
100        l_bytes: &[u8],
101        y: &<C::Group as CpaceGroup>::Scalar,
102        context: &[u8],
103        id_prover: &[u8],
104        id_verifier: &[u8],
105    ) -> Result<(Vec<u8>, Vec<u8>, VerifierState), Spake2PlusError> {
106        Self::start_inner(
107            share_p_bytes,
108            w0,
109            l_bytes,
110            y,
111            context,
112            id_prover,
113            id_verifier,
114        )
115    }
116
117    fn start_inner(
118        share_p_bytes: &[u8],
119        w0: &<C::Group as CpaceGroup>::Scalar,
120        l_bytes: &[u8],
121        y: &<C::Group as CpaceGroup>::Scalar,
122        context: &[u8],
123        id_prover: &[u8],
124        id_verifier: &[u8],
125    ) -> Result<(Vec<u8>, Vec<u8>, VerifierState), Spake2PlusError> {
126        // Decode shareP and reject identity (defense-in-depth)
127        let share_p = C::Group::from_bytes(share_p_bytes)?;
128        if share_p.is_identity() {
129            return Err(Spake2PlusError::IdentityPoint);
130        }
131
132        // Decode M from ciphersuite constants
133        let m = C::Group::from_bytes(C::M_BYTES)?;
134
135        // Decode L (verifier point)
136        let l = C::Group::from_bytes(l_bytes)?;
137
138        // Decode N from ciphersuite constants
139        let n = C::Group::from_bytes(C::N_BYTES)?;
140
141        // shareV = y*G + w0*N
142        let y_g = C::Group::basepoint_mul(y);
143        let w0_n = n.scalar_mul(w0);
144        let share_v = y_g.add(&w0_n);
145
146        let share_v_bytes = share_v.to_bytes();
147        // ctgrind: shareV is the wire key share — public by protocol design.
148        pakery_core::ct::declassify(&share_v_bytes);
149
150        // tmp = shareP - w0*M (= x*G)
151        let w0_m = m.scalar_mul(w0);
152        let tmp = share_p.add(&w0_m.negate());
153
154        // Z = y * tmp (= y*x*G)
155        let z = tmp.scalar_mul(y);
156
157        // V = y * L (= y*w1*G)
158        let v = l.scalar_mul(y);
159
160        // Check Z != identity, V != identity
161        if z.is_identity() {
162            return Err(Spake2PlusError::IdentityPoint);
163        }
164        if v.is_identity() {
165            return Err(Spake2PlusError::IdentityPoint);
166        }
167
168        let z_bytes = Zeroizing::new(z.to_bytes());
169        let v_bytes = Zeroizing::new(v.to_bytes());
170        let w0_bytes = Zeroizing::new(C::Group::scalar_to_bytes(w0));
171        // ctgrind: Z, V, and w0 are secret transcript inputs (P-256 group
172        // ops launder taint through the scalar parse, so re-mark at the
173        // byte boundary).
174        pakery_core::ct::mark_secret(&z_bytes);
175        pakery_core::ct::mark_secret(&v_bytes);
176        pakery_core::ct::mark_secret(&w0_bytes);
177
178        // Use canonical group element encoding for M and N in the transcript
179        // (same encoding as all other group elements, e.g. uncompressed for P-256).
180        let m_bytes = m.to_bytes();
181        let n_bytes = n.to_bytes();
182
183        // Build transcript TT (10 fields)
184        let tt = build_transcript(
185            context,
186            id_prover,
187            id_verifier,
188            &m_bytes,
189            &n_bytes,
190            share_p_bytes,
191            &share_v_bytes,
192            &z_bytes,
193            &v_bytes,
194            &w0_bytes,
195        );
196
197        // Derive key schedule
198        let mut ks = derive_key_schedule::<C>(&tt, share_p_bytes, &share_v_bytes)?;
199
200        let state = VerifierState {
201            expected_confirm_p: core::mem::take(&mut ks.confirm_p),
202            session_key: core::mem::replace(&mut ks.session_key, SharedSecret::new(Vec::new())),
203        };
204
205        let confirm_v = core::mem::take(&mut ks.confirm_v);
206        // ctgrind: confirmV goes on the wire — public once sent. The
207        // expected confirmP stays secret until compared.
208        pakery_core::ct::declassify(&confirm_v);
209        Ok((share_v_bytes, confirm_v, state))
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    /// The manual `Zeroize` impl (called from `Drop`) must clear every
218    /// secret field (roadmap item 7: catches a future field added without
219    /// zeroization).
220    #[test]
221    fn zeroize_clears_all_secret_fields() {
222        let mut state = VerifierState {
223            expected_confirm_p: alloc::vec![0xAA; 64],
224            session_key: SharedSecret::new(alloc::vec![0xBB; 32]),
225        };
226        state.zeroize();
227        assert!(state.expected_confirm_p.is_empty());
228        assert!(state.session_key.as_bytes().is_empty());
229    }
230}