matter-commissioning 0.5.1

Matter commissioning state machine: setup payload, attestation, NOC issuance, network commissioning.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# matter-commissioning

Matter commissioning: setup payloads, the commissioning state machine, device
attestation, NOC issuance, and network commissioning.

Part of [`matter-rust`](https://github.com/phunapps/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` / `Paa` wrappers, chain
>   validation against a `PaaTrustStore`, `AttestationResponse` signature
>   verification, and CSA Certification Declaration (CMS) verification.
> - **NOC issuance**`FabricRecord`, CSR verification, RCAC/NOC minting,
>   and the `OperationalCredentials` command codecs.
> - **The commissioning state machine** — a sans-IO cursor over the whole
>   flow, `SecurePairing` through `Action::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 `driver` feature: the
>   Tokio IO layer that runs that state machine for real (PASE, mDNS, CASE,
>   Invoke/Read round-trips). See [below]#optional-driver-feature.
>
> Stability: this is a `0.x` crate, 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`]https://crates.io/crates/matter-controller, which is
> built on this crate. Reach for `matter-commissioning` directly 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

```rust
use matter_commissioning::setup::parse_qr;

let payload = parse_qr("MT:Y.K90AFN00KA0648G00")?;
assert_eq!(payload.vendor_id, Some(0xFFF1));
assert_eq!(payload.passcode.as_u32(), 20_202_021);
# Ok::<(), matter_commissioning::SetupError>(())
```

