Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
29d5cb7
Move ChipLog{Format,Value}KeyId into CHIPCert.h
ksperling-apple Sep 8, 2026
5a2380c
PDC commissioning in DeviceCommissioner / AutoCommissioner
ksperling-apple Aug 24, 2026
9d88650
Implement NetworkIdentityManagementRegistrar
ksperling-apple Sep 8, 2026
b39c21e
Implement PDC commissioning support in chip-tool
ksperling-apple Sep 8, 2026
e5b0141
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2026
9902e67
Device Controller: Avoid unused variable when logging is compiled out
ksperling-apple Sep 9, 2026
ab13550
CommissioningDelegate: Note to check for PDC support
ksperling-apple Sep 9, 2026
9daee06
NetworkIdentityRegistrar.h: duplicate header, doc wording
ksperling-apple Sep 9, 2026
866444c
NetworkIdentityManagementRegistrar: Fix nits (mostly in the test)
ksperling-apple Sep 9, 2026
3ab456b
Keep track of whether a RegisterClient failure is determinate, i.e. we
ksperling-apple Sep 10, 2026
9a8a78f
Fold ControllerInvokeOperationBase into the registrar's Operation
ksperling-apple Sep 10, 2026
b72718c
Add missing inttypes.h include for PRIX32
ksperling-apple Sep 10, 2026
54ad91d
Tidy up PairingCommand::Shutdown()
ksperling-apple Sep 10, 2026
6b24590
Increase pw_unit_test fixtures memory pool to 64k on host builds
ksperling-apple Sep 10, 2026
6f54aa6
Integrate DeferExitForPDCRegistrar with Proxy Disconnect code paths
ksperling-apple Sep 10, 2026
c14658d
Fix PossessionSignature size checks and static_assert nonce length co…
ksperling-apple Sep 10, 2026
88b65d8
Comment clarifications from review
ksperling-apple Sep 10, 2026
84b28b3
Avoid calling GetWiFiCredentials() twice in the same block
ksperling-apple Sep 11, 2026
c95608b
Add missing build dep for the constant static_assert
ksperling-apple Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion config/pw_unit_test/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ import("//build_overrides/chip.gni")
import("//build_overrides/pigweed.gni")

import("${chip_root}/build/chip/tests.gni")
import("${chip_root}/src/lib/core/core.gni")

import("$dir_pw_build/target_types.gni")
pw_source_set("define_overrides") {
public_configs = [ ":define_options" ]
}

