matter-crypto 0.3.1

Matter protocol session establishment: PASE (SPAKE2+) and CASE (SIGMA).
Documentation

matter-crypto

Matter protocol session establishment and the key derivations around it — PASE (Password Authenticated Session Establishment) via SPAKE2+, CASE (Certificate Authenticated Session Establishment) via SIGMA-I, operational identity derivations, the ICD check-in codec, and the AES-CCM AEAD the secured-message layer uses. Part of the matter-rust workspace.

Scope

PASE — spec §3.10

  • Sans-IO state machines (PaseProver, PaseVerifier) — drive bytes through method-per-message-type APIs; caller owns the transport.
  • SPAKE2+ math over P-256 with Matter's M and N constants.
  • PBKDF2 setup-PIN derivation; HKDF session-key derivation.
  • Constant-time confirmation tag comparison via subtle.
  • Byte-for-byte verified against matter.js for three handshake scenarios (negotiation, known-params, max-iterations).

CASE — spec §4.13

  • Sans-IO CaseInitiator / CaseResponder state machines.
  • SIGMA-I math: ephemeral P-256 ECDH, mutual ECDSA signatures, AES-CCM-128 encrypted blobs.
  • NOC chain validation via matter-cert::CertificateChain::validate.
  • Pluggable signing via the CaseSigner trait — wire your own HSM/TPM/secure-element by implementing one method.
  • Session resumption: Sigma1 + Sigma2_Resume fast path. The caller drives record lookup via the Sigma1Outcome enum (sans-IO purity).
  • Byte-for-byte verified against matter.js for three scenarios: new session, resumption accepted, and resumption declined.

Operational identity — spec §4.3

  • Compressed Fabric Identifier and operational IPK derivation.
  • Group session key, group privacy key, and the group multicast IPv6 address.

ICD check-in — spec §4.18.2

  • The Check-In message codec: the payload an intermittently-connected device sends a registered client when it briefly wakes.

AEAD

  • AES-128-CCM-128 helpers, used by CASE here and by matter-transport's secured-message framing. SessionAead keeps the expanded AES key across calls; prefer it over the free functions on any path that encrypts or decrypts more than once per key.

Status

0.3.1, published on crates.io. PASE and CASE feature-complete, and validated against real silicon through the higher-level crates. Stability: a 0.x crate, so a minor bump may break API.

[dependencies]
matter-crypto = "0.3"

Minimal example

use matter_crypto::{PasePbkdfParams, PaseProver, PaseVerifier};

fn main() -> matter_crypto::Result<()> {
    let pin = 20202021_u32;
    let params = PasePbkdfParams {
        iterations: 1_000,
        salt: vec![0x42u8; 16],
    };

    // Each side picks its own local session id, exchanged during the handshake.
    let mut prover = PaseProver::new_with_negotiation(pin, /* initiator_session_id */ 1)?;
    let mut verifier = PaseVerifier::new_from_pin(pin, params, /* responder_session_id */ 2)?;

    // Drive the 5-message handshake. Both peers are in-process here; in
    // production the caller pipes each `Vec<u8>` across the network.
    let m = prover.start()?;
    verifier.handle_pbkdf_request(&m)?;
    let m = verifier.next_message()?;
    prover.handle_pbkdf_response(&m)?;
    let m = prover.next_message()?;
    verifier.handle_pake1(&m)?;
    let m = verifier.next_message()?;
    prover.handle_pake2(&m)?;
    let m = prover.next_message()?;
    verifier.handle_pake3(&m)?;

    let prover_keys = prover.finish()?;
    let verifier_keys = verifier.finish()?;
    assert_eq!(prover_keys.ke, verifier_keys.ke);
    Ok(())
}

Minimal example — CASE

use matter_cert::{MatterCertificate, MatterTime, TrustedRoots};
use matter_crypto::{
    CaseCredentials, CaseInitiator, CaseResponder, RingSigner, Sigma1Outcome,
};

/// Build one side's operational identity.
///
/// `noc` is a `MatterCertificate` from matter-cert (issued by this fabric's
/// CA chain), `signer` holds the NOC private key, `ipk` is the fabric's
/// 16-byte Identity Protection Key, and `rcac_public_key` is the fabric root
/// CA's SEC1-uncompressed public key. Commissioning (matter-commissioning)
/// produces all four.
fn credentials(
    noc: MatterCertificate,
    signer: RingSigner,
    ipk: [u8; 16],
    rcac_public_key: [u8; 65],
    fabric_id: u64,
    node_id: u64,
) -> CaseCredentials {
    CaseCredentials {
        noc,
        icac: None,
        signer: Box::new(signer),
        fabric_id,
        node_id,
        ipk,
        rcac_public_key,
    }
}

/// Drive the 3-message Sigma1/2/3 handshake. Both peers are in-process here;
/// in a real deployment the caller pipes each message across the network.
fn handshake(
    initiator_creds: CaseCredentials,
    responder_creds: CaseCredentials,
    trusted_roots: TrustedRoots,
    responder_node_id: u64,
    fabric_id: u64,
    now: MatterTime,
) -> matter_crypto::Result<()> {
    let mut initiator = CaseInitiator::new(
        initiator_creds,
        trusted_roots.clone(),
        responder_node_id,
        fabric_id,
        /* initiator_session_id */ 1,
        now,
    )?;
    let mut responder = CaseResponder::new(
        responder_creds,
        trusted_roots,
        /* responder_session_id */ 2,
        now,
    )?;

    let sigma1 = initiator.start()?;
    let outcome = responder.handle_sigma1(&sigma1)?;
    assert!(matches!(outcome, Sigma1Outcome::NewSession));

    let sigma2 = responder.next_message()?;
    initiator.handle_sigma2(&sigma2)?;

    let sigma3 = initiator.next_message()?;
    responder.handle_sigma3(&sigma3)?;

    let init_out = initiator.finish()?;
    let resp_out = responder.finish()?;
    // Both sides derive the same session keys.
    assert_eq!(init_out.keys.i2r_key, resp_out.keys.i2r_key);
    Ok(())
}

Cryptographic primitives

This crate never implements crypto primitives. Underlying math:

  • ring — SHA-256, HMAC, HKDF, PBKDF2, ECDSA-verify.
  • p256 — P-256 scalar/point arithmetic for SPAKE2+ (ring deliberately doesn't expose these).
  • subtle — constant-time comparison for PASE confirmation tags.
  • aes + ccm — AES-CCM-128 for CASE encrypted blobs (ring 0.17 does not expose AES-CCM).

Cross-verification

PASE messages produced by our PaseProver and PaseVerifier are byte-identical to matter.js's output for the same inputs. CI runs this verification on every PR against three captured handshake scenarios.

CASE messages are byte-identical to matter.js's output for the same inputs, on all three captured scenarios in test-vectors/case/: new session (Sigma1/2/3), resumption accepted (Sigma1 → Sigma2_Resume), and resumption declined (Sigma1 → full Sigma2/3).

License

Apache 2.0. See LICENSE.