Skip to main content

matter_crypto/
lib.rs

1//! Matter session-establishment protocols.
2//!
3//! # Scope
4//!
5//! - [`pase`]: Password Authenticated Session Establishment via SPAKE2+
6//!   (spec §3.10). Sans-IO [`PaseProver`] / [`PaseVerifier`] state machines,
7//!   PBKDF2 setup-PIN derivation, HKDF session-key derivation, and
8//!   constant-time confirmation-tag comparison.
9//! - [`case`]: Certificate Authenticated Session Establishment via SIGMA-I
10//!   (spec §4.13). Sans-IO [`CaseInitiator`] / [`CaseResponder`] state
11//!   machines, NOC chain validation via `matter-cert`, and session
12//!   resumption (Sigma1 + `Sigma2_Resume`). Signing goes through the
13//!   [`CaseSigner`] trait, so an HSM, TPM, or secure element can hold the
14//!   operational key instead of this process.
15//! - [`operational`]: operational identity derivations (spec §4.3) — the
16//!   Compressed Fabric Identifier, the operational IPK, and the group
17//!   session/privacy keys and multicast address.
18//! - [`checkin`]: the ICD Check-In message codec (spec §4.18.2), the payload
19//!   an intermittently-connected device sends a registered client when it
20//!   briefly wakes.
21//! - [`aead`]: AES-128-CCM-128 AEAD helpers, used by CASE here and by
22//!   `matter-transport`'s secured-message framing. Prefer [`SessionAead`]
23//!   over the free functions on any path that encrypts/decrypts more than
24//!   once per key, to avoid repeating AES key expansion per call.
25//! - [`error`]: the crate error type.
26//!
27//! Both handshakes are sans-IO: they consume and produce message bytes, and
28//! the caller owns the transport. PASE and CASE are byte-checked against
29//! matter.js fixtures.
30//!
31//! # Cryptographic discipline
32//!
33//! This crate never implements primitives. AES, ECDH, ECDSA, SHA, HKDF, and
34//! HMAC come from `ring`. EC scalar/point arithmetic (which ring deliberately
35//! doesn't expose) comes from `p256`. We implement only the Matter-defined
36//! protocols on top of those primitives.
37
38#![forbid(unsafe_code)]
39
40pub mod aead;
41pub mod case;
42pub mod checkin;
43pub mod error;
44pub mod operational;
45pub mod pase;
46
47#[cfg(feature = "test-support")]
48pub mod test_support;
49
50pub use aead::SessionAead;
51pub use case::initiator::CaseInitiator;
52pub use case::responder::CaseResponder;
53pub use case::signer::{CaseSigner, RingSigner, SignerError};
54
55/// Canonical name for the ECDSA-P256-SHA256 signer trait outside CASE.
56///
57/// `CaseSigner` is the original name. Outside the CASE handshake, callers
58/// should import this re-export — the trait itself is identical.
59pub use case::signer::CaseSigner as Signer;
60pub use case::{
61    CaseCredentials, CaseMessageKind, CaseSessionKeys, CaseSessionOutput, LocalInfo, PeerInfo,
62    ResumptionId, ResumptionRecord, Sigma1Outcome,
63};
64pub use error::{Error, Result};
65pub use operational::{
66    derive_compressed_fabric_id, derive_group_privacy_key, derive_group_session_id,
67    derive_operational_ipk, group_multicast_ipv6,
68};
69pub use pase::{
70    pake_passcode_verifier, PaseMessageKind, PasePbkdfParams, PaseProver, PaseSessionKeys,
71    PaseVerifier,
72};
73
74/// Fill `buf` with cryptographically secure random bytes (ring `SystemRandom`).
75///
76/// # Errors
77/// Returns [`Error::Rng`] if the system RNG fails.
78pub fn random_bytes(buf: &mut [u8]) -> Result<()> {
79    use ring::rand::SecureRandom;
80    ring::rand::SystemRandom::new()
81        .fill(buf)
82        .map_err(|_| Error::Rng)
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
87mod tests {
88    #[test]
89    fn random_bytes_fills_and_varies() {
90        let mut a = [0u8; 32];
91        let mut b = [0u8; 32];
92        crate::random_bytes(&mut a).unwrap();
93        crate::random_bytes(&mut b).unwrap();
94        assert_ne!(a, [0u8; 32]);
95        assert_ne!(a, b); // collision probability ~2^-256
96    }
97}
98
99/// Compile-checks the Rust examples in this crate's `README.md`.
100///
101/// `#[cfg(doctest)]` means the item exists only while rustdoc is collecting
102/// doctests, so the README is compiled by `cargo test --doc` without being
103/// duplicated into the rendered crate docs.
104#[cfg(doctest)]
105#[doc = include_str!("../README.md")]
106struct ReadmeDoctests;