Skip to content

controller: add commissioner-side commissioning building blocks#456

Closed
glennswest wants to merge 1 commit into
project-chip:mainfrom
glennswest:feat/controller-commission-core
Closed

controller: add commissioner-side commissioning building blocks#456
glennswest wants to merge 1 commit into
project-chip:mainfrom
glennswest:feat/controller-commission-core

Conversation

@glennswest
Copy link
Copy Markdown

Summary

Introduces a new controller module that provides the IM-invoke and orchestration primitives a controller needs to drive a Matter accessory from a freshly-established PASE session through to CommissioningComplete.

The accessory role is already well-covered in this crate. This PR adds the inverse — the role that commissions accessories — with a deliberately small, validated surface. Larger follow-ups (BLE, attestation chain, network commissioning, operational discovery + CASE, a state-machine wrapper) are explicitly scoped out so this can land as a focused review.

Public API

controller::commissioner

pub async fn arm_fail_safe(matter, expiry_seconds, breadcrumb) -> Result<(), ControllerError>;
pub async fn csr_request(matter, &csr_nonce) -> Result<CsrPayload, ControllerError>;
pub fn     decode_nocsr_elements(blob) -> Result<DecodedNocsr<'_>, ControllerError>;
pub async fn add_noc(matter, noc, icac, ipk, admin_subject, admin_vendor_id)
              -> Result<AddNocResult, ControllerError>;
pub async fn commission_pase(matter, crypto, fabric_creds,
                             admin_subject, admin_vendor_id, fail_safe_secs)
              -> Result<PaseCommissionResult, ControllerError>;
  • arm_fail_safe invokes GeneralCommissioning::ArmFailSafe over PASE and decodes ArmFailSafeResponse.errorCode (non-OK → FailSafeExpired).
  • csr_request does OperationalCredentials::CSRRequest and decodes CSRResponse (returns NOCSRElements + AttestationSignature).
  • decode_nocsr_elements walks the TLV struct from §11.18.6.5.2 and returns the borrowed PKCS#10 CSR DER + the device's nonce-echo so callers can verify freshness.
  • add_noc does OperationalCredentials::AddNOC and decodes NOCResponse (non-OK status → AddNocRejected); returns the device-assigned FabricIndex.
  • commission_pase chains all of the above plus AddTrustedRootCertificate and CommissioningComplete, calling crate::commissioner::FabricCredentials::generate_device_credentials to mint the NOC.

controller::setup_code — manual pairing-code + QR-code (MT:…) parser per Matter spec §5.1.4.

Existing-file additions (small):

  • commissioner::FabricCredentials::root_secret_key(), ::rcac_id(), ::from_persisted(...) — persistence accessors so a controller can store CA material and reload it on next boot.
  • commissioner::NocGenerator::root_secret_key() — matching accessor on the inner type.

Out of scope (planned follow-ups)

Each of these is its own design conversation and doesn't belong in a single PR:

  • BLE central + BTP framing (the bootstrap transport for not-yet-on-IP devices).
  • DCL fetch + Device Attestation chain verification (AttestationRequest / CertificateChainRequest).
  • NetworkCommissioning cluster (Thread / Wi-Fi credential delivery).
  • Operational discovery (_matter._tcp mDNS-SD client) + CASE establishment.
  • A higher-level state-machine wrapping all of the above.

Validation

examples/src/bin/pase_smoke_test.rs is included as both an example and an interop smoke test. Drives PASE + commission_pase against an external responder on [::1]:5540 with passcode 20202021.

With this PR plus #454 (fix(cert): NotBefore=0 collides with CHIP no-expiry sentinel — required for CHIP signature interop), the smoke test commissions chip-tool's all-clusters-app end-to-end:

✓ PASE handshake completed
✓ ArmFailSafe(60s)
✓ CSRRequest → 243 B NOCSRElements + 64 B AttestationSignature
✓ NOCSRElements decoded; nonce-echo verified
✓ AddTrustedRootCertificate
✓ AddNOC → fabric_index=1
✓ CommissioningComplete

Responder log:

OpCreds: successfully created fabric index 0x1 via AddNOC

Without #454 the smoke test still completes PASE / ArmFailSafe / CSRRequest, but AddTrustedRootCertificate is rejected by CHIP as Invalid signature — see #454 for the root cause.

