matter-commissioning
Matter commissioning: setup payloads, the commissioning state machine, device attestation, NOC issuance, and network commissioning.
Part of matter-rust.
Status: 0.5.1, published on crates.io. The commissioning flow here has been driven against real Matter hardware — over IP and over BLE, onto Wi-Fi and onto Thread — not only against tests.
What the crate gives you:
- Setup payloads — QR and manual pairing codes, decode and encode.
- Device attestation — typed
Dac/Pai/Paawrappers, chain validation against aPaaTrustStore,AttestationResponsesignature verification, and CSA Certification Declaration (CMS) verification.- NOC issuance —
FabricRecord, CSR verification, RCAC/NOC minting, and theOperationalCredentialscommand codecs.- The commissioning state machine — a sans-IO cursor over the whole flow,
SecurePairingthroughAction::Done(CommissionedFabric), including the network-commissioning subgraph (Wi-Fi, Thread, or a device already on its operational network) and the PASE→CASE handoff.- An async driver, behind the off-by-default
driverfeature: the Tokio IO layer that runs that state machine for real (PASE, mDNS, CASE, Invoke/Read round-trips). See below.Stability: this is a
0.xcrate, so a minor bump may break API. Encodings are byte-checked against matter.js where fixtures exist.If you want a complete controller — commissioning plus reading, writing, invoking and subscribing — use
matter-controller, which is built on this crate. Reach formatter-commissioningdirectly when you want the commissioning pieces on their own, or want to drive the state machine from your own IO layer.
Example: parse a QR code
use parse_qr;
let payload = parse_qr?;
assert_eq!;
assert_eq!;
# Ok::
(That QR string is the spec's example payload, kept as a fixture at
test-vectors/commissioning/setup/qr-spec-example.json. Substitute the code
printed on your own device.)
Example: parse a manual pairing code
use parse_manual_code;
let payload = parse_manual_code?;
assert_eq!;
# Ok::
Example: parse a DAC and reach for a trusted root
use ;
#
Parsing a DAC does not validate it. Chain validation against the trust store is the next example.
Example: validate an attestation chain
use MatterTime;
use ;
# #
Production callers build their own PaaTrustStore from CSA-published
production roots — PaaTrustStore::empty() plus add() per root, or
matter-controller's AttestationTrust::from_dirs, which loads PAA and CD
roots from two directories. The bundled with_example_device_roots() carries
the CSA test roots: fine for examples and integration tests, and it will
reject an arbitrary certified product.
Example: verify an attestation response
use ;
#
The dac_public_key is exactly what Dac::public_key() returns
(raw SEC1 uncompressed P-256, 65 bytes). The attestation_challenge
is the 16-byte session value at [32..48] of the PASE/CASE session
key blob (exposed as CaseSessionKeys::attestation_challenge or
PaseSessionKeys::attestation_key). Any verification failure folds
into the single coarse AttestationError::BadResponseSignature.
Example: configure the state machine and drive it
Commissioner is sans-IO: it emits an Action describing what to send, and you
feed the device's reply back with on_response. The loop below is cut down to
the two action shapes the early stages produce — the
full loop further
down handles every variant.
use Arc;
use MatterTime;
use CdSigningRoots;
use ;
use ;
use ;
#
Those first stages are SecurePairing → ReadCommissioningInfo →
ArmFailsafe → ConfigRegulatory. The cursor then continues through
attestation, CSR and NOC issuance, network commissioning, and the CASE
handoff, as the following sections show.
Example: attestation flow through CD verification
The same driver loop works unchanged — after ConfigRegulatory the state
machine emits three more Action::Invoke calls (PAI cert, DAC cert,
AttestationRequest) and one off-wire AttestationVerification step, which
includes the CSA-signed Certification Declaration check:
use ;
#
From there the cursor walks into the CSR and NOC issuance stages
(SendOpCertSigningRequest → ValidateCsr → GenerateNocChain →
SendTrustedRootCert → SendNoc).
Example: verify a Certification Declaration standalone
verify_certification_declaration can be called directly without
involving the state machine — useful for offline analysis of captured
CD blobs:
use ;
#
Production callers replace with_example_device_roots() with
CdSigningRoots::from_pem(&[my_root_pem]) loading the CSA-published
signing root(s) supplied by deployment.
The verifier performs five checks in order:
- Parse the CMS/PKCS#7 SignedData via the
cmscrate. - Validate the CMS envelope shape (single signer, attached content,
ecdsa-with-SHA256). - Verify the ECDSA-P256/SHA-256 signature against each trusted root; accept on first match.
- Decode the inner Matter-TLV CD body to extract
vendor_id+product_id_array. - Cross-check the declared VID/PID against the
expected_vid/expected_pidarguments.
Any failure surfaces as a specific
AttestationError::CertificationDeclaration* variant.
Example: full commissioning driver loop reaching Action::Done
The complete cursor walks from SecurePairing through
Action::Done(CommissionedFabric). The caller frames Invoke envelopes +
routes via matter-transport, then performs mDNS find-operational + the
SIGMA handshake when the state machine signals Action::EstablishCase.
The driver feature ships exactly such a caller, so you only need to write
this loop yourself if you are supplying your own IO:
use ;
#
The returned CommissionedFabric carries the long-lived fabric record
(RCAC + IPK + fabric ID), the peer's operational node ID, the device's
NOC public key, and the terminal stage cursor (always
Stage::Cleanup).
Wi-Fi commissioning configuration
use Arc;
use MatterTime;
use CdSigningRoots;
use ;
use ;
#
For Ethernet-only devices (or devices already on their operational
network), set network: NetworkCredentials::AlreadyOnNetwork — the state
machine detects the network shape at Stage::ReadNetworkCommissioningInfo
and skips the Wi-Fi sub-cursor.
Thread commissioning is supported: set
network: NetworkCredentials::Thread(dataset) with a
ThreadDataset built from an operational dataset
(e.g. ot-ctl dataset active -x, hex-decoded). If the supplied credential
type doesn't match what the device actually offers — e.g. Thread
credentials against a device whose NetworkCommissioning::FeatureMap lacks
the Thread bit — commissioning fails fast with
CommissioningError::NetworkFeatureUnsupported { needed }, naming the
network type the device is missing.
Optional driver feature
Everything above is sans-IO: the state machine says what to send and consumes
what comes back, but never touches a socket. The driver feature adds the
Tokio IO layer that closes the loop:
= { = "0.5", = ["driver"] }
driver::commission takes a DriverConfig — the same CommissionerConfig as
above, the passcode, and the controller's persistent commissioner operational
identity (its NOC plus PKCS#8 key, which the caller owns and stores) — along
with an AsyncDatagram transport and an mDNS Discovery. It then runs the
whole thing: resolve the commissionable device, PASE (SPAKE2+),
the poll loop with each action framed as an Invoke or Read over the right
session, mDNS find-operational, the CASE handshake, and CommissioningComplete.
driver::commission_ble is the same flow over a BLE/BTP AsyncDatagram — MRP
suppressed, since BTP is already reliable and ordered (spec §4.12) — with a
separate UDP transport for the operational phase. It does not contain a
Bluetooth stack: scanning and the GATT/BTP connection happen above this crate.
matter-ble provides them and matter-controller wires the two together.
The transport seam is the AsyncDatagram trait, so the driver is not tied to
one socket implementation; InMemoryDatagram is what the in-process end-to-end
tests commission over.
There is a runnable operator binary for the IP path:
Optional tracing feature
Enable per-method spans for observability:
= { = "...", = ["tracing"] }
Span field names (stage, expectation) align best-effort with
matter.js's log-event format so operators can grep across both
implementations.
Byte parity
Every fixture in test-vectors/commissioning/setup/ is captured from
matter.js by cargo xtask capture-setup. The integration test in
tests/setup_byte_parity.rs asserts that encode_qr / encode_manual_code
produce byte-identical output and that parse_qr / parse_manual_code
recover the same SetupPayload.
For attestation-response verification, test-vectors/attestation/response/
is captured by cargo xtask capture-attestation. The integration test
in tests/attestation_response_byte_parity.rs asserts that Rust and
matter.js's NodeJsStyleCrypto.verifyEcdsa produce the same
accept/reject verdict for a happy-path tuple plus four single-byte
mutations. (Byte-parity is on verdicts, not raw bytes — ECDSA's k
is randomized per signing call, so the captured signature varies
across script runs while the test assertions remain stable.)