Skip to main content

pakery_spake2plus/
prover.rs

1//! SPAKE2+ Prover (client) state machine.
2//!
3//! The Prover knows the password and derives `(w0, w1)` from it.
4
5use alloc::vec::Vec;
6use rand_core::CryptoRng;
7use subtle::ConstantTimeEq;
8use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
9
10use pakery_core::crypto::CpaceGroup;
11use pakery_core::SharedSecret;
12
13use crate::ciphersuite::Spake2PlusCiphersuite;
14use crate::encoding::build_transcript;
15use crate::error::Spake2PlusError;
16use crate::transcript::derive_key_schedule;
17
18/// State held by the Prover between sending shareP and receiving (shareV, confirmV).
19#[derive(Zeroize, ZeroizeOnDrop)]
20pub struct ProverState<C: Spake2PlusCiphersuite> {
21    x: <C::Group as CpaceGroup>::Scalar,
22    w0: <C::Group as CpaceGroup>::Scalar,
23    w1: <C::Group as CpaceGroup>::Scalar,
24    share_p_bytes: Vec<u8>,
25    context: Vec<u8>,
26    id_prover: Vec<u8>,
27    id_verifier: Vec<u8>,
28    #[zeroize(skip)]
29    _marker: core::marker::PhantomData<C>,
30}
31
32/// Output returned by the Prover after verifying confirmV.
33#[derive(Zeroize, ZeroizeOnDrop)]
34pub struct ProverOutput {
35    /// The shared session key.
36    #[zeroize(skip)]
37    pub session_key: SharedSecret,
38    /// The Prover's confirmation MAC to send to the Verifier.
39    pub confirm_p: Vec<u8>,
40}
41
42impl ProverOutput {
43    /// Consume the output and yield the session key.
44    ///
45    /// Because [`ProverOutput`] derives `ZeroizeOnDrop`, it cannot be
46    /// pattern-destructured by the caller. This consumer extracts the
47    /// session key cleanly without the boilerplate `mem::replace` shim
48    /// users would otherwise have to write themselves.
49    ///
50    /// Both fields remain `pub`, so to keep `confirm_p` while consuming
51    /// `session_key`, clone it first:
52    ///
53    /// ```ignore
54    /// let confirm_p = output.confirm_p.clone();
55    /// let session_key = output.into_session_key();
56    /// ```
57    #[must_use]
58    pub fn into_session_key(mut self) -> SharedSecret {
59        core::mem::replace(&mut self.session_key, SharedSecret::new(Vec::new()))
60    }
61
62    /// Consume the output and yield the `confirmP` MAC.
63    ///
64    /// Mirror of [`Self::into_session_key`]. To also keep `session_key`,
65    /// clone it first:
66    ///
67    /// ```ignore
68    /// let session_key = output.session_key.clone();
69    /// let confirm_p = output.into_confirm_p();
70    /// ```
71    #[must_use]
72    pub fn into_confirm_p(mut self) -> Vec<u8> {
73        core::mem::take(&mut self.confirm_p)
74    }
75}
76
77/// SPAKE2+ Prover: generates the first message and processes the Verifier's response.
78pub struct Prover<C: Spake2PlusCiphersuite>(core::marker::PhantomData<C>);
79
80impl<C: Spake2PlusCiphersuite> Prover<C> {
81    /// Start the SPAKE2+ protocol as the Prover.
82    ///
83    /// `w0` and `w1` are the password-derived scalars. The caller is responsible
84    /// for password stretching.
85    ///
86    /// Returns `(shareP_bytes, state)` where `shareP_bytes` is sent to the Verifier.
87    pub fn start(
88        w0: &<C::Group as CpaceGroup>::Scalar,
89        w1: &<C::Group as CpaceGroup>::Scalar,
90        context: &[u8],
91        id_prover: &[u8],
92        id_verifier: &[u8],
93        rng: &mut impl CryptoRng,
94    ) -> Result<(Vec<u8>, ProverState<C>), Spake2PlusError> {
95        let x = C::Group::random_scalar(rng);
96        Self::start_inner(w0.clone(), w1.clone(), x, context, id_prover, id_verifier)
97    }
98
99    /// Start with a deterministic scalar (for testing).
100    ///
101    /// # Security
102    ///
103    /// Using a non-random scalar completely breaks security.
104    /// This method is gated behind the `test-utils` feature and must
105    /// only be used for RFC test vector validation.
106    #[cfg(feature = "test-utils")]
107    pub fn start_with_scalar(
108        w0: &<C::Group as CpaceGroup>::Scalar,
109        w1: &<C::Group as CpaceGroup>::Scalar,
110        x: &<C::Group as CpaceGroup>::Scalar,
111        context: &[u8],
112        id_prover: &[u8],
113        id_verifier: &[u8],
114    ) -> Result<(Vec<u8>, ProverState<C>), Spake2PlusError> {
115        Self::start_inner(
116            w0.clone(),
117            w1.clone(),
118            x.clone(),
119            context,
120            id_prover,
121            id_verifier,
122        )
123    }
124
125    fn start_inner(
126        w0: <C::Group as CpaceGroup>::Scalar,
127        w1: <C::Group as CpaceGroup>::Scalar,
128        x: <C::Group as CpaceGroup>::Scalar,
129        context: &[u8],
130        id_prover: &[u8],
131        id_verifier: &[u8],
132    ) -> Result<(Vec<u8>, ProverState<C>), Spake2PlusError> {
133        // Decode M from ciphersuite constants
134        let m = C::Group::from_bytes(C::M_BYTES)?;
135
136        // shareP = x*G + w0*M
137        let x_g = C::Group::basepoint_mul(&x);
138        let w0_m = m.scalar_mul(&w0);
139        let share_p = x_g.add(&w0_m);
140
141        let share_p_bytes = share_p.to_bytes();
142        // ctgrind: shareP is the wire key share — public by protocol design.
143        pakery_core::ct::declassify(&share_p_bytes);
144
145        let state = ProverState {
146            x,
147            w0,
148            w1,
149            share_p_bytes: share_p_bytes.clone(),
150            context: context.to_vec(),
151            id_prover: id_prover.to_vec(),
152            id_verifier: id_verifier.to_vec(),
153            _marker: core::marker::PhantomData,
154        };
155
156        Ok((share_p_bytes, state))
157    }
158}
159
160impl<C: Spake2PlusCiphersuite> ProverState<C> {
161    /// Finish the SPAKE2+ protocol by processing the Verifier's response.
162    ///
163    /// The Prover receives `(shareV_bytes, confirm_v)` from the Verifier,
164    /// verifies `confirm_v`, and returns `ProverOutput` containing the session
165    /// key and `confirm_p` to send back.
166    pub fn finish(
167        self,
168        share_v_bytes: &[u8],
169        confirm_v: &[u8],
170    ) -> Result<ProverOutput, Spake2PlusError> {
171        // Decode shareV and reject identity (defense-in-depth)
172        let share_v = C::Group::from_bytes(share_v_bytes)?;
173        if share_v.is_identity() {
174            return Err(Spake2PlusError::IdentityPoint);
175        }
176
177        // Decode N from ciphersuite constants
178        let n = C::Group::from_bytes(C::N_BYTES)?;
179
180        // tmp = shareV - w0*N (= y*G)
181        let w0_n = n.scalar_mul(&self.w0);
182        let tmp = share_v.add(&w0_n.negate());
183
184        // Z = x * tmp (= x*y*G, since cofactor h=1 for ristretto255)
185        let z = tmp.scalar_mul(&self.x);
186
187        // V = w1 * tmp (= w1*y*G)
188        let v = tmp.scalar_mul(&self.w1);
189
190        // Check Z != identity, V != identity
191        if z.is_identity() {
192            return Err(Spake2PlusError::IdentityPoint);
193        }
194        if v.is_identity() {
195            return Err(Spake2PlusError::IdentityPoint);
196        }
197
198        let z_bytes = Zeroizing::new(z.to_bytes());
199        let v_bytes = Zeroizing::new(v.to_bytes());
200        let w0_bytes = Zeroizing::new(C::Group::scalar_to_bytes(&self.w0));
201        // ctgrind: Z, V, and w0 are secret transcript inputs (P-256 group
202        // ops launder taint through the scalar parse, so re-mark at the
203        // byte boundary).
204        pakery_core::ct::mark_secret(&z_bytes);
205        pakery_core::ct::mark_secret(&v_bytes);
206        pakery_core::ct::mark_secret(&w0_bytes);
207
208        // Decode M and N to get canonical group element encoding for transcript.
209        // This ensures M/N use the same encoding as other group elements (e.g.
210        // uncompressed SEC1 for P-256), regardless of how they are stored in the
211        // ciphersuite constants.
212        let m = C::Group::from_bytes(C::M_BYTES)?;
213        let n_point = C::Group::from_bytes(C::N_BYTES)?;
214        let m_bytes = m.to_bytes();
215        let n_bytes = n_point.to_bytes();
216
217        // Build transcript TT (10 fields)
218        let tt = build_transcript(
219            &self.context,
220            &self.id_prover,
221            &self.id_verifier,
222            &m_bytes,
223            &n_bytes,
224            &self.share_p_bytes,
225            share_v_bytes,
226            &z_bytes,
227            &v_bytes,
228            &w0_bytes,
229        );
230
231        // Derive key schedule
232        let mut ks = derive_key_schedule::<C>(&tt, &self.share_p_bytes, share_v_bytes)?;
233
234        // Verify confirmV: MAC(K_confirmV, shareP)
235        // ctgrind: the verification outcome is a public accept/reject
236        // decision; the comparison itself stays constant-time.
237        if !pakery_core::ct::declassify_choice(ks.confirm_v.ct_eq(confirm_v)) {
238            return Err(Spake2PlusError::ConfirmationFailed);
239        }
240
241        let confirm_p = core::mem::take(&mut ks.confirm_p);
242        // ctgrind: confirmP goes on the wire — public once sent.
243        pakery_core::ct::declassify(&confirm_p);
244        Ok(ProverOutput {
245            session_key: core::mem::replace(&mut ks.session_key, SharedSecret::new(Vec::new())),
246            confirm_p,
247        })
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::test_mocks::MockSuite;
255    use alloc::vec;
256
257    /// Calling `.zeroize()` on a live value must clear every secret field
258    /// (roadmap item 7: catches a future field added without zeroization).
259    #[test]
260    fn state_zeroize_clears_all_secret_fields() {
261        let mut state = ProverState::<MockSuite> {
262            x: [0xAA; 32],
263            w0: [0xBB; 32],
264            w1: [0xCC; 32],
265            share_p_bytes: vec![0xDD; 32],
266            context: vec![0xEE; 8],
267            id_prover: vec![0xFF; 8],
268            id_verifier: vec![0x11; 8],
269            _marker: core::marker::PhantomData,
270        };
271        state.zeroize();
272        assert_eq!(state.x, [0u8; 32]);
273        assert_eq!(state.w0, [0u8; 32]);
274        assert_eq!(state.w1, [0u8; 32]);
275        assert!(state.share_p_bytes.is_empty());
276        assert!(state.context.is_empty());
277        assert!(state.id_prover.is_empty());
278        assert!(state.id_verifier.is_empty());
279    }
280
281    /// `session_key` is `#[zeroize(skip)]` by design: `SharedSecret` zeroizes
282    /// itself on its own drop, so it must survive the struct-level call.
283    #[test]
284    fn output_zeroize_clears_all_secret_fields() {
285        let mut output = ProverOutput {
286            session_key: SharedSecret::new(vec![0xAA; 32]),
287            confirm_p: vec![0xBB; 64],
288        };
289        output.zeroize();
290        assert!(output.confirm_p.is_empty());
291        // Skipped field is untouched (self-zeroizing on its own drop).
292        assert_eq!(output.session_key.as_bytes(), &[0xAA; 32]);
293    }
294}