config("define_options") {
if (chip_fake_platform && chip_link_tests) {
# pw_unit_test's light backend constructs each test fixture in a single static
# memory pool, so the pool has to fit the largest fixture in the build. Use a
# larger pool for host builds that run controller-side tests with large fixtures.
if (chip_fake_platform || chip_target_style != "embedded") {
defines = [ "PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE=65536" ]
} else {
defines = [ "PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE=16384" ]
Expand Down
109 changes: 97 additions & 12 deletions examples/chip-tool/commands/pairing/PairingCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ namespace {
// Endpoint used for the CommissioningProxy cluster when --proxy-endpoint is not given.
constexpr chip::EndpointId kDefaultProxyEndpointId = 1;

// Endpoint used for the Network Identity Management cluster when --pdc-netim-endpoint-id is not given.
constexpr chip::EndpointId kDefaultNETIMEndpointId = 1;

bool IsNoPasswordMarker(chip::ByteSpan password)
{
// Use a one-character marker value (which is invalid in every supported Wi-Fi password encoding)
// to signal that no password is to be used. An empty string, which would otherwise be the more
// obvious choice, is used by the Network Commissioning Cluster to represent an open network.
return password.data_equal(ByteSpan::fromCharSpan("-"_span));
}

// Upper bound on back-to-back null-Message polls that yield a message but no reply.
// A conformant proxy drains in a handful; the bound only stops a misbehaving one from
// spinning the commissioner.
Expand Down Expand Up @@ -108,6 +119,17 @@ CHIP_ERROR PairingCommand::RunCommand()

mDeviceIsICD = false;

if (mPDCRegistrarNodeId.HasValue())
{
mPDCRegistrar.emplace(CurrentCommissioner(), mPDCRegistrarNodeId.Value(),
mPDCRegistrarEndpointId.ValueOr(kDefaultNETIMEndpointId));
}
else if (IsNoPasswordMarker(mPassword))
{
ChipLogError(chipTool, "Either a password (or '') or --pdc-netim-node-id is required");
return CHIP_ERROR_INVALID_ARGUMENT;
}

if (mCASEAuthTags.HasValue() && mCASEAuthTags.Value().size() <= kMaxSubjectCATAttributeCount)
{
CATValues cats = kUndefinedCATs;
Expand Down Expand Up @@ -197,6 +219,69 @@ CHIP_ERROR PairingCommand::RunInternal(NodeId remoteId)
return err;
}

void PairingCommand::Shutdown()
{
if (mPDCRegistrar.has_value())
{
// Release the registrar before ResetArguments() invalidates the arguments it was built from.
// Stop the pairing first, as the NetworkIdentityRegistrar contract requires: this run may have
// ended on a timeout, with commissioning still under way and the commissioner still pointing at
// the registrar. An error just means there was nothing left to stop.
RETURN_SAFELY_IGNORED CurrentCommissioner().StopPairing(mNodeId);

// Drop the idle notification before releasing the registrar: destroying it aborts whatever
// is in flight, which would otherwise release the waiter and set an exit status from here,
// re-entering StopWaiting() while the rest of the shutdown is still running.
mPDCRegistrarIdleCallback.Cancel();

// Anything still in flight is a revocation the run did not last long enough to see through;
// the destructor aborts it and the commissioner reports the identity left behind.
mPDCRegistrar.reset();
}
CHIPCommand::Shutdown();
}

void PairingCommand::FinishCommand(CHIP_ERROR aExitErr)
{
// A rollback of the Network Client Identity may still be under way; the commissioner does not
// generally wait (letting it complete in the background), but we should before quitting.
VerifyOrReturn(!DeferExitForPDCRegistrar(aExitErr));

SetCommandExitStatus(aExitErr);
}

bool PairingCommand::DeferExitForPDCRegistrar(CHIP_ERROR aExitErr)
{
VerifyOrReturnValue(mPDCRegistrar.has_value() && !mPDCRegistrar->IsIdle(), false);

ChipLogProgress(chipTool, "Waiting for the Network Client Identity revocation to complete");
mPDCRegistrarExitErr = aExitErr;

// Stop the registrar taking on anything new, so that the revocation in flight is all we wait for.
mPDCRegistrar->StopAcceptingRequests();
mPDCRegistrar->WaitForIdle(&mPDCRegistrarIdleCallback);
return true;
}

void PairingCommand::OnPDCRegistrarIdle(void * context)
{
auto * self = static_cast<PairingCommand *>(context);
self->SetCommandExitStatus(self->mPDCRegistrarExitErr);
}

WiFiCredentials PairingCommand::GetWiFiCredentials()
{
if (!mPDCRegistrar.has_value())
{
return WiFiCredentials(mSSID, mPassword);
}
if (IsNoPasswordMarker(mPassword))
{
return WiFiCredentials(mSSID, &mPDCRegistrar.value()); // PDC only
}
return WiFiCredentials(mSSID, &mPDCRegistrar.value(), mPassword); // PDC if supported
}

CommissioningParameters PairingCommand::GetCommissioningParameters()
{
auto params = CommissioningParameters();
Expand All @@ -209,13 +294,13 @@ CommissioningParameters PairingCommand::GetCommissioningParameters()
switch (mNetworkType)
{
case PairingNetworkType::WiFi:
params.SetWiFiCredentials(Controller::WiFiCredentials(mSSID, mPassword));
params.SetWiFiCredentials(GetWiFiCredentials());
break;
case PairingNetworkType::Thread:
params.SetThreadOperationalDataset(mOperationalDataset);
break;
case PairingNetworkType::WiFiOrThread:
params.SetWiFiCredentials(Controller::WiFiCredentials(mSSID, mPassword));
params.SetWiFiCredentials(GetWiFiCredentials());
params.SetThreadOperationalDataset(mOperationalDataset);
break;
case PairingNetworkType::None:
Expand Down Expand Up @@ -576,11 +661,12 @@ void PairingCommand::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err)
if (mPairingMode == PairingMode::Proxy)
{
// Clean up the proxy session before exiting, regardless of success or failure.
// The disconnect completing is what eventually reaches FinishCommand().
SendProxyDisconnect(err);
return;
}

SetCommandExitStatus(err);
FinishCommand(err);
}

void PairingCommand::OnReadCommissioningInfo(const Controller::ReadCommissioningInfo & info)
Expand Down Expand Up @@ -706,8 +792,7 @@ CHIP_ERROR PairingCommand::WiFiCredentialsNeeded(EndpointId endpoint)

auto & commissioner = CurrentCommissioner();
CommissioningParameters params = commissioner.GetCommissioningParameters();
auto credentials = Controller::WiFiCredentials(mSSID, mPassword);
params.SetWiFiCredentials(credentials);
params.SetWiFiCredentials(GetWiFiCredentials());
TEMPORARY_RETURN_IGNORED commissioner.UpdateCommissioningParameters(params);

TEMPORARY_RETURN_IGNORED commissioner.NetworkCredentialsReady();
Expand Down Expand Up @@ -1138,7 +1223,7 @@ void PairingCommand::OnError(const chip::app::CommandSender * client, CHIP_ERROR
{
// The disconnect is best-effort; log but use the original exit status.
ChipLogDetail(chipTool, "PairViaProxy: ProxyDisconnectRequest error (ignored): %" CHIP_ERROR_FORMAT, error.Format());
SetCommandExitStatus(mProxyDisconnectExitErr);
FinishCommand(mProxyDisconnectExitErr);
return;
}
ChipLogError(chipTool, "PairViaProxy CommandSender error: %" CHIP_ERROR_FORMAT, error.Format());
Expand Down Expand Up @@ -1190,12 +1275,12 @@ void PairingCommand::OnDone(chip::app::CommandSender * client)
{
mProxyDisconnectCmdSender.reset();
mProxySession.Release();
SetCommandExitStatus(mProxyDisconnectExitErr);
FinishCommand(mProxyDisconnectExitErr);
}
}

// Send ProxyDisconnectRequest to clean up the proxy session, then exit.
// SetCommandExitStatus is deferred until the response (or a timeout) is received so
// Finishing the command is deferred until the response (or a timeout) is received so
// that chip-tool keeps the TCP session alive long enough for the proxy to reply.
void PairingCommand::SendProxyDisconnect(CHIP_ERROR exitErr, bool aCancelPendingConnect)
{
Expand All @@ -1204,7 +1289,7 @@ void PairingCommand::SendProxyDisconnect(CHIP_ERROR exitErr, bool aCancelPending
const bool haveSomethingToSend = aCancelPendingConnect || mProxySessionActive;
if (!haveSomethingToSend || mProxyExchangeMgr == nullptr || !static_cast<bool>(mProxySession))
{
SetCommandExitStatus(exitErr);
FinishCommand(exitErr);
return;
}

Expand Down Expand Up @@ -1252,7 +1337,7 @@ void PairingCommand::SendProxyDisconnect(CHIP_ERROR exitErr, bool aCancelPending
{
ChipLogError(chipTool, "PairViaProxy: failed to allocate CommandSender for ProxyDisconnectRequest");
mProxySession.Release();
SetCommandExitStatus(exitErr);
FinishCommand(exitErr);
return;
}

Expand All @@ -1261,13 +1346,13 @@ void PairingCommand::SendProxyDisconnect(CHIP_ERROR exitErr, bool aCancelPending
{
ChipLogError(chipTool, "PairViaProxy: failed to send ProxyDisconnectRequest");
mProxySession.Release();
SetCommandExitStatus(exitErr);
FinishCommand(exitErr);
return;
}

ChipLogProgress(chipTool, "PairViaProxy: sent ProxyDisconnectRequest, waiting for response");
mProxyDisconnectCmdSender = std::move(cmdSender);
// SetCommandExitStatus is deferred until OnDone/OnError fires for mProxyDisconnectCmdSender.
// Finishing the command is deferred until OnDone/OnError fires for mProxyDisconnectCmdSender.
}

// ProxyTransportDelegate — called by ProxyTransport when it needs to forward
Expand Down
45 changes: 43 additions & 2 deletions examples/chip-tool/commands/pairing/PairingCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "../common/CHIPCommand.h"
#include <controller/CommissioningDelegate.h>
#include <controller/CurrentFabricRemover.h>
#include <controller/NetworkIdentityManagementRegistrar.h>

#include <commands/common/CredentialIssuerCommands.h>
#include <lib/support/Span.h>
Expand Down Expand Up @@ -80,8 +81,8 @@ class PairingCommand : public CHIPCommand,
CHIPCommand(commandName, credIssuerCmds),
mPairingMode(mode), mNetworkType(networkType), mFilterType(filterType),
mRemoteAddr{ IPAddress::Any, chip::Inet::InterfaceId::Null() }, mComplex_TimeZones(&mTimeZoneList),
mComplex_DSTOffsets(&mDSTOffsetList), mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this),
mOnProxyConnectedCallback(OnProxyDeviceConnected, this),
mComplex_DSTOffsets(&mDSTOffsetList), mPDCRegistrarIdleCallback(OnPDCRegistrarIdle, this),
mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this), mOnProxyConnectedCallback(OnProxyDeviceConnected, this),
mOnProxyConnectionFailureCallback(OnProxyDeviceConnectionFailed, this)
{
AddArgument("node-id", 0, UINT64_MAX, &mNodeId);
Expand Down Expand Up @@ -118,6 +119,19 @@ class PairingCommand : public CHIPCommand,
break;
}

if (networkType == PairingNetworkType::WiFi || networkType == PairingNetworkType::WiFiOrThread)
{
AddArgument(
"pdc-netim-node-id", 0, UINT64_MAX, &mPDCRegistrarNodeId,
"Node ID on this fabric hosting a Network Identity Management cluster that can grant the commissionee "
"access to the Wi-Fi network using Per-Device Credentials (PDC). If given, PDC is used whenever the "
"commissionee supports it, and 'password' only serves as a fallback for commissionees that do not. "
"Pass \"-\" as the password to require PDC, as distinct from an empty password, which means an open network.");
AddArgument("pdc-netim-endpoint-id", 0, UINT16_MAX, &mPDCRegistrarEndpointId,
"Endpoint on the Network Infrastructure Manager hosting the Network Identity Management cluster. "
"Defaults to 1.");
}

switch (mode)
{
case PairingMode::None:
Expand Down Expand Up @@ -278,6 +292,7 @@ class PairingCommand : public CHIPCommand,
/////////// CHIPCommand Interface /////////
CHIP_ERROR RunCommand() override;
chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(120)); }
void Shutdown() override;

/////////// DevicePairingDelegate Interface /////////
void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override;
Expand Down Expand Up @@ -310,6 +325,23 @@ class PairingCommand : public CHIPCommand,
CHIP_ERROR PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index);
CHIP_ERROR Unpair(NodeId remoteId);
chip::Controller::CommissioningParameters GetCommissioningParameters();
chip::Controller::WiFiCredentials GetWiFiCredentials();

/**
* Finishes the command with the given status, once nothing is left to wind down. Paths that
* end a pairing run go through here rather than calling SetCommandExitStatus() directly, so
* that a Network Client Identity revocation still in flight gets to complete first.
*/
void FinishCommand(CHIP_ERROR aExitErr);

/**
* If the PDC registrar still has a Network Client Identity revocation in flight -- which the
* commissioner issues without waiting for it -- shut it down gracefully and defer exiting with
* aExitErr until it is done, so the entry does not survive us on the network. Returns true if the
* exit was deferred, in which case the caller must not set an exit status itself.
*/
bool DeferExitForPDCRegistrar(CHIP_ERROR aExitErr);
static void OnPDCRegistrarIdle(void * context);
CHIP_ERROR MaybeDisplayTermsAndConditions(chip::Controller::CommissioningParameters & params);
CHIP_ERROR
GetMeshcopCommissionParams(chip::Controller::SetUpCodePairer::ThreadMeshcopCommissionParameters & meshcopCommissionParams);
Expand Down Expand Up @@ -360,6 +392,15 @@ class PairingCommand : public CHIPCommand,
chip::ByteSpan mOperationalDataset;
chip::ByteSpan mSSID;
chip::ByteSpan mPassword;
// Network Infrastructure Manager to obtain Wi-Fi Per-Device Credentials from, if any.
chip::Optional<NodeId> mPDCRegistrarNodeId;
chip::Optional<chip::EndpointId> mPDCRegistrarEndpointId;
// Built from the two arguments above by RunCommand() and released again by Shutdown(), so that
// it outlives the commissioning attempt whose CommissioningParameters point at it.
std::optional<chip::Controller::NetworkIdentityManagementRegistrar> mPDCRegistrar;
chip::Callback::Callback<chip::Controller::OnNetworkIdentityRegistrarIdleFunct> mPDCRegistrarIdleCallback;
// Exit status to deliver once the registrar reports itself idle.
CHIP_ERROR mPDCRegistrarExitErr = CHIP_NO_ERROR;
char * mOnboardingPayload = nullptr;
uint64_t mDiscoveryFilterCode = 0;
char * mDiscoveryFilterInstanceName = nullptr;
Expand Down
41 changes: 41 additions & 0 deletions examples/lighting-app/esp32/sdkconfig_pdc.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# Copyright (c) 2026 Project CHIP Authors
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Description:
# Overlay enabling Per-Device Credentials. Apply on top of another defaults
# file, e.g.
# idf.py -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig_pdc.defaults' reconfigure
#

CONFIG_ENABLE_WIFI_PDC=y

# PDC associates with the AP using EAP-TLS over WPA3-Enterprise.
CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y
CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y

# Matter requires EAP-TLS 1.3 (RFC 9190). ESP-IDF still considers this experimental, and the
# option makes every TLS user in the application prefer 1.3, not just the supplicant.
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_ESP_WIFI_EAP_TLS1_3=y

# Supplicant debug logging, while PDC association is still being brought up. The Kconfig option
# only compiles the wpa_printf() calls in; ESPWiFiDriver::Init() raises the "wpa" tag to DEBUG at
# runtime, which needs the compile-time ceiling lifted off INFO as well. Comment all three out to
# get the ~60 kB and the serial bandwidth back.
CONFIG_ESP_WIFI_DEBUG_PRINT=y
# CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT is not set
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
CONFIG_ESP_CONSOLE_UART_BAUDRATE=921600
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ source_set("network-identity-management-server") {
"AuthenticatorDriver.h",
"DefaultNetworkIdentityStorage.cpp",
"DefaultNetworkIdentityStorage.h",
"Logging.h",
"NetworkAdministratorSecret.cpp",
"NetworkAdministratorSecret.h",
"NetworkIdentityKeystore.h",
Expand Down
Loading
Loading