Skip to main content

matter_crypto/pase/
mod.rs

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