(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

```rust
use matter_commissioning::setup::parse_manual_code;

let payload = parse_manual_code("11693312331")?;
assert_eq!(payload.discriminator.short(), 0x5);
# Ok::<(), matter_commissioning::SetupError>(())
```

## Example: parse a DAC and reach for a trusted root

```rust,no_run
use matter_commissioning::{Dac, PaaTrustStore, VendorId};

# fn run(dac_der: &[u8]) -> Result<(), matter_commissioning::AttestationError> {
let dac = Dac::from_der(dac_der)?;
assert_eq!(dac.subject_vid(), VendorId::new(0xFFF1));

let trust_store = PaaTrustStore::with_example_device_roots();
assert!(trust_store.len() > 0);
# Ok(())
# }
```

Parsing a DAC does not validate it. Chain validation against the trust store is
the next example.

## Example: validate an attestation chain

```rust,no_run
use matter_cert::time::MatterTime;
use matter_commissioning::{verify_chain, Dac, Pai, PaaTrustStore};

# fn run(dac_der: &[u8], pai_der: &[u8])
#   -> Result<(), matter_commissioning::AttestationError> {
let dac = Dac::from_der(dac_der)?;
let pai = Pai::from_der(pai_der)?;
let store = PaaTrustStore::with_example_device_roots();
let now = MatterTime::from_unix_secs(1_704_067_200);

let chain = verify_chain(&dac, &pai, &store, now)?;
println!("DAC verified for VID={} PID={}", chain.vendor_id, chain.product_id);
# Ok(())
# }
```

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

```rust,no_run
use matter_commissioning::{
    verify_attestation_response, AttestationResponse,
};

# fn run(
#     attestation_elements: Vec<u8>,
#     signature: [u8; 64],
#     dac_public_key: &[u8],
#     attestation_challenge: &[u8; 16],
# ) -> Result<(), matter_commissioning::AttestationError> {
let response = AttestationResponse {
    attestation_elements,
    signature,
};
verify_attestation_response(&response, attestation_challenge, dac_public_key)?;
# Ok(())
# }
```

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](#example-full-commissioning-driver-loop-reaching-actiondone) further
down handles every variant.

```rust,no_run
use std::sync::Arc;

use matter_cert::time::MatterTime;
use matter_commissioning::attestation::CdSigningRoots;
use matter_commissioning::noc::{FabricRecord, NocRng, SystemNocRng};
use matter_commissioning::{
    Action, Commissioner, CommissionerConfig, NetworkCredentials, PaaTrustStore, SetupPayload,
};
use matter_crypto::{RingSigner, Signer};

# fn run(
#     pase_attestation_challenge: [u8; 16],
#     setup: SetupPayload,
# ) -> Result<(), Box<dyn std::error::Error>> {
let (signer, _pkcs8) = RingSigner::generate()?;
let signer: Arc<dyn Signer> = Arc::new(signer);
let rng_for_fabric = SystemNocRng;
let fabric = FabricRecord::new_root_only(
    /* fabric_id */ 0x0000_0000_0000_0001,
    signer,
    MatterTime::from_unix_secs(1_704_067_200),
    MatterTime::from_unix_secs(1_735_689_600),
    /* rcac_id */ 0xDEAD_BEEF_CAFE_F00D,
    &rng_for_fabric,
)?;

let paa = PaaTrustStore::with_example_device_roots();
let cd_signing_roots = CdSigningRoots::with_example_device_roots();
let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
let cfg = CommissionerConfig {
    pase_attestation_challenge,
    fabric: &fabric,
    setup_payload: &setup,
    paa_trust_store: &paa,
    cd_signing_roots: &cd_signing_roots,
    commissioner_node_id: 0x1,
    assigned_node_id: 0x2,
    ipk_epoch_key: [0x42_u8; 16],
    case_admin_subject: 0x1,
    admin_vendor_id: 0xFFF1,
    now: MatterTime::from_unix_secs(1_704_067_200),
    rng,
    // This device is already on its operational network; see the Wi-Fi /
    // Thread section below for the provisioning variants.
    network: NetworkCredentials::AlreadyOnNetwork,
};
let mut sm = Commissioner::new(cfg)?;
loop {
    match sm.poll()? {
        Action::ReadAttribute { expect, .. } | Action::Invoke { expect, .. } => {
            // The caller (or the `driver` feature) frames the request
            // into an Invoke/Read envelope, routes via matter-transport
            // over the PASE session, and feeds the response back:
            let response_bytes: &[u8] = unimplemented!("driver supplies the bytes");
            sm.on_response(expect, response_bytes)?;
        }
        Action::Abort { send_disarm_failsafe, reason } => {
            eprintln!("commissioning aborted at {:?}: {reason}", sm.stage());
            if send_disarm_failsafe {
                // ... send DisarmFailsafe (ArmFailSafe with expiry=0) over PASE ...
            }
            break;
        }
        Action::Done(_) => break,
        other => unreachable!("this cut-down example doesn't handle {other:?}"),
    }
}
# Ok(())
# }
```

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:

```rust,no_run
use matter_commissioning::{Commissioner, Expectation};

# fn run(
#     sm: &mut Commissioner,
#     pai_response_tlv: &[u8],
#     dac_response_tlv: &[u8],
#     attestation_response_tlv: &[u8],
# ) -> Result<(), Box<dyn std::error::Error>> {
// After ConfigRegulatory, cursor reaches SendPaiCertRequest.

// Stage 4: PAI cert request.
let _ = sm.poll()?;
sm.on_response(Expectation::PaiCertChainResponse, pai_response_tlv)?;

// Stage 5: DAC cert request.
let _ = sm.poll()?;
sm.on_response(Expectation::DacCertChainResponse, dac_response_tlv)?;

// Stage 6: AttestationRequest with fresh 32-byte random nonce.
let _ = sm.poll()?;
sm.on_response(Expectation::AttestationResponse, attestation_response_tlv)?;

// Stage 7: AttestationVerification (off-wire). Runs the whole
// verifier chain — chain validation, attestation signature, nonce
// echo, then CD verification — and advances past attestation on
// success. On failure, `poll()` returns a typed `CommissioningError`
// and the cursor transitions to `Failed`.
let _ = sm.poll()?;
# Ok(())
# }
```

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:

```rust,no_run
use matter_commissioning::{
    verify_certification_declaration, AttestationError, CdSigningRoots,
    ProductId, VendorId,
};

# fn run(cd_bytes: &[u8]) -> Result<(), AttestationError> {
let trust = CdSigningRoots::with_example_device_roots();
verify_certification_declaration(
    cd_bytes,
    VendorId::new(0xFFF1),
    ProductId::new(0x8001),
    &trust,
)?;
# Ok(())
# }
```

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:
1. Parse the CMS/PKCS#7 SignedData via the `cms` crate.
2. Validate the CMS envelope shape (single signer, attached content,
   `ecdsa-with-SHA256`).
3. Verify the ECDSA-P256/SHA-256 signature against each trusted root;
   accept on first match.
4. Decode the inner Matter-TLV CD body to extract `vendor_id` +
   `product_id_array`.
5. Cross-check the declared VID/PID against the `expected_vid` /
   `expected_pid` arguments.

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:

```rust,no_run
use matter_commissioning::{
    Action, CommissionedFabric, Commissioner, CommissioningError,
};

# fn run(mut sm: Commissioner) -> Result<CommissionedFabric, CommissioningError> {
loop {
    match sm.poll()? {
        Action::Invoke { expect, .. } | Action::ReadAttribute { expect, .. } => {
            // Caller frames the request into Invoke/Read envelope and
            // routes via matter-transport. The session is PASE for all
            // pre-NOC stages and CASE after EstablishCase succeeds.
            let response_bytes: &[u8] = unimplemented!("driver supplies the bytes");
            sm.on_response(expect, response_bytes)?;
        }
        Action::EstablishCase { fabric_id, peer_node_id } => {
            // Driver work: mDNS find-operational for the operational
            // service name keyed off (compressed_fabric_id, peer_node_id),
            // then run the SIGMA-I handshake from matter-crypto.
            // Pretend success here:
            let _ = (fabric_id, peer_node_id);
            sm.on_case_established()?;

            // On failure instead:
            //   sm.on_response(Expectation::CaseFailed, &[])?;
        }
        Action::EvictCase { .. } => {
            // Reserved for multi-fabric eviction; never emitted by
            // the current new-fabric flow.
        }
        Action::Done(commissioned_fabric) => {
            return Ok(commissioned_fabric);
        }
        Action::Abort { send_disarm_failsafe, reason } => {
            eprintln!("commissioning aborted: {reason}");
            if send_disarm_failsafe {
                // ... send ArmFailSafe(expiry=0) over PASE ...
            }
            return Err(CommissioningError::CaseEstablishmentFailed); // pick a representative error
        }
        // `Action` is `#[non_exhaustive]`: a future minor release can add a
        // variant this loop has never seen. Return an error rather than
        // panicking — the driver stays in control and can still disarm the
        // failsafe on the device before giving up.
        _ => return Err(CommissioningError::InvalidConfig("unhandled action")),
    }
}
# }
```

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

```rust,no_run
use std::sync::Arc;