Test plan

  • cargo build -p rs-matter --features=os,zbus — clean, zero warnings on the controller crate.
  • cargo test -p rs-matter --lib --features=os,zbus — 532 tests pass (no behavior change to existing modules).
  • cargo build --release -p rs-matter-examples --bin pase_smoke_test — clean.
  • End-to-end commissioning against chip-tool all-clusters-app (with fix(cert): NotBefore=0 collides with CHIP no-expiry sentinel #454 layered in).

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 18, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the Matter controller role, adding end-to-end commissioning orchestration, onboarding payload decoders for manual and QR codes, and persistence support for fabric credentials. The review feedback identifies several critical issues regarding TLV encoding and decoding, specifically the need to use OctetString types for nonces and certificates to avoid interoperability and validation failures. Further improvements were suggested to ensure proper response verification during the fail-safe arming process and to increase certificate buffer sizes to prevent capacity errors.

// CommandFields struct at CmdDataTag::Data
// containing field 0: CSRNonce (32-byte octstr).
w.start_struct(&TLVTag::Context(CmdDataTag::Data as u8))?;
w.str(&TLVTag::Context(0), csr_nonce)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The CSRNonce field in Matter is an OctetString, not a UTF8String. Using w.str (which typically encodes as UTF-8 in this crate) for a random 32-byte nonce will likely fail if the nonce contains non-UTF-8 sequences, or it will encode the wrong TLV type tag, causing interoperability issues with strict responders.

Suggested change
w.str(&TLVTag::Context(0), csr_nonce)?;
w.octets(&TLVTag::Context(0), csr_nonce)?;

Comment on lines +335 to +337
w.str(&TLVTag::Context(0), noc)?;
w.str(&TLVTag::Context(1), icac)?;
w.str(&TLVTag::Context(2), ipk)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fields NOCValue, ICACValue, and IPKValue are defined as OctetString in the Matter specification (§11.18.6.8). They should be encoded using w.octets rather than w.str to ensure the correct TLV type tag is used and to avoid potential UTF-8 validation errors on binary certificate data.

Suggested change
w.str(&TLVTag::Context(0), noc)?;
w.str(&TLVTag::Context(1), icac)?;
w.str(&TLVTag::Context(2), ipk)?;
w.octets(&TLVTag::Context(0), noc)?;
w.octets(&TLVTag::Context(1), icac)?;
w.octets(&TLVTag::Context(2), ipk)?;

// CommandFields struct at CmdDataTag::Data
// containing field 0: RootCACertificate (octstr).
w.start_struct(&TLVTag::Context(CmdDataTag::Data as u8))?;
w.str(&TLVTag::Context(0), rcac_tlv)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The RootCACertificate field is an OctetString. It should be encoded using w.octets to match the Matter specification and correctly handle binary certificate data.

Suggested change
w.str(&TLVTag::Context(0), rcac_tlv)?;
w.octets(&TLVTag::Context(0), rcac_tlv)?;

Comment on lines +553 to +557
.map_err(ControllerError::from)?;
let csr_nonce = root
.ctx(2)
.map_err(ControllerError::from)?
.str()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The csr and CSRNonce fields in the NOCSRElements structure are OctetString types. Calling .str() on a TLVElement that contains an OctetString will return an InvalidDataType error in rs-matter because it expects a UTF8String tag. Use .octets() instead.

    let csr_der = root
        .ctx(1)
        .map_err(ControllerError::from)?
        .octets()
        .map_err(ControllerError::from)?;
    let csr_nonce = root
        .ctx(2)
        .map_err(ControllerError::from)?
        .octets()
        .map_err(ControllerError::from)?;

Comment on lines +122 to +152
let mut got_response = false;
loop {
if !got_response {
if let Some(resp) = chunk.response().map_err(ControllerError::from)? {
for (_endpoint, r) in resp.responses::<ArmFailSafeResponse>(
CL_GENERAL_COMMISSIONING,
// InvokeResponseIB carries the *response* command ID,
// not the request — ArmFailSafe(0x00) →
// ArmFailSafeResponse(0x01).
CMD_ARM_FAIL_SAFE_RESPONSE,
) {
match r {
Ok(afs) => {
let code = afs.error_code().map_err(ControllerError::from)?;
if (code as u8) != 0 {
return Err(ControllerError::FailSafeExpired);
}
got_response = true;
break;
}
Err(e) => return Err(ControllerError::Inner(e)),
}
}
}
}
match chunk.complete().await.map_err(ControllerError::from)? {
Some(next) => chunk = next,
None => break,
}
}
Ok(())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The arm_fail_safe function does not verify that a valid ArmFailSafeResponse was actually received and processed. If the device returns a StatusResponse or an unexpected command ID, the loop will terminate and the function will return Ok(()), leading the controller to believe the fail-safe was armed when it might not have been. Since ArmFailSafe is not a DefaultSuccess command, it must return a specific response struct.

    let mut got_response = false;
    loop {
        if !got_response {
            if let Some(resp) = chunk.response().map_err(ControllerError::from)? {
                for (_endpoint, r) in resp.responses::<ArmFailSafeResponse>(
                    CL_GENERAL_COMMISSIONING,
                    CMD_ARM_FAIL_SAFE_RESPONSE,
                ) {
                    match r {
                        Ok(afs) => {
                            let code = afs.error_code().map_err(ControllerError::from)?;
                            if (code as u8) != 0 {
                                return Err(ControllerError::FailSafeExpired);
                            }
                            got_response = true;
                            break;
                        }
                        Err(e) => return Err(ControllerError::Inner(e)),
                    }
                }
            }
        }
        match chunk.complete().await.map_err(ControllerError::from)? {
            Some(next) => chunk = next,
            None => break,
        }
    }

    if got_response {
        Ok(())
    } else {
        Err(ControllerError::PaseFailed)
    }

Comment on lines +592 to +593
pub noc_der: heapless::Vec<u8, 400>,
pub icac_der: heapless::Vec<u8, 400>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the Matter specification mentions a 400-byte limit for NOCs, actual certificates (especially with CATs or vendor extensions) can exceed this. The crate uses MAX_CERT_TLV_LEN (600 bytes) internally for certificate storage. Using a smaller limit here may cause BufferTooSmall errors during commissioning for valid certificates issued by the controller's own CA.

Suggested change
pub noc_der: heapless::Vec<u8, 400>,
pub icac_der: heapless::Vec<u8, 400>,
pub noc_der: heapless::Vec<u8, { crate::cert::MAX_CERT_TLV_LEN }>,
pub icac_der: heapless::Vec<u8, { crate::cert::MAX_CERT_TLV_LEN }>,

Comment on lines +649 to +655
let mut noc_out: heapless::Vec<u8, 400> = heapless::Vec::new();
noc_out.extend_from_slice(&device_creds.noc).map_err(|_| {
ControllerError::Inner(crate::error::Error::new(
crate::error::ErrorCode::BufferTooSmall,
))
})?;
let mut icac_out: heapless::Vec<u8, 400> = heapless::Vec::new();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The output buffers for noc_der and icac_der should match the internal MAX_CERT_TLV_LEN to ensure that valid certificates generated by the FabricCredentials can be returned without truncation or capacity errors.

Suggested change
let mut noc_out: heapless::Vec<u8, 400> = heapless::Vec::new();
noc_out.extend_from_slice(&device_creds.noc).map_err(|_| {
ControllerError::Inner(crate::error::Error::new(
crate::error::ErrorCode::BufferTooSmall,
))
})?;
let mut icac_out: heapless::Vec<u8, 400> = heapless::Vec::new();
let mut noc_out: heapless::Vec<u8, { crate::cert::MAX_CERT_TLV_LEN }> = heapless::Vec::new();
noc_out.extend_from_slice(&device_creds.noc).map_err(|_| {
ControllerError::Inner(crate::error::Error::new(
crate::error::ErrorCode::BufferTooSmall,
))
})?;
let mut icac_out: heapless::Vec<u8, { crate::cert::MAX_CERT_TLV_LEN }> = heapless::Vec::new();

@glennswest glennswest force-pushed the feat/controller-commission-core branch 2 times, most recently from 4e6917b to dabfefc Compare May 18, 2026 16:17
Introduces a new `controller` module that provides the IM-invoke and
orchestration primitives a controller needs to drive a Matter accessory
from a freshly-established PASE session through to
CommissioningComplete.

The accessory role is already well-covered in this crate. This module
is the inverse role — the thing that *commissions* accessories — and
adds a deliberately small, validated surface:

Public API (controller::commissioner):
- arm_fail_safe(matter, expiry_seconds, breadcrumb)
    GeneralCommissioning::ArmFailSafe over PASE, decodes
    ArmFailSafeResponse.errorCode → maps non-OK to FailSafeExpired.
- csr_request(matter, &csr_nonce) -> CsrPayload
    OperationalCredentials::CSRRequest with full CSRResponse decode.
    Returns NOCSRElements (octstr<400>) + AttestationSignature (64 B).
- decode_nocsr_elements(blob) -> DecodedNocsr<'_>
    TLV decoder for the NOCSRElements struct (§11.18.6.5.2) — pulls
    the PKCS#10 CSR DER and the device's nonce-echo so the controller
    can verify nonce freshness before issuing a NOC.
- add_noc(matter, noc, icac, ipk, admin_subject, admin_vendor_id)
    OperationalCredentials::AddNOC with NOCResponse status +
    FabricIndex decode. Non-OK status → AddNocRejected.
- commission_pase(matter, crypto, fabric_creds, admin_subject,
                  admin_vendor_id, fail_safe_secs) -> PaseCommissionResult
    Orchestrator that chains the above plus the two unexposed steps
    (AddTrustedRootCertificate, CommissioningComplete) and calls
    FabricCredentials::generate_device_credentials to mint the NOC.
    Returns the device-assigned FabricIndex + NodeID + cert chain.

Public API (controller::setup_code):
- Manual pairing-code + QR-code (MT:...) parser per Matter spec §5.1.4,
  with version / vendor-id / product-id / discriminator / passcode /
  discovery-capabilities-bitmask decoding.

Supporting additions to existing files:
- commissioner::FabricCredentials gains `root_secret_key()`,
  `rcac_id()`, and `from_persisted(...)` so a controller can
  persist its CA material and reload it on next boot.
- commissioner::NocGenerator gains the matching `root_secret_key()`
  accessor.

What is intentionally *not* in this PR (planned follow-ups):
- BLE central + BTP framing (the bootstrap transport).
- DCL fetch + Device Attestation chain verification.
- NetworkCommissioning cluster (Thread / Wi-Fi credential delivery).
- Operational discovery (_matter._tcp mDNS-SD client) + CASE.
- A higher-level Commissioner state machine wrapping all of the above.

Each of those is its own design conversation and they don't belong in
a single 3000-line PR.

Validation
----------
End-to-end test: examples/src/bin/pase_smoke_test.rs. With this PR
plus project-chip#454 (cert: NotBefore=0 sentinel fix — required for CHIP interop)
applied, the smoke test commissions chip-tool's all-clusters-app
end-to-end:

  ✓ PASE handshake
  ✓ ArmFailSafe(60s)
  ✓ CSRRequest → 243B NOCSRElements + 64B AttestationSignature
  ✓ NOCSRElements decoded; nonce verified
  ✓ AddTrustedRootCertificate
  ✓ AddNOC → device returns fabric_index=1
  ✓ CommissioningComplete

Responder log (chip-tool all-clusters-app):
  OpCreds: successfully created fabric index 0x1 via AddNOC

`cargo test -p rs-matter --lib --features=os,zbus` passes (532
existing tests; no behavior change to existing crates).

Without project-chip#454 the smoke test will still complete PASE/ArmFailSafe/
CSRRequest but AddTrustedRootCertificate is rejected by CHIP as
'Invalid signature' — see project-chip#454 for the root cause.
@glennswest glennswest force-pushed the feat/controller-commission-core branch from dabfefc to f7fe853 Compare May 18, 2026 16:41
@github-actions
Copy link
Copy Markdown

PR #456: Size comparison from da2d79e to f7fe853

Full report (8 builds for (core), dimmable-light, onoff-light, onoff-light-bt, speaker)
platform target config section da2d79e f7fe853 change % change
(core) riscv32imac-unknown-none-elf infodefmt-optz-ltofat FLASH 436352 436350 -2 -0.0
RAM 70944 70944 0 0.0
thumbv6m-none-eabi infodefmt-optz-ltofat FLASH 354420 354424 4 0.0
RAM 66660 66660 0 0.0
thumbv7em-none-eabi infodefmt-optz-ltofat FLASH 332688 332648 -40 -0.0
RAM 66428 66428 0 0.0
x86_64-unknown-linux-gnu infologs-optz-ltofat FLASH 859667 859651 -16 -0.0
RAM 71258 71258 0 0.0
dimmable-light x86_64-unknown-linux-gnu infologs-optz-ltofat FLASH 1987104 1987024 -80 -0.0
RAM 60616 60616 0 0.0
onoff-light x86_64-unknown-linux-gnu infologs-optz-ltofat FLASH 1913920 1913864 -56 -0.0
RAM 59776 59776 0 0.0
onoff-light-bt x86_64-unknown-linux-gnu infologs-optz-ltofat FLASH 3355296 3355256 -40 -0.0
RAM 5776 5776 0 0.0
speaker x86_64-unknown-linux-gnu infologs-optz-ltofat FLASH 1950360 1950296 -64 -0.0
RAM 5472 5472 0 0.0

@ivmarkov
Copy link
Copy Markdown
Contributor

Same here - please assess the code review feedback from Gemini, then I'll review.

@ivmarkov
Copy link
Copy Markdown
Contributor

Also a review from Opus 4.7 below.

I think some of the comments from Opus are a bit nit-picking, but quite a few are good. My feeling is this PR needs а few more iterations, but given that the author fronting the LLM that created this PR is not replying to any pings from my side, and not addressing even the initial Gemini code review, I'll likely close.

rs-matter development is not a dark factory, at least not yet.

=================

PR #456: Controller-Side Commissioning Building Blocks

What it does

Adds an inverse-direction module (rs-matter/src/controller/) to drive an accessory from an already-established PASE session through to CommissioningComplete. The first slice covers the post-PASE leg only — BLE, DCL/DAC verification, NetworkCommissioning, operational discovery, and any orchestrating state machine are explicitly deferred.

Files added:

  • controller/mod.rs — module root, scope statement
  • controller/commissioner.rs — 679 lines, five IM-invoke helpers + commission_pase orchestrator + a NOCSRElements decoder
  • controller/error.rsControllerError enum (Inner, PaseFailed, FailSafeExpired, AddNocRejected)
  • controller/setup_code.rs — manual-pairing-code and MT: QR-code parsers + tests
  • examples/src/bin/pase_smoke_test.rs — runnable end-to-end smoke test against onoff_light/chip-tool

Files touched:

  • commissioner/fabric_credentials.rs — adds root_secret_key(), rcac_id(), from_persisted()
  • commissioner/noc_generator.rs — adds root_secret_key()
  • lib.rs — registers pub mod controller;

The flow (commission_pase):

  1. ArmFailSafe(secs, breadcrumb=0)
  2. CSRRequest(random 32B nonce)
  3. decode NOCSRElements + verify nonce echo
  4. FabricCredentials::generate_device_credentials(csr_der, …)
  5. AddTrustedRootCertificate(rcac)
  6. AddNOC(noc, icac, ipk, admin_subject, admin_vendor_id)
  7. CommissioningComplete()

Returns { fabric_index, device_node_id, noc_der, icac_der }.

My pros

  1. Scope discipline is good. mod.rs spells out what's in/out, the in-scope set is narrow, and there's a working smoke test proving the chain end-to-end.
  2. Each step is a standalone pub async fn. Callers can drive one stage at a time, which is the right shape for a future state-machine layer.
  3. Persistence hooks (root_secret_key, from_persisted, rcac_id) match a real controller's needs — surviving a restart without invalidating commissioned devices.
  4. Setup-code parser is decent — Verhoeff check, reserved-passcode rejection per §5.1.7, both 11-digit and MT: forms, real unit tests.
  5. ControllerError is #[non_exhaustive] with room to grow.

My cons

  1. Massive boilerplate duplication. Each of the 5 IM invokes (arm_fail_safe, csr_request, add_noc, add_trusted_root_certificate, commissioning_complete) repeats the same ~80-line Exchange::initiate → invoke_sender → loop tx → BuildRequest → push → path → data → end ×3 → loop response/complete dance, with a .map_err(ControllerError::from)? on every single fallible call. ~400 lines that should be one generic helper.

  2. Hand-rolled TLV with magic field numbers. The PR writes raw w.start_struct(&TLVTag::Context(CmdDataTag::Data as u8)), w.u16(&TLVTag::Context(0), …), w.str(&TLVTag::Context(0), noc), etc. — bypassing the type-safe request builders the rest of the crate uses on the responder side. The auto-generated ArmFailSafeRequest/AddNocRequest/CSRRequest builders already exist; this re-implements them by hand. Any spec churn breaks silently.

  3. The w.str() octstr bug is real and critical. CSRNonce, NOC, ICAC, IPK, RCAC are all octstr per spec, but w.str() emits a TLV UTF-8 string. The reviewers flagged it; the device side rejects on wire-type mismatch.

  4. Cluster/command IDs as magic constants (CL_GENERAL_COMMISSIONING: u32 = 0x0030) duplicate values that already exist in dm::clusters::decl::*. Future renumber → silent divergence.

  5. ControllerError vocabulary is muddled. PaseFailed is overloaded for "PASE failed" and "nonce mismatch" and "couldn't extract response IB" — three very different failure modes collapsed into one. FailSafeExpired is raised when ArmFailSafe returned non-OK, which is not the same as an actually-expired fail-safe.

  6. Response-decode pattern is fragile. Each helper sets got_response: bool, peeks chunk.response() on the first chunk only, then drains the rest with chunk.complete(). If the device legally puts the response IB in a later chunk, it's silently dropped and the helper returns success.

  7. 400-byte buffers for NOC/ICAC are too small post-Do not use NotBefore=0 in certs validity periods #458. commission_pase returns heapless::Vec<u8, 400> for NOC and ICAC — but MAX_CERT_TLV_LEN is 600. Real certs overflow.

  8. heapless::Vec<u8, 64> for attestation_signature assumes raw r||s — a DER-encoded ECDSA sig (~70B) overflows. Reviewers also flagged this.

  9. fab=0, peer=0, secure=true as the "find the PASE session" incantation appears five times with no helper. Implicit dependency on there being exactly one PASE session.

  10. commission_pase takes 6 positional args (crypto, fabric_creds, admin_case_subject, admin_vendor_id, fail_safe_secs, plus matter) — every call site has to remember the order. Either group into a CommissioningRequest struct or make it a builder.

  11. No DAC verification anywhere. A malicious responder can claim any VID/PID and the controller commissions it onto its fabric. The attestation_signature is returned but never verified. Acknowledged-as-deferred, but it's the single most important security check and the rest of the flow is effectively unsafe without it.

  12. from_persisted takes 7 raw arguments. Should be a struct, or — better — a TLV-serialized blob that goes through tlv::tlv_codec, which is how the rest of the crate handles persistent state.

  13. SetupPayload.discriminator semantics are confusing. When sourced from a manual code, the 4-bit short discriminator lives in the top 4 bits of a 12-bit field, padded with zeros. The short_discriminator: bool flag tells you the layout, but every consumer has to interpret it differently. Easy to get wrong; would prefer enum { Short(u8), Full(u16) }.

  14. Hack-ish leftovers: let _ = Error::new(crate::error::ErrorCode::NoExchange); // silence unused import at the bottom of add_noc exists only to suppress an unused-import lint. The hand-rolled BitReader duplicates logic in pairing::qr. use crate::dm::clusters::… appears in function bodies instead of top-of-file. The smoke test runs on a 550 KB-stack thread, which is a smell that something allocates huge future state.

  15. No state machine, no exchange reuse, no cancellation/cleanup. Each helper opens its own exchange and leaves the PASE session in whatever state the device left it. After CommissioningComplete the device tears down PASE but the controller's session state isn't explicitly closed.

@ivmarkov ivmarkov closed this May 27, 2026
ivmarkov added a commit that referenced this pull request May 27, 2026
Similar in spirit to #456.

This adds an (initial) commissioning flow to rs-matter.

My feeling is that for the controller/commissioner use-case we should
initially only provide "the building blocks" (unlike for devices where
we are almost at a plug-your-RGB-hardware-and-play).

That's because for now it seems to me that each controller and
commissioner use case might be a bit unique, and I don't know to what
extent we want to decide how the controller/commissioner should be
arranged end-to-end on behalf of the user (meaning, we don't want to).

The commissioning flow (`Commissioner` and `NocGenerator`) is covered
with:
- An extended e2e test (the pre-existing `commissioning.rs` module in
test/ which - however - only did exercise a handful of the commissioning
steps previously.
- A new integration test against the "all-clusters-app" executable from
the C++ SDK - `commissioning_tests` - where this test completes
successfully only if the app reports it had been successfully
commissioned.

I also took the opportunity to rename a few modules:
- `commissioning` became `onboard` even if the latter is less precise
because it is much shorter
- `credentials` - which is really about **attestation** credentials (and
not only credentials but really about attestation) became `attest` as
this is more concrete and shorter

====

Forgot to add: there were some heavy `heapless::Vec`s returned and
allocated on-stack throughout the commissioning code. These are now
gone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants