matter-commissioning 0.4.0

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
# matter-commissioning

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

Part of [`matter-rust`](https://github.com/phunapps/matter-rust). Milestone 6.

> Status: **0.2.0**.
>
> **Milestone 6.4 (Commissioning State Machine): complete** — the
> state machine drives end-to-end from `SecurePairing` through
> `Action::Done(CommissionedFabric)` on canned responses + a mock
> `on_case_established` callback. matter.js byte-parity gate
> infrastructure shipped (operator-touch wiring deferred —
> see `TODO-1.0.md`).
>
> Phases available:
> - **M6.1:** the setup-payload codec (QR + manual pairing code).
> - **M6.2.1:** typed attestation cert wrappers (`Dac` / `Pai` /
>   `Paa`), `PaaTrustStore` with bundled CSA test roots, `VendorId` /
>   `ProductId` newtypes. Parsing only.
> - **M6.2.2:** `verify_chain``rustls-webpki` 0.103 path validation
>   with `KeyUsage::client_auth()` plus a Matter VID/PID equality
>   overlay. Six granular `AttestationError` variants with a
>   documented `webpki::Error` mapping. 8-row negative-fixture matrix.
> - **M6.2.3 (M6.2 feature-complete):** `verify_attestation_response`
>   — pure ECDSA P-256/SHA-256 verification via `ring` over
>   `attestation_elements || attestation_challenge`. Single coarse
>   `BadResponseSignature` error variant; matter.js byte-parity for
>   happy-path + four single-byte mutations.
> - **M6.3 (feature-complete):** NOC issuance — `FabricRecord`,
>   `verify_csr_response`, `issue_noc`, OpCreds command codecs with
>   matter.js byte-parity.
> - **M6.4 (complete):** commissioning state machine — all six
>   sub-phases (M6.4.1 skeleton → M6.4.6 byte-parity gate
>   infrastructure) shipped.
>
> Next: **M6.5** (Wi-Fi network commissioning subgraph) and **M6.6**
> (Tokio driver + first real-device commission). With M6.6 lands the
> first public demo of the library commissioning a real Matter device.

## 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);
```

(Replace the QR string with the actual captured value from
`test-vectors/commissioning/setup/qr-spec-example.json`.)

## 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);
```

## Example: parse a DAC and reach for a trusted root (M6.2.1)

```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(())
# }
```

Chain validation against the trust store is M6.2.2.

## Example: validate an attestation chain (M6.2.2)

```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 (M8 deliverable). The bundled `with_example_device_roots()`
is for examples and integration tests only.

## Example: verify an attestation response (M6.2.3)

```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: drive the early commissioning stages (M6.4.1)

```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, Expectation, 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,
};
let mut sm = Commissioner::new(cfg)?;
loop {
    match sm.poll()? {
        Action::ReadAttribute { expect, .. } | Action::Invoke { expect, .. } => {
            // The caller (M6.6 driver) frames the request into an
            // Invoke/Read envelope, routes via matter-transport over
            // the PASE session, and feeds the decoded 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!("M6.4.1 doesn't emit {other:?} yet"),
    }
}
# Ok(())
# }
```

M6.4.1 only drives `SecurePairing` → `ReadCommissioningInfo` →
`ArmFailsafe` → `ConfigRegulatory`. M6.4.2 extends the flow through
the attestation request/response stages, and M6.4.3 wires the
CSA-signed Certification Declaration check into the off-wire
`AttestationVerification` step so the cursor can advance past
attestation into the (M6.4.4) CSR + NOC issuance stages.

## Example: attestation flow through CD verification (M6.4.3)

The same driver loop from the M6.4.1 example works unchanged — after
`ConfigRegulatory` the state machine emits four more `Action::Invoke`
calls (PAI cert, DAC cert, AttestationRequest) and one off-wire
`AttestationVerification` step. M6.4.3 wires the CD-verify step in,
so on a valid CD the cursor advances past attestation:

```rust,no_run
use matter_commissioning::{
    Action, Commissioner, CommissionerConfig, CommissioningError, 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 M6.2/M6.4.3
// 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(())
# }
```

M6.4.4 will land the CSR / NOC issuance stages that consume the
advanced cursor.

## Example: verify a Certification Declaration standalone (M6.4.3)

`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` (M6.4.5)

The complete cursor walks from `SecurePairing` through
`Action::Done(CommissionedFabric)`. The caller (M6.6's Tokio driver in
the next major milestone) frames Invoke envelopes + routes via
`matter-transport`, then performs mDNS find-operational + the SIGMA
handshake when the state machine signals `Action::EstablishCase`:

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

# 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 } => {
            // M6.6 driver: 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 M8 multi-fabric work; never emitted by
            // M6.4's 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
        }
    }
}
# }
```

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 (M6.5+)

```rust
use matter_commissioning::{CommissionerConfig, NetworkCredentials, WiFiCredentials};

let config = 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,
    network: NetworkCredentials::WiFi(WiFiCredentials {
        ssid: b"matter".to_vec(),
        credentials: b"hunter22".to_vec(),
    }),
};
let mut sm = Commissioner::new(config)?;
```

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 `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.)