use matter_cert::time::MatterTime;
use matter_commissioning::attestation::CdSigningRoots;
use matter_commissioning::noc::{FabricRecord, NocRng};
use matter_commissioning::{
    Commissioner, CommissionerConfig, NetworkCredentials, PaaTrustStore, SetupPayload,
    WiFiCredentials,
};

# fn run(
#     pase_attestation_challenge: [u8; 16],
#     fabric: FabricRecord,
#     setup: SetupPayload,
#     paa: PaaTrustStore,
#     cd_roots: CdSigningRoots,
#     rng: Arc<dyn NocRng>,
# ) -> Result<(), Box<dyn std::error::Error>> {
// `CommissionerConfig` borrows the fabric, payload and trust stores — they
// must outlive the `Commissioner`.
let config = CommissionerConfig {
    pase_attestation_challenge,
    fabric: &fabric,
    setup_payload: &setup,
    paa_trust_store: &paa,
    cd_signing_roots: &cd_roots,
    commissioner_node_id: 0x1,
    assigned_node_id: 0x2,
    ipk_epoch_key: [0x42_u8; 16],
    case_admin_subject: 0x1,
    admin_vendor_id: 0xFFF1,
    now: MatterTime::from_unix_secs(1_704_067_200),
    rng,
    network: NetworkCredentials::WiFi(WiFiCredentials {
        ssid: b"matter".to_vec(),
        credentials: b"hunter22".to_vec(),
    }),
};
let mut sm = Commissioner::new(config)?;
# let _ = &mut sm;
# Ok(())
# }
```

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`](src/thread_dataset.rs) 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:

```toml
matter-commissioning = { version = "0.5", features = ["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:

```bash
cargo run -p matter-commissioning --example commission_ip --features driver -- --help
```

## Optional `tracing` feature

Enable per-method spans for observability:

```toml
matter-commissioning = { version = "...", features = ["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.)