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}