Skip to main content

matter_crypto/pase/
verifier.rs

1//! Device-side PASE state machine.
2//!
3//! Drives the 5-message PASE handshake (or 3-message known-params path)
4//! from the device's (verifier's) perspective. Sans-IO: the caller feeds raw
5//! TLV bytes in and gets raw TLV bytes out; no sockets, no async, no I/O.
6//!
7//! # Protocol flow (negotiation path — Matter Core Spec §3.10.5)
8//!
9//! ```text
10//! Commissioner (Prover)          Device (Verifier)
11//! ───────────────────────────────────────────────
12//!   → PBKDFParamRequest  ────────────────────────>
13//!                        handle_pbkdf_request()
14//!                        next_message()  [PBKDFParamResponse]
15//!                        <──────── PBKDFParamResponse
16//!   → Pake1              ────────────────────────>
17//!                        handle_pake1()
18//!                        next_message()  [Pake2]
19//!                        <──────── Pake2
20//!   → Pake3              ────────────────────────>
21//!                        handle_pake3()
22//!                        finish() → PaseSessionKeys
23//! ```
24//!
25//! # Known-params path
26//!
27//! When the commissioner already has the PBKDF params cached it skips
28//! `PBKDFParamRequest` and sends Pake1 directly. The verifier's
29//! `AwaitingFirstMessage` state accepts either message kind and branches
30//! automatically.
31//!
32//! ```text
33//!   → Pake1  →  handle_pake1()  →  next_message() [Pake2]
34//!          →  handle_pake3()  →  finish()
35//! ```
36//!
37//! # Verifier values
38//!
39//! The device stores `w0` (32-byte big-endian scalar) and `L` (65-byte
40//! uncompressed P-256 point) computed from the setup PIN at provisioning time.
41//! The PIN is **never** stored after provisioning. See [`PaseVerifier::new`] for
42//! the production constructor and [`PaseVerifier::new_from_pin`] for the
43//! test/convenience path.
44//!
45//! # Transcript context composition
46//!
47//! Same as the prover — `hash_context` in `spake2plus.rs`:
48//! `context = SHA-256("CHIP PAKE V1 Commissioning" || pbkdfReq || pbkdfResp)`.
49//! For the known-params path (no param exchange): `context = SHA-256("CHIP PAKE V1 Commissioning")`.
50//!
51//! # Session key assignment (device = responder)
52//!
53//! From matter.js `NodeSession.ts` with `isInitiator = false`:
54//! ```ts
55//! const decryptKey  = isInitiator ? keys.slice(16, 32) : keys.slice(0, 16);
56//! const encryptKey  = isInitiator ? keys.slice(0, 16)  : keys.slice(16, 32);
57//! ```
58//! So for the device (responder):
59//! - `r2i_key` (device → commissioner, device encrypts) = `blob[16..32]`.
60//! - `i2r_key` (commissioner → device, device decrypts) = `blob[0..16]`.
61//!
62//! The prover (commissioner) uses the opposite assignment, so both sides
63//! produce the same `i2r_key` and `r2i_key` in `PaseSessionKeys` from
64//! their respective perspectives.
65
66use p256::elliptic_curve::group::ff::PrimeField; // PrimeField for Scalar::from_repr
67use ring::rand::{SecureRandom, SystemRandom};
68
69use crate::error::{Error, Result};
70use crate::pase::kdf::{derive_l, derive_w0_w1, validate_params};
71use crate::pase::messages::{
72    Pake1, Pake2, Pake3, PbkdfParamRequest, PbkdfParamResponse, PbkdfParamsInner,
73};
74use crate::pase::spake2plus::{
75    compute_ca, compute_cb, compute_y, compute_z_v_verifier, derive_confirmation_keys,
76    derive_session_keys, hash_context, ka_ke_from_transcript, sample_scalar, transcript_hash,
77    verify_tag,
78};
79use crate::pase::{PaseMessageKind, PasePbkdfParams, PaseSessionKeys};
80use zeroize::Zeroize;
81
82// =============================================================================
83// Internal state enum
84// =============================================================================
85
86/// Internal states of the device-side PASE handshake.
87///
88/// Each variant corresponds to a point in the protocol flow from Matter Core
89/// Spec §3.10.5 as seen by the responder. Named for the *next action* the
90/// state machine expects or is ready to perform.
91///
92/// The `AwaitingFirstMessage` state is a branch point: the verifier accepts
93/// either `PBKDFParamRequest` (negotiation path) or `Pake1` (known-params
94/// path) as its first inbound message. The caller does not need to know which
95/// path the commissioner will choose.
96#[derive(Debug)]
97enum State {
98    /// Waiting for the first inbound message.
99    ///
100    /// The verifier does not know yet whether the commissioner will send
101    /// `PBKDFParamRequest` (negotiation) or `Pake1` (known-params). Both
102    /// are valid here.
103    ///
104    /// `y_scalar` is pre-sampled so that `handle_pake1` is infallible for
105    /// randomness after construction succeeds.
106    AwaitingFirstMessage {
107        w0: p256::Scalar,
108        /// 65-byte uncompressed P-256 point `L = w1·P` stored by the device.
109        l: [u8; 65],
110        params: PasePbkdfParams,
111        y_scalar: p256::Scalar,
112        /// Pre-sampled 32-byte responder nonce, used if the commissioner sends
113        /// a `PBKDFParamRequest`.
114        responder_random: [u8; 32],
115        /// Session ID to include in `PBKDFParamResponse`. Set to 0 by the
116        /// production constructors; overrideable for testing.
117        responder_session_id: u16,
118    },
119
120    /// `PBKDFParamRequest` received; `next_message()` will emit the Response.
121    ///
122    /// Holds the verbatim bytes of the request (for transcript context), the
123    /// responder random, and enough to build the Response.
124    ReadyToSendPbkdfResponse {
125        w0: p256::Scalar,
126        l: [u8; 65],
127        params: PasePbkdfParams,
128        y_scalar: p256::Scalar,
129        /// Verbatim TLV bytes of the `PBKDFParamRequest`, saved for transcript context.
130        request_bytes: Vec<u8>,
131        responder_random: [u8; 32],
132        initiator_random: [u8; 32],
133        responder_session_id: u16,
134    },
135
136    /// `PBKDFParamResponse` sent; waiting for Pake1.
137    ///
138    /// `transcript_context` is already computed as
139    /// `SHA-256(SPAKE_CONTEXT || pbkdfReq || pbkdfResp)`.
140    AwaitingPake1 {
141        w0: p256::Scalar,
142        l: [u8; 65],
143        y_scalar: p256::Scalar,
144        /// SHA-256 context hash to pass into `transcript_hash`.
145        transcript_context: [u8; 32],
146    },
147
148    /// Pake1 received; `next_message()` will emit Pake2 (Y + cB).
149    ReadyToSendPake2 {
150        /// Our Y point (65 bytes, uncompressed P-256), to send in Pake2.
151        y_bytes: [u8; 65],
152        /// Our confirmation tag `cB = HMAC-SHA256(KcB, X)`, to send in Pake2.
153        cb: [u8; 32],
154        /// The expected `cA = HMAC-SHA256(KcA, Y)` that Pake3 must carry.
155        ca_expected: [u8; 32],
156        session_keys: PaseSessionKeys,
157    },
158
159    /// Pake3 received and `cA` verified; `finish()` may be called.
160    Complete { session_keys: PaseSessionKeys },
161
162    /// Sentinel used during `std::mem::replace` state transitions.
163    ///
164    /// This variant is **never observable** to callers: every `mem::replace`
165    /// immediately replaces `Poisoned` with the next real state, or returns
166    /// an error before storing it. If somehow reached, all methods return
167    /// [`Error::HandshakeIncomplete`].
168    Poisoned,
169}
170
171// =============================================================================
172// PaseVerifier
173// =============================================================================
174
175/// Device-side PASE state machine.
176///
177/// Drives the SPAKE2+ handshake from the device's (responder's) perspective.
178/// Sans-IO: the caller is responsible for transmitting and receiving bytes.
179///
180/// # Construction
181///
182/// - [`PaseVerifier::new`] — production path; device stores pre-computed `w0`
183///   and `L` (never the PIN after provisioning).
184/// - [`PaseVerifier::new_from_pin`] — test/convenience path; derives `w0` and
185///   `L` from the PIN via PBKDF2.
186///
187/// # Driving the handshake
188///
189/// 1. Feed the inbound `PBKDFParamRequest` bytes into
190///    [`handle_pbkdf_request`][Self::handle_pbkdf_request] (negotiation path),
191///    or skip to step 3 (known-params path).
192/// 2. Call [`next_message`][Self::next_message] to emit `PBKDFParamResponse`.
193/// 3. Feed the inbound `Pake1` bytes into
194///    [`handle_pake1`][Self::handle_pake1].
195/// 4. Call [`next_message`][Self::next_message] to emit Pake2.
196/// 5. Feed the inbound `Pake3` bytes into
197///    [`handle_pake3`][Self::handle_pake3].
198/// 6. Call [`finish`][Self::finish] to retrieve the [`PaseSessionKeys`].
199///
200/// Use [`expected_inbound`][Self::expected_inbound] at any point to query
201/// which message type the machine is currently waiting for.
202pub struct PaseVerifier {
203    state: State,
204}
205
206impl PaseVerifier {
207    // ─── Public constructors ──────────────────────────────────────────────
208
209    /// Production constructor: device stores pre-computed verification values.
210    ///
211    /// In production the PIN is hashed to `w0` and `L` once at provisioning
212    /// time and the raw PIN is discarded. Pass those stored values here.
213    ///
214    /// Validates `params` against Matter spec §3.10.3 bounds before accepting.
215    ///
216    /// # Parameters
217    ///
218    /// - `w0`: 32-byte big-endian P-256 scalar derived from the PIN.
219    /// - `l`: 65-byte uncompressed P-256 point `L = w1·P`.
220    /// - `params`: PBKDF2 parameters used when `w0`/`L` were derived.
221    /// - `responder_session_id`: the non-zero secured-session id this device
222    ///   advertises (in `PBKDFParamResponse`) for the peer to address it by.
223    ///
224    /// # Errors
225    ///
226    /// - [`Error::PbkdfIterationsTooLow`] if `params.iterations < 1000`.
227    /// - [`Error::PbkdfSaltLengthInvalid`] if `params.salt.len()` ∉ \[16, 32\].
228    /// - [`Error::InvalidScalar`] if the CSPRNG is broken and a non-zero
229    ///   scalar cannot be sampled after 16 attempts (practically impossible).
230    /// - [`Error::PinDerivationFailed`] if the nonce fill fails.
231    pub fn new(
232        w0: [u8; 32],
233        l: [u8; 65],
234        params: PasePbkdfParams,
235        responder_session_id: u16,
236    ) -> Result<Self> {
237        validate_params(params.iterations, &params.salt)?;
238        let rng = SystemRandom::new();
239        Self::new_using_rng(w0, l, params, responder_session_id, &rng)
240    }
241
242    /// Deterministic constructor for testing — accepts an injectable RNG.
243    ///
244    /// Production code should always use [`new`][Self::new].
245    pub(crate) fn new_using_rng(
246        w0_bytes: [u8; 32],
247        l: [u8; 65],
248        params: PasePbkdfParams,
249        responder_session_id: u16,
250        rng: &dyn SecureRandom,
251    ) -> Result<Self> {
252        validate_params(params.iterations, &params.salt)?;
253
254        // Decode `w0` from 32-byte big-endian representation.
255        // `Scalar::from_repr` returns `CtOption<Scalar>` — this is a direct
256        // conversion (no modular reduction needed because the stored value is
257        // already reduced at provisioning time).
258        let w0_opt: Option<p256::Scalar> =
259            p256::Scalar::from_repr(p256::FieldBytes::from(w0_bytes)).into();
260        let w0 = w0_opt.ok_or(Error::InvalidScalar)?;
261        // The zero scalar is not a valid w0 — it would collapse all SPAKE2+ math.
262        // `is_zero()` returns `subtle::Choice`; convert via `bool::from`.
263        if bool::from(p256::elliptic_curve::group::ff::Field::is_zero(&w0)) {
264            return Err(Error::InvalidScalar);
265        }
266
267        let y_scalar = sample_scalar(rng)?;
268        let mut responder_random = [0u8; 32];
269        rng.fill(&mut responder_random)
270            .map_err(|_| Error::PinDerivationFailed)?;
271
272        Ok(Self {
273            state: State::AwaitingFirstMessage {
274                w0,
275                l,
276                params,
277                y_scalar,
278                responder_random,
279                responder_session_id,
280            },
281        })
282    }
283
284    /// Test/convenience constructor: derive `w0` and `L` from the PIN.
285    ///
286    /// In production a device never stores the PIN after provisioning —
287    /// it stores `w0` and `L` instead. This constructor is provided for
288    /// tests and development use where deriving from a PIN is convenient.
289    ///
290    /// # Parameters
291    ///
292    /// - `responder_session_id`: the non-zero secured-session id this device
293    ///   advertises (in `PBKDFParamResponse`) for the peer to address it by.
294    ///
295    /// # Errors
296    ///
297    /// - [`Error::PbkdfIterationsTooLow`] / [`Error::PbkdfSaltLengthInvalid`]
298    ///   if `params` are out of spec.
299    /// - [`Error::PinDerivationFailed`] if PBKDF2 fails.
300    /// - [`Error::InvalidScalar`] if the CSPRNG is broken.
301    pub fn new_from_pin(
302        pin: u32,
303        params: PasePbkdfParams,
304        responder_session_id: u16,
305    ) -> Result<Self> {
306        let rng = SystemRandom::new();
307        Self::new_from_pin_using_rng(pin, params, responder_session_id, &rng)
308    }
309
310    /// Deterministic constructor for testing — derives `w0`/`L` from the PIN
311    /// and accepts an injectable RNG for the SPAKE2+ scalar and nonce.
312    ///
313    /// Production code should always use [`new_from_pin`][Self::new_from_pin].
314    pub(crate) fn new_from_pin_using_rng(
315        pin: u32,
316        params: PasePbkdfParams,
317        responder_session_id: u16,
318        rng: &dyn SecureRandom,
319    ) -> Result<Self> {
320        let (w0_scalar, w1_scalar) = derive_w0_w1(pin, &params.salt, params.iterations)?;
321        let l = derive_l(&w1_scalar);
322        // Encode w0 as 32-byte big-endian for the `new_using_rng` constructor.
323        // `Scalar::to_bytes()` returns `FieldBytes` in big-endian order.
324        let w0_be: p256::FieldBytes = w0_scalar.to_bytes();
325        let mut w0_arr = [0u8; 32];
326        w0_arr.copy_from_slice(&w0_be);
327        Self::new_using_rng(w0_arr, l, params, responder_session_id, rng)
328    }
329
330    /// Deterministic constructor for testing — injects a fixed `y` scalar
331    /// directly, bypassing the RNG. Accepts pre-computed `w0` and `L`.
332    ///
333    /// Uses an all-zero `responder_random` and `responder_session_id = 0`.
334    /// For full matter.js byte-parity tests, use
335    /// [`new_with_scalar_and_random`][Self::new_with_scalar_and_random] instead.
336    ///
337    /// Production code should always use [`new`][Self::new].
338    ///
339    /// # Errors
340    ///
341    /// - [`Error::PbkdfIterationsTooLow`] / [`Error::PbkdfSaltLengthInvalid`]
342    ///   if `params` are out of spec.
343    /// - [`Error::InvalidScalar`] if `w0_bytes` or `y_scalar_bytes` is zero or
344    ///   not a valid P-256 scalar.
345    pub(crate) fn new_with_scalar(
346        w0_bytes: [u8; 32],
347        l: [u8; 65],
348        params: PasePbkdfParams,
349        y_scalar_bytes: [u8; 32],
350    ) -> Result<Self> {
351        Self::new_with_scalar_and_random(w0_bytes, l, params, y_scalar_bytes, [0u8; 32], 0)
352    }
353
354    /// Deterministic constructor for testing — injects a fixed `y` scalar,
355    /// `responder_random`, and `responder_session_id` directly.
356    ///
357    /// Used by `test_support` to reproduce matter.js fixture handshakes that
358    /// capture a specific responder nonce and session ID in
359    /// `PBKDFParamResponse`. Production code always uses [`new`][Self::new].
360    ///
361    /// # Errors
362    ///
363    /// - [`Error::PbkdfIterationsTooLow`] / [`Error::PbkdfSaltLengthInvalid`]
364    ///   if `params` are out of spec.
365    /// - [`Error::InvalidScalar`] if `w0_bytes` or `y_scalar_bytes` is zero or
366    ///   not a valid P-256 scalar.
367    pub(crate) fn new_with_scalar_and_random(
368        w0_bytes: [u8; 32],
369        l: [u8; 65],
370        params: PasePbkdfParams,
371        y_scalar_bytes: [u8; 32],
372        responder_random: [u8; 32],
373        responder_session_id: u16,
374    ) -> Result<Self> {
375        use p256::elliptic_curve::group::ff::Field;
376        validate_params(params.iterations, &params.salt)?;
377
378        let w0_opt: Option<p256::Scalar> =
379            p256::Scalar::from_repr(p256::FieldBytes::from(w0_bytes)).into();
380        let w0 = w0_opt.ok_or(Error::InvalidScalar)?;
381        if bool::from(w0.is_zero()) {
382            return Err(Error::InvalidScalar);
383        }
384
385        let y_opt: Option<p256::Scalar> =
386            p256::Scalar::from_repr(p256::FieldBytes::from(y_scalar_bytes)).into();
387        let y_scalar = y_opt.ok_or(Error::InvalidScalar)?;
388        if bool::from(y_scalar.is_zero()) {
389            return Err(Error::InvalidScalar);
390        }
391
392        Ok(Self {
393            state: State::AwaitingFirstMessage {
394                w0,
395                l,
396                params,
397                y_scalar,
398                responder_random,
399                responder_session_id,
400            },
401        })
402    }
403
404    /// Deterministic constructor for testing — derives `w0`/`L` from the PIN
405    /// and injects a fixed `y` scalar directly, bypassing the RNG.
406    ///
407    /// Used by `test_support` to construct a verifier with a known scalar for
408    /// matter.js byte-parity tests.
409    ///
410    /// Production code should always use [`new_from_pin`][Self::new_from_pin].
411    ///
412    /// # Errors
413    ///
414    /// - [`Error::PbkdfIterationsTooLow`] / [`Error::PbkdfSaltLengthInvalid`]
415    ///   if `params` are out of spec.
416    /// - [`Error::PinDerivationFailed`] if PBKDF2 fails.
417    /// - [`Error::InvalidScalar`] if `y_scalar_bytes` is zero or not a valid
418    ///   P-256 scalar.
419    pub(crate) fn new_from_pin_with_scalar(
420        pin: u32,
421        params: PasePbkdfParams,
422        y_scalar_bytes: [u8; 32],
423    ) -> Result<Self> {
424        let (w0_scalar, w1_scalar) = derive_w0_w1(pin, &params.salt, params.iterations)?;
425        let l = derive_l(&w1_scalar);
426        let w0_be: p256::FieldBytes = w0_scalar.to_bytes();
427        let mut w0_arr = [0u8; 32];
428        w0_arr.copy_from_slice(&w0_be);
429        Self::new_with_scalar(w0_arr, l, params, y_scalar_bytes)
430    }
431
432    // ─── State inspection ─────────────────────────────────────────────────
433
434    /// Returns the message kind the state machine is currently waiting to
435    /// receive, or `None` if the machine is in an outbound-only or completed
436    /// state.
437    ///
438    /// Useful for routing inbound messages in a dispatcher.
439    pub fn expected_inbound(&self) -> Option<PaseMessageKind> {
440        match &self.state {
441            State::AwaitingFirstMessage { .. } => {
442                // Either PbkdfParamRequest or Pake1 is valid; return the more
443                // common (negotiation-path) expectation. Callers that need
444                // strict routing should branch on `handle_pbkdf_request` vs
445                // `handle_pake1`.
446                Some(PaseMessageKind::PbkdfParamRequest)
447            }
448            State::AwaitingPake1 { .. } => Some(PaseMessageKind::Pake1),
449            State::ReadyToSendPake2 { .. } => Some(PaseMessageKind::Pake3),
450            _ => None,
451        }
452    }
453
454    // ─── Inbound handlers ─────────────────────────────────────────────────
455
456    /// Process an inbound `PBKDFParamRequest` message (negotiation path).
457    ///
458    /// Decodes the request, captures the raw bytes for transcript composition,
459    /// and transitions to `ReadyToSendPbkdfResponse`. After this call,
460    /// [`next_message`][Self::next_message] emits `PBKDFParamResponse`.
461    ///
462    /// # Errors
463    ///
464    /// - [`Error::UnexpectedMessage`] if called from any state other than
465    ///   `AwaitingFirstMessage`.
466    /// - [`Error::Codec`] on TLV decoding failure.
467    /// - [`Error::InvalidParameter`] if the request is malformed.
468    pub fn handle_pbkdf_request(&mut self, bytes: &[u8]) -> Result<()> {
469        let prev = std::mem::replace(&mut self.state, State::Poisoned);
470        match prev {
471            State::AwaitingFirstMessage {
472                w0,
473                l,
474                params,
475                y_scalar,
476                responder_random,
477                responder_session_id,
478            } => {
479                // Decode the request so we can capture `initiator_random`.
480                // We also keep the verbatim bytes for transcript context.
481                let req = PbkdfParamRequest::decode(bytes)?;
482
483                self.state = State::ReadyToSendPbkdfResponse {
484                    w0,
485                    l,
486                    params,
487                    y_scalar,
488                    request_bytes: bytes.to_vec(),
489                    responder_random,
490                    initiator_random: req.initiator_random,
491                    responder_session_id,
492                };
493                Ok(())
494            }
495
496            other => {
497                self.state = other;
498                Err(Error::UnexpectedMessage {
499                    expected: PaseMessageKind::PbkdfParamRequest,
500                    got: PaseMessageKind::PbkdfParamRequest,
501                })
502            }
503        }
504    }
505
506    /// Process an inbound `Pake1` message.
507    ///
508    /// Valid in two states:
509    /// - `AwaitingFirstMessage` — commissioner skipped param negotiation
510    ///   (known-params path); context = `SHA-256(SPAKE_CONTEXT)`.
511    /// - `AwaitingPake1` — negotiation complete; context already computed.
512    ///
513    /// After this call, [`next_message`][Self::next_message] emits Pake2.
514    ///
515    /// # Cryptography
516    ///
517    /// 1. Decode X from Pake1 TLV.
518    /// 2. Compute `Y = y·P + w0·N` (verifier's SPAKE2+ share).
519    /// 3. Compute `Z = y·(X − w0·M)` and `V = y·L` (shared secrets).
520    /// 4. Compute the SPAKE2+ transcript hash `TT_HASH`.
521    /// 5. Split `Ka` (first 16 bytes) and `Ke` (last 16 bytes) from `TT_HASH`.
522    /// 6. Derive confirmation keys `KcA`/`KcB` from `Ka`.
523    /// 7. Compute `cB = HMAC-SHA256(KcB, X)` (our confirmation tag to send).
524    /// 8. Compute `cA_expected = HMAC-SHA256(KcA, Y)` (to verify in Pake3).
525    /// 9. Derive session keys from `Ke`.
526    ///
527    /// # Errors
528    ///
529    /// - [`Error::UnexpectedMessage`] if called from the wrong state.
530    /// - [`Error::Codec`] on TLV decoding failure.
531    /// - [`Error::InvalidParameter`] if X is not a valid P-256 point.
532    /// - [`Error::PinDerivationFailed`] on HKDF failure.
533    pub fn handle_pake1(&mut self, bytes: &[u8]) -> Result<()> {
534        let prev = std::mem::replace(&mut self.state, State::Poisoned);
535        match prev {
536            // Known-params path: commissioner sent Pake1 as the first message.
537            State::AwaitingFirstMessage {
538                w0,
539                l,
540                y_scalar,
541                // params, responder_random, and responder_session_id are not used
542                // on the known-params path (no PbkdfParam exchange).
543                ..
544            } => {
545                // context = SHA-256("CHIP PAKE V1 Commissioning") — no param exchange.
546                let transcript_context = hash_context(&[]);
547                self.state = State::Poisoned; // keep Poisoned while we do crypto
548                self.compute_pake2(w0, l, y_scalar, transcript_context, bytes)
549            }
550
551            // Negotiation path: param exchange complete, now handle Pake1.
552            State::AwaitingPake1 {
553                w0,
554                l,
555                y_scalar,
556                transcript_context,
557            } => {
558                self.state = State::Poisoned; // keep Poisoned while we do crypto
559                self.compute_pake2(w0, l, y_scalar, transcript_context, bytes)
560            }
561
562            other => {
563                self.state = other;
564                Err(Error::UnexpectedMessage {
565                    expected: PaseMessageKind::Pake1,
566                    got: PaseMessageKind::Pake1,
567                })
568            }
569        }
570    }
571
572    /// Process an inbound `Pake3` message.
573    ///
574    /// Verifies the commissioner's confirmation tag `cA` using constant-time
575    /// comparison (`subtle::ConstantTimeEq`). If verification succeeds the
576    /// state machine transitions to `Complete` and [`finish`][Self::finish]
577    /// may be called.
578    ///
579    /// # Security
580    ///
581    /// Tag comparison MUST be constant-time. This is enforced by routing
582    /// through `verify_tag` (in `pase::spake2plus`) which uses
583    /// `subtle::ConstantTimeEq`.
584    ///
585    /// # Errors
586    ///
587    /// - [`Error::UnexpectedMessage`] if called before Pake2 was sent.
588    /// - [`Error::ConfirmationTagMismatch`] if `cA` fails constant-time
589    ///   verification (wrong PIN on the commissioner's side).
590    /// - [`Error::Codec`] on TLV decoding failure.
591    pub fn handle_pake3(&mut self, bytes: &[u8]) -> Result<()> {
592        let prev = std::mem::replace(&mut self.state, State::Poisoned);
593        match prev {
594            State::ReadyToSendPake2 {
595                y_bytes: _,
596                cb: _,
597                ca_expected,
598                session_keys,
599            } => {
600                let pake3 = Pake3::decode(bytes)?;
601
602                // CT-eq — never use `==` on HMAC tags.
603                verify_tag(&ca_expected, &pake3.verifier)?;
604
605                self.state = State::Complete { session_keys };
606                Ok(())
607            }
608
609            other => {
610                self.state = other;
611                Err(Error::UnexpectedMessage {
612                    expected: PaseMessageKind::Pake3,
613                    got: PaseMessageKind::Pake3,
614                })
615            }
616        }
617    }
618
619    /// Produce the next outbound message.
620    ///
621    /// - After [`handle_pbkdf_request`][Self::handle_pbkdf_request]: emits
622    ///   `PBKDFParamResponse` (TLV bytes).
623    /// - After [`handle_pake1`][Self::handle_pake1]: emits Pake2 (TLV bytes).
624    ///
625    /// Calling from any other state returns [`Error::UnexpectedMessage`].
626    ///
627    /// # Errors
628    ///
629    /// - [`Error::UnexpectedMessage`] if called from the wrong state.
630    /// - [`Error::Codec`] on TLV encoding failure.
631    pub fn next_message(&mut self) -> Result<Vec<u8>> {
632        let prev = std::mem::replace(&mut self.state, State::Poisoned);
633        match prev {
634            State::ReadyToSendPbkdfResponse {
635                w0,
636                l,
637                params,
638                y_scalar,
639                request_bytes,
640                responder_random,
641                initiator_random,
642                responder_session_id,
643            } => {
644                // §3.10.5 step 2: build and send PBKDFParamResponse.
645                // Include our PBKDF parameters (the commissioner set
646                // has_pbkdf_parameters=false, so we must include them).
647                let resp = PbkdfParamResponse {
648                    initiator_random,
649                    responder_random,
650                    responder_session_id,
651                    pbkdf_parameters: Some(PbkdfParamsInner {
652                        iterations: params.iterations,
653                        salt: params.salt.clone(),
654                    }),
655                    responder_session_params: None,
656                };
657                let resp_bytes = resp.encode()?;
658
659                // Compose transcript context: SHA-256(SPAKE_CONTEXT || req || resp).
660                let transcript_context = hash_context(&[&request_bytes, &resp_bytes]);
661
662                self.state = State::AwaitingPake1 {
663                    w0,
664                    l,
665                    y_scalar,
666                    transcript_context,
667                };
668                Ok(resp_bytes)
669            }
670
671            State::ReadyToSendPake2 {
672                y_bytes,
673                cb,
674                ca_expected,
675                session_keys,
676            } => {
677                // §3.10.5: send Y and cB in Pake2.
678                let pake2_bytes = Pake2 {
679                    y: y_bytes,
680                    verifier: cb,
681                }
682                .encode()?;
683                // Keep ca_expected and session_keys for when Pake3 arrives.
684                self.state = State::ReadyToSendPake2 {
685                    y_bytes,
686                    cb,
687                    ca_expected,
688                    session_keys,
689                };
690                Ok(pake2_bytes)
691            }
692
693            other => {
694                self.state = other;
695                Err(Error::UnexpectedMessage {
696                    expected: PaseMessageKind::PbkdfParamResponse,
697                    got: PaseMessageKind::PbkdfParamResponse,
698                })
699            }
700        }
701    }
702
703    /// Finalise the session and retrieve the derived session keys.
704    ///
705    /// May only be called after [`handle_pake3`][Self::handle_pake3] has
706    /// successfully verified `cA` (i.e., the state machine is in `Complete`).
707    ///
708    /// # Errors
709    ///
710    /// - [`Error::HandshakeIncomplete`] if called before the handshake has
711    ///   completed all phases.
712    pub fn finish(self) -> Result<PaseSessionKeys> {
713        match self.state {
714            State::Complete { session_keys } => Ok(session_keys),
715            _ => Err(Error::HandshakeIncomplete),
716        }
717    }
718}
719
720// =============================================================================
721// Internal helpers
722// =============================================================================
723
724impl PaseVerifier {
725    /// Perform the verifier-side SPAKE2+ cryptography for Pake1 processing.
726    ///
727    /// This is a shared helper called from `handle_pake1` in both the
728    /// known-params and post-negotiation code paths. Sets `self.state` to
729    /// `ReadyToSendPake2` on success.
730    ///
731    /// The state machine must be in `Poisoned` before this call (the caller
732    /// has already `mem::replace`d it out). On error, the machine remains
733    /// `Poisoned` — the caller should not recover from a crypto failure.
734    // kca / kcb are the SPAKE2+ confirmation key names from Matter
735    // Core Spec §3.10 (RFC 9383 §3.3). Renaming would impair
736    // verification against the spec.
737    #[allow(clippy::similar_names)]
738    fn compute_pake2(
739        &mut self,
740        w0: p256::Scalar,
741        l: [u8; 65],
742        y_scalar: p256::Scalar,
743        transcript_context: [u8; 32],
744        pake1_bytes: &[u8],
745    ) -> Result<()> {
746        let pake1 = Pake1::decode(pake1_bytes)?;
747
748        // §3.10.5 verifier side:
749        //   Y = y·P + w0·N
750        //   Z = y·(X − w0·M)
751        //   V = y·L
752        let y_bytes = compute_y(&y_scalar, &w0);
753        let (z_bytes, v_bytes) = compute_z_v_verifier(&y_scalar, &w0, &l, &pake1.x)?;
754
755        // Transcript hash: SHA-256 over all protocol elements.
756        let t_t = transcript_hash(
757            &transcript_context,
758            &pake1.x,
759            &y_bytes,
760            &z_bytes,
761            &v_bytes,
762            &w0,
763        );
764
765        // Split Ka (first 16 bytes) and Ke (last 16 bytes). Both are SPAKE2+
766        // secrets (`ke` is the root session secret); zeroize them after their
767        // last use so no copy lingers on the stack.
768        let (mut ka, mut ke) = ka_ke_from_transcript(&t_t);
769
770        // Derive KcA and KcB from Ka.
771        let (kca, kcb) = derive_confirmation_keys(&ka)?;
772        ka.zeroize();
773
774        // cB = HMAC-SHA256(KcB, X) — sent to the commissioner in Pake2.
775        let cb = compute_cb(&kcb, &pake1.x);
776
777        // cA_expected = HMAC-SHA256(KcA, Y) — verified when Pake3 arrives.
778        let ca_expected = compute_ca(&kca, &y_bytes);
779
780        // Derive 48-byte session key material from Ke.
781        // `build_session_keys` copies `ke` into the `ZeroizeOnDrop`
782        // `PaseSessionKeys`; wipe our local copy afterwards.
783        let session_keys_blob = derive_session_keys(&ke)?;
784        let session_keys = build_session_keys(ke, &session_keys_blob);
785        ke.zeroize();
786
787        self.state = State::ReadyToSendPake2 {
788            y_bytes,
789            cb,
790            ca_expected,
791            session_keys,
792        };
793        Ok(())
794    }
795}
796
797// =============================================================================
798// Session-key builder (device = responder)
799// =============================================================================
800
801/// Build a [`PaseSessionKeys`] from `Ke` and the 48-byte derived key blob.
802///
803/// Key assignment for the device (= responder), per matter.js `NodeSession.ts`
804/// with `isInitiator = false`:
805/// ```ts
806/// const decryptKey = isInitiator ? keys.slice(16, 32) : keys.slice(0, 16);
807/// const encryptKey = isInitiator ? keys.slice(0, 16)  : keys.slice(16, 32);
808/// const attestationKey = keys.slice(32, 48);
809/// ```
810///
811/// - `i2r_key` = `decryptKey` (device decrypts incoming from commissioner).
812/// - `r2i_key` = `encryptKey` (device encrypts outgoing to commissioner).
813///
814/// By convention, `PaseSessionKeys::i2r_key` is the initiator→responder
815/// direction key. From the device's perspective:
816/// - Initiator→responder (i2r): the device *decrypts* → `blob[0..16]` (because
817///   `isInitiator=false` means `decryptKey = blob[0..16]`).
818/// - Responder→initiator (r2i): the device *encrypts* → `blob[16..32]`.
819///
820/// This is the mirror of `prover.rs`'s `build_session_keys`, which assigns:
821/// - `i2r_key` = `blob[0..16]` (commissioner encrypts).
822/// - `r2i_key` = `blob[16..32]` (commissioner decrypts).
823///
824/// Both sides end up with the same `i2r_key` and `r2i_key` values.
825fn build_session_keys(ke: [u8; 16], blob_48: &[u8; 48]) -> PaseSessionKeys {
826    let mut i2r_key = [0u8; 16];
827    let mut r2i_key = [0u8; 16];
828    let mut attestation_key = [0u8; 16];
829
830    // Device = responder (isInitiator = false):
831    //   decryptKey (i2r) = blob[0..16]
832    //   encryptKey (r2i) = blob[16..32]
833    i2r_key.copy_from_slice(&blob_48[0..16]);
834    r2i_key.copy_from_slice(&blob_48[16..32]);
835    attestation_key.copy_from_slice(&blob_48[32..48]);
836
837    PaseSessionKeys {
838        ke,
839        i2r_key,
840        r2i_key,
841        attestation_key,
842    }
843}
844
845// =============================================================================
846// Tests
847// =============================================================================
848
849#[cfg(test)]
850#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
851mod tests {
852    use super::*;
853    use crate::pase::messages::PbkdfParamResponse;
854
855    /// Shared PBKDF params used across tests: valid per spec §3.10.3.
856    fn test_params() -> PasePbkdfParams {
857        PasePbkdfParams {
858            iterations: 1_000,
859            salt: vec![0x42u8; 16],
860        }
861    }
862
863    /// PIN used in tests; matches the matter.js canonical test PIN.
864    const TEST_PIN: u32 = 20_202_021;
865
866    // ─── Construction ─────────────────────────────────────────────────────
867
868    #[test]
869    fn new_from_pin_accepts_valid_params() {
870        let _ = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
871    }
872
873    #[test]
874    fn new_from_pin_rejects_low_iterations() {
875        let params = PasePbkdfParams {
876            iterations: 999,
877            salt: vec![0u8; 16],
878        };
879        assert!(matches!(
880            PaseVerifier::new_from_pin(TEST_PIN, params, 0x0033),
881            Err(Error::PbkdfIterationsTooLow(999))
882        ));
883    }
884
885    #[test]
886    fn new_from_pin_rejects_short_salt() {
887        let params = PasePbkdfParams {
888            iterations: 1_000,
889            salt: vec![0u8; 15],
890        };
891        assert!(matches!(
892            PaseVerifier::new_from_pin(TEST_PIN, params, 0x0033),
893            Err(Error::PbkdfSaltLengthInvalid(15))
894        ));
895    }
896
897    #[test]
898    fn new_raw_rejects_invalid_w0_scalar() {
899        // All-zeros is not a valid P-256 scalar (zero scalar is the identity — rejected).
900        let w0_zero = [0u8; 32];
901        let l = [0x04u8; 65];
902        let params = test_params();
903        assert!(matches!(
904            PaseVerifier::new(w0_zero, l, params, 0x0033),
905            Err(Error::InvalidScalar)
906        ));
907    }
908
909    // ─── expected_inbound ─────────────────────────────────────────────────
910
911    #[test]
912    fn expected_inbound_after_construction_is_pbkdf_request() {
913        let v = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
914        assert_eq!(
915            v.expected_inbound(),
916            Some(PaseMessageKind::PbkdfParamRequest)
917        );
918    }
919
920    // ─── Negotiation path state transitions ───────────────────────────────
921
922    #[test]
923    fn handle_pbkdf_request_advances_state() {
924        let mut v = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
925
926        // Build a minimal valid PbkdfParamRequest.
927        let req = PbkdfParamRequest {
928            initiator_random: [0x11u8; 32],
929            initiator_session_id: 0,
930            passcode_id: 0,
931            has_pbkdf_parameters: false,
932            initiator_session_params: None,
933        };
934        let req_bytes = req.encode().unwrap();
935
936        v.handle_pbkdf_request(&req_bytes).unwrap();
937        // After request, ready to send response — no inbound expected.
938        assert_eq!(v.expected_inbound(), None);
939    }
940
941    #[test]
942    fn next_message_after_pbkdf_request_emits_response() {
943        let mut v = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
944
945        let req = PbkdfParamRequest {
946            initiator_random: [0x11u8; 32],
947            initiator_session_id: 0,
948            passcode_id: 0,
949            has_pbkdf_parameters: false,
950            initiator_session_params: None,
951        };
952        v.handle_pbkdf_request(&req.encode().unwrap()).unwrap();
953
954        let resp_bytes = v.next_message().unwrap();
955        // PbkdfParamResponse is an anonymous TLV structure.
956        assert_eq!(resp_bytes[0], 0x15, "first byte must be 0x15 (anon struct)");
957
958        // After sending response, waiting for Pake1.
959        assert_eq!(v.expected_inbound(), Some(PaseMessageKind::Pake1));
960
961        // Round-trip: the response must decode successfully.
962        let decoded = PbkdfParamResponse::decode(&resp_bytes).unwrap();
963        // PBKDF params must be present (commissioner said has_pbkdf_parameters=false).
964        assert!(decoded.pbkdf_parameters.is_some());
965        let inner = decoded.pbkdf_parameters.unwrap();
966        assert_eq!(inner.iterations, 1_000);
967        assert_eq!(inner.salt, vec![0x42u8; 16]);
968    }
969
970    // ─── Out-of-order rejection ───────────────────────────────────────────
971
972    #[test]
973    fn out_of_order_handle_pake3_returns_unexpected_message() {
974        // Verifier is AwaitingFirstMessage; Pake3 is wrong here.
975        let mut v = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
976        let dummy_pake3 = Pake3 {
977            verifier: [0x00u8; 32],
978        };
979        let pake3_bytes = dummy_pake3.encode().unwrap();
980        assert!(matches!(
981            v.handle_pake3(&pake3_bytes),
982            Err(Error::UnexpectedMessage { .. })
983        ));
984    }
985
986    #[test]
987    fn finish_before_complete_returns_handshake_incomplete() {
988        let v = PaseVerifier::new_from_pin(TEST_PIN, test_params(), 0x0033).unwrap();
989        assert!(matches!(v.finish(), Err(Error::HandshakeIncomplete)));
990    }
991
992    // ─── Tag mismatch rejection ───────────────────────────────────────────
993
994    #[test]
995    fn handle_pake3_rejects_wrong_ca_tag() {
996        use crate::pase::kdf::derive_w0_w1;
997        use crate::pase::spake2plus::{compute_x, sample_scalar};
998        use ring::rand::SystemRandom;
999
1000        let rng = SystemRandom::new();
1001        let params = test_params();
1002
1003        // Verifier side.
1004        let mut v = PaseVerifier::new_from_pin(TEST_PIN, params.clone(), 0x0033).unwrap();
1005
1006        // Derive prover-side values to build a plausible Pake1.
1007        let (w0_scalar, _w1_scalar) =
1008            derive_w0_w1(TEST_PIN, &params.salt, params.iterations).unwrap();
1009        let x_scalar = sample_scalar(&rng).unwrap();
1010        let x_bytes = compute_x(&x_scalar, &w0_scalar);
1011        let pake1_bytes = Pake1 { x: x_bytes }.encode().unwrap();
1012
1013        // Drive verifier: known-params path (skip PbkdfParamRequest).
1014        v.handle_pake1(&pake1_bytes).unwrap();
1015        let _pake2_bytes = v.next_message().unwrap();
1016
1017        // Send Pake3 with a wrong cA tag (all zeros — almost certainly wrong).
1018        let wrong_pake3 = Pake3 {
1019            verifier: [0x00u8; 32],
1020        };
1021        let wrong_pake3_bytes = wrong_pake3.encode().unwrap();
1022        assert!(matches!(
1023            v.handle_pake3(&wrong_pake3_bytes),
1024            Err(Error::ConfirmationTagMismatch)
1025        ));
1026    }
1027
1028    // ─── responder_session_id propagation ────────────────────────────────
1029
1030    #[test]
1031    fn verifier_advertises_responder_session_id() {
1032        let params = test_params();
1033        let mut verifier = PaseVerifier::new_from_pin(TEST_PIN, params, 0x0033).unwrap();
1034        // Drive a PBKDFParamRequest in so next_message() emits the response.
1035        let mut prover =
1036            crate::pase::prover::PaseProver::new_with_negotiation(TEST_PIN, 0x0001).unwrap();
1037        let req = prover.start().unwrap();
1038        verifier.handle_pbkdf_request(&req).unwrap();
1039        let resp = verifier.next_message().unwrap();
1040        let decoded = crate::pase::messages::PbkdfParamResponse::decode(&resp).unwrap();
1041        assert_eq!(decoded.responder_session_id, 0x0033);
1042    }
1043
1044    // ─── Known-params path (Pake1 as first message) ───────────────────────
1045
1046    #[test]
1047    fn handle_pake1_as_first_message_succeeds() {
1048        use crate::pase::kdf::derive_w0_w1;
1049        use crate::pase::spake2plus::{compute_x, sample_scalar};
1050        use ring::rand::SystemRandom;
1051
1052        let rng = SystemRandom::new();
1053        let params = test_params();
1054        let mut v = PaseVerifier::new_from_pin(TEST_PIN, params.clone(), 0x0033).unwrap();
1055
1056        let (w0_scalar, _) = derive_w0_w1(TEST_PIN, &params.salt, params.iterations).unwrap();
1057        let x_scalar = sample_scalar(&rng).unwrap();
1058        let x_bytes = compute_x(&x_scalar, &w0_scalar);
1059        let pake1_bytes = Pake1 { x: x_bytes }.encode().unwrap();
1060
1061        // Feed Pake1 directly — no PbkdfParamRequest.
1062        v.handle_pake1(&pake1_bytes).unwrap();
1063        // Ready to send Pake2.
1064        assert_eq!(v.expected_inbound(), Some(PaseMessageKind::Pake3));
1065
1066        let pake2_bytes = v.next_message().unwrap();
1067        assert_eq!(pake2_bytes[0], 0x15, "Pake2 must be anon TLV structure");
1068
1069        let decoded = Pake2::decode(&pake2_bytes).unwrap();
1070        assert_eq!(
1071            decoded.y[0], 0x04,
1072            "Y must have SEC1 uncompressed prefix 0x04"
1073        );
1074        assert_eq!(decoded.verifier.len(), 32);
1075    }
1076}