Skip to main content

matter_crypto/pase/
mod.rs

1//! Matter PASE (Password Authenticated Session Establishment).
2//!
3//! Implementation lands across phases:
4//! - M3.1 (current): math + KDF primitives in submodules.
5//! - M3.2: wire-format messages + PaseProver/PaseVerifier state machines.
6//! - M3.3: matter.js byte-parity verification + readiness markers.
7
8pub(crate) mod kdf;
9pub(crate) mod messages;
10pub(crate) mod prover;
11pub(crate) mod spake2plus;
12pub(crate) mod verifier;
13
14pub use kdf::pake_passcode_verifier;
15pub use prover::PaseProver;
16pub use verifier::PaseVerifier;
17
18/// Identifies one of the 5 PASE message types. Used by
19/// [`crate::Error::UnexpectedMessage`] and `expected_inbound()` accessors
20/// on the state machines (added in M3.2).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum PaseMessageKind {
24    /// Commissioner -> verifier, negotiation path.
25    PbkdfParamRequest,
26    /// Verifier -> commissioner, negotiation path.
27    PbkdfParamResponse,
28    /// Commissioner -> verifier, X point.
29    Pake1,
30    /// Verifier -> commissioner, Y point + cB confirmation.
31    Pake2,
32    /// Commissioner -> verifier, cA confirmation.
33    Pake3,
34}
35
36/// Negotiable PASE PBKDF parameters (Matter spec §3.10.3).
37///
38/// Produced by decoding a `PbkdfParamResponse` and consumed by both
39/// `PaseProver` and `PaseVerifier` state machines.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct PasePbkdfParams {
42    /// PBKDF2 iteration count. Matter spec §3.10.3 requires ≥ 1000.
43    pub iterations: u32,
44    /// PBKDF2 salt. Matter spec §3.10.3 requires 16–32 bytes.
45    pub salt: Vec<u8>,
46}
47
48/// Session keys produced by a completed PASE handshake (spec §3.10.7).
49///
50/// Contains the 16-byte shared secret `Ke` (`TT_HASH`\[16..32\]) and the
51/// three per-session keys derived from it via HKDF `"SessionKeys"`.
52///
53/// # Key layout (matter.js `NodeSession.ts`, commissioner = initiator)
54///
55/// ```text
56/// blob = HKDF-SHA256(Ke, salt=[], "SessionKeys", 48)
57/// i2r_key  = blob[0..16]   (initiator→responder; encrypt for commissioner)
58/// r2i_key  = blob[16..32]  (responder→initiator; decrypt for commissioner)
59/// attestation_key = blob[32..48]
60/// ```
61///
62/// # Secret hygiene
63///
64/// This type carries live symmetric key material. It implements
65/// [`zeroize::ZeroizeOnDrop`] so the key bytes are wiped from memory when the
66/// value is dropped, and its [`Debug`] impl redacts every field (printing
67/// `PaseSessionKeys { .. }`) so key bytes never reach logs. Equality is
68/// intentionally *not* derived: comparing session keys with the variable-time
69/// `==` would be a timing side-channel, and no caller needs it (tests compare
70/// individual byte-array fields directly).
71#[derive(Clone, zeroize::ZeroizeOnDrop)]
72pub struct PaseSessionKeys {
73    /// Shared symmetric secret (`Ke`): `TT_HASH`\[16..32\].
74    ///
75    /// This is the raw SPAKE2+ session secret. Higher layers can re-derive
76    /// `i2r_key`, `r2i_key`, and `attestation_key` from this alone.
77    pub ke: [u8; 16],
78    /// Initiator-to-responder (commissioner → device) encryption key.
79    pub i2r_key: [u8; 16],
80    /// Responder-to-initiator (device → commissioner) decryption key.
81    pub r2i_key: [u8; 16],
82    /// Attestation challenge key (used for device attestation in commissioning).
83    pub attestation_key: [u8; 16],
84}
85
86impl core::fmt::Debug for PaseSessionKeys {
87    /// Redacts all key material; never prints key bytes.
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        f.debug_struct("PaseSessionKeys").finish_non_exhaustive()
90    }
91}
92
93#[cfg(test)]
94mod secret_hygiene_tests {
95    use super::*;
96
97    /// Compile-time proof that `PaseSessionKeys: ZeroizeOnDrop`.
98    fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
99
100    #[test]
101    fn pase_session_keys_is_zeroize_on_drop() {
102        assert_zeroize_on_drop::<PaseSessionKeys>();
103    }
104
105    #[test]
106    fn pase_session_keys_debug_redacts_key_bytes() {
107        let keys = PaseSessionKeys {
108            ke: [0xAA; 16],
109            i2r_key: [0xBB; 16],
110            r2i_key: [0xCC; 16],
111            attestation_key: [0xDD; 16],
112        };
113        let s = format!("{keys:?}");
114        assert!(!s.contains("aa"), "ke bytes leaked: {s}");
115        assert!(!s.contains("bb"), "i2r_key bytes leaked: {s}");
116        assert!(!s.contains("cc"), "r2i_key bytes leaked: {s}");
117        assert!(!s.contains("dd"), "attestation_key bytes leaked: {s}");
118        assert!(s.contains("PaseSessionKeys"));
119    }
120}