diff --git a/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.matter b/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.matter index 7f3de425528f64..46bd51d578e6ee 100644 --- a/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.matter +++ b/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.matter @@ -1334,6 +1334,57 @@ cluster GroupKeyManagement = 63 { fabric command access(invoke: administer) KeySetReadAllIndices(): KeySetReadAllIndicesResponse = 4; } +/** Supports the ability for clients to request the commissioning of themselves or other nodes onto a fabric which the cluster server can commission onto. */ +provisional cluster CommissionerControl = 1873 { + revision 1; + + bitmap SupportedDeviceCategoryBitmap : bitmap32 { + kFabricSynchronization = 0x1; + } + + fabric_sensitive info event access(read: manage) CommissioningRequestResult = 0 { + int64u requestId = 0; + node_id clientNodeId = 1; + enum8 statusCode = 2; + fabric_idx fabricIndex = 254; + } + + readonly attribute access(read: manage) SupportedDeviceCategoryBitmap supportedDeviceCategories = 0; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute event_id eventList[] = 65530; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; + + request struct RequestCommissioningApprovalRequest { + int64u requestId = 0; + vendor_id vendorId = 1; + int16u productId = 2; + optional char_string<64> label = 3; + } + + request struct CommissionNodeRequest { + int64u requestId = 0; + int16u responseTimeoutSeconds = 1; + optional octet_string ipAddress = 2; + optional int16u port = 3; + } + + response struct ReverseOpenCommissioningWindow = 2 { + int16u commissioningTimeout = 0; + octet_string PAKEPasscodeVerifier = 1; + int16u discriminator = 2; + int32u iterations = 3; + octet_string<32> salt = 4; + } + + /** This command is sent by a client to request approval for a future CommissionNode call. */ + command access(invoke: manage) RequestCommissioningApproval(RequestCommissioningApprovalRequest): DefaultSuccess = 0; + /** This command is sent by a client to request that the server begins commissioning a previously approved request. */ + command access(invoke: manage) CommissionNode(CommissionNodeRequest): ReverseOpenCommissioningWindow = 1; +} + endpoint 0 { device type ma_rootdevice = 22, version 1; @@ -1647,6 +1698,21 @@ endpoint 0 { handle command KeySetReadAllIndices; handle command KeySetReadAllIndicesResponse; } + + server cluster CommissionerControl { + emits event CommissioningRequestResult; + ram attribute supportedDeviceCategories default = 0; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute eventList; + callback attribute attributeList; + ram attribute featureMap default = 0; + ram attribute clusterRevision default = 1; + + handle command RequestCommissioningApproval; + handle command CommissionNode; + handle command ReverseOpenCommissioningWindow; + } } endpoint 1 { device type ma_aggregator = 14, version 1; diff --git a/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.zap b/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.zap index cbf31a039b85ee..6345745cde4fce 100644 --- a/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.zap +++ b/examples/fabric-bridge-app/fabric-bridge-common/fabric-bridge-app.zap @@ -4003,6 +4003,164 @@ "reportableChange": 0 } ] + }, + { + "name": "Commissioner Control", + "code": 1873, + "mfgCode": null, + "define": "COMMISSIONER_CONTROL_CLUSTER", + "side": "server", + "enabled": 1, + "apiMaturity": "provisional", + "commands": [ + { + "name": "RequestCommissioningApproval", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "CommissionNode", + "code": 1, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "ReverseOpenCommissioningWindow", + "code": 2, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + } + ], + "attributes": [ + { + "name": "SupportedDeviceCategories", + "code": 0, + "mfgCode": null, + "side": "server", + "type": "SupportedDeviceCategoryBitmap", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "GeneratedCommandList", + "code": 65528, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AcceptedCommandList", + "code": 65529, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EventList", + "code": 65530, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AttributeList", + "code": 65531, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "FeatureMap", + "code": 65532, + "mfgCode": null, + "side": "server", + "type": "bitmap32", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ClusterRevision", + "code": 65533, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + } + ], + "events": [ + { + "name": "CommissioningRequestResult", + "code": 0, + "mfgCode": null, + "side": "server", + "included": 1 + } + ] } ] }, diff --git a/src/app/clusters/commissioner-control-server/commissioner-control-server.cpp b/src/app/clusters/commissioner-control-server/commissioner-control-server.cpp new file mode 100644 index 00000000000000..98616f9e0e281b --- /dev/null +++ b/src/app/clusters/commissioner-control-server/commissioner-control-server.cpp @@ -0,0 +1,268 @@ +/* + * + * Copyright (c) 2024 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. + */ +#include "commissioner-control-server.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace chip; +using namespace chip::app; + +using chip::Protocols::InteractionModel::Status; + +namespace { + +NodeId GetNodeId(const CommandHandler * commandObj) +{ + auto descriptor = commandObj->GetSubjectDescriptor(); + + if (descriptor.authMode != Access::AuthMode::kCase) + { + return kUndefinedNodeId; + } + return descriptor.subject; +} + +void AddReverseOpenCommissioningWindowResponse(CommandHandler * commandObj, const ConcreteCommandPath & path, + const Clusters::CommissionerControl::CommissioningWindowParams & params) +{ + Clusters::CommissionerControl::Commands::ReverseOpenCommissioningWindow::Type response; + response.commissioningTimeout = params.commissioningTimeout; + response.discriminator = params.discriminator; + response.iterations = params.iterations; + response.PAKEPasscodeVerifier = params.PAKEPasscodeVerifier; + response.salt = params.salt; + + commandObj->AddResponse(path, response); +} + +void RunDeferredCommissionNode(intptr_t commandArg) +{ + auto * info = reinterpret_cast(commandArg); + + Clusters::CommissionerControl::Delegate * delegate = + Clusters::CommissionerControl::CommissionerControlServer::Instance().GetDelegate(); + + if (delegate != nullptr) + { + CHIP_ERROR err = delegate->ReverseCommissionNode(info->params, info->ipAddress.GetIPAddress(), info->port); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "ReverseCommissionNode error: %" CHIP_ERROR_FORMAT, err.Format()); + } + } + else + { + ChipLogError(Zcl, "No delegate available for ReverseCommissionNode"); + } + + delete info; +} + +} // namespace + +namespace chip { +namespace app { +namespace Clusters { +namespace CommissionerControl { + +CommissionerControlServer CommissionerControlServer::sInstance; + +CommissionerControlServer & CommissionerControlServer::Instance() +{ + return sInstance; +} + +CHIP_ERROR CommissionerControlServer::Init(Delegate & delegate) +{ + mDelegate = &delegate; + return CHIP_NO_ERROR; +} + +Status CommissionerControlServer::GetSupportedDeviceCategoriesValue( + EndpointId endpoint, BitMask * supportedDeviceCategories) const +{ + Status status = Attributes::SupportedDeviceCategories::Get(endpoint, supportedDeviceCategories); + if (status != Status::Success) + { + ChipLogProgress(Zcl, "CommissionerControl: reading supportedDeviceCategories, err:0x%x", to_underlying(status)); + } + return status; +} + +Status +CommissionerControlServer::SetSupportedDeviceCategoriesValue(EndpointId endpoint, + const BitMask supportedDeviceCategories) +{ + Status status = Status::Success; + + if ((status = Attributes::SupportedDeviceCategories::Set(endpoint, supportedDeviceCategories)) != Status::Success) + { + ChipLogProgress(Zcl, "CommissionerControl: writing supportedDeviceCategories, err:0x%x", to_underlying(status)); + return status; + } + + return status; +} + +CHIP_ERROR +CommissionerControlServer::GenerateCommissioningRequestResultEvent(const Events::CommissioningRequestResult::Type & result) +{ + EventNumber eventNumber; + CHIP_ERROR error = LogEvent(result, kRootEndpointId, eventNumber); + if (CHIP_NO_ERROR != error) + { + ChipLogError(Zcl, "CommissionerControl: Unable to emit CommissioningRequestResult event: %" CHIP_ERROR_FORMAT, + error.Format()); + } + + return error; +} + +} // namespace CommissionerControl +} // namespace Clusters +} // namespace app +} // namespace chip + +bool emberAfCommissionerControlClusterRequestCommissioningApprovalCallback( + app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, + const Clusters::CommissionerControl::Commands::RequestCommissioningApproval::DecodableType & commandData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + Status status = Status::Success; + + ChipLogProgress(Zcl, "Received command to request commissioning approval"); + + auto sourceNodeId = GetNodeId(commandObj); + + // Check if the command is executed via a CASE session + if (sourceNodeId == kUndefinedNodeId) + { + ChipLogError(Zcl, "Commissioning approval request not executed via CASE session, failing with UNSUPPORTED_ACCESS"); + commandObj->AddStatus(commandPath, Status::UnsupportedAccess); + return true; + } + + auto fabricIndex = commandObj->GetAccessingFabricIndex(); + auto requestId = commandData.requestId; + auto vendorId = commandData.vendorId; + auto productId = commandData.productId; + + // The label assigned from commandData need to be stored in CommissionerControl::Delegate which ensure that the backing buffer + // of it has a valid lifespan during fabric sync setup process. + auto & label = commandData.label; + + // Create a CommissioningApprovalRequest struct and populate it with the command data + Clusters::CommissionerControl::CommissioningApprovalRequest request = { .requestId = requestId, + .vendorId = vendorId, + .productId = productId, + .clientNodeId = sourceNodeId, + .fabricIndex = fabricIndex, + .label = label }; + + Clusters::CommissionerControl::Delegate * delegate = + Clusters::CommissionerControl::CommissionerControlServer::Instance().GetDelegate(); + + VerifyOrExit(delegate != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + + // Handle commissioning approval request + err = delegate->HandleCommissioningApprovalRequest(request); + +exit: + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "emberAfCommissionerControlClusterRequestCommissioningApprovalCallback error: %" CHIP_ERROR_FORMAT, + err.Format()); + status = StatusIB(err).mStatus; + } + + commandObj->AddStatus(commandPath, status); + return true; +} + +bool emberAfCommissionerControlClusterCommissionNodeCallback( + app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, + const Clusters::CommissionerControl::Commands::CommissionNode::DecodableType & commandData) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + ChipLogProgress(Zcl, "Received command to commission node"); + + auto sourceNodeId = GetNodeId(commandObj); + + // Check if the command is executed via a CASE session + if (sourceNodeId == kUndefinedNodeId) + { + ChipLogError(Zcl, "Commission node request not executed via CASE session, failing with UNSUPPORTED_ACCESS"); + commandObj->AddStatus(commandPath, Status::UnsupportedAccess); + return true; + } + + auto requestId = commandData.requestId; + + auto commissionNodeInfo = std::make_unique(); + + Clusters::CommissionerControl::Delegate * delegate = + Clusters::CommissionerControl::CommissionerControlServer::Instance().GetDelegate(); + + VerifyOrExit(delegate != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + + // Set IP address and port in the CommissionNodeInfo struct + commissionNodeInfo->port = commandData.port; + err = commissionNodeInfo->ipAddress.SetIPAddress(commandData.ipAddress); + SuccessOrExit(err == CHIP_NO_ERROR); + + // Validate the commission node command. + err = delegate->ValidateCommissionNodeCommand(sourceNodeId, requestId); + SuccessOrExit(err == CHIP_NO_ERROR); + + // Populate the parameters for the commissioning window + err = delegate->GetCommissioningWindowParams(commissionNodeInfo->params); + SuccessOrExit(err == CHIP_NO_ERROR); + + // Add the response for the commissioning window. + AddReverseOpenCommissioningWindowResponse(commandObj, commandPath, commissionNodeInfo->params); + + // Schedule the deferred reverse commission node task + DeviceLayer::PlatformMgr().ScheduleWork(RunDeferredCommissionNode, reinterpret_cast(commissionNodeInfo.release())); + +exit: + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "emberAfCommissionerControlClusterCommissionNodeCallback error: %" CHIP_ERROR_FORMAT, err.Format()); + commandObj->AddStatus(commandPath, StatusIB(err).mStatus); + } + + return true; +} + +void MatterCommissionerControlPluginServerInitCallback() +{ + ChipLogProgress(Zcl, "Initializing Commissioner Control cluster."); +} diff --git a/src/app/clusters/commissioner-control-server/commissioner-control-server.h b/src/app/clusters/commissioner-control-server/commissioner-control-server.h new file mode 100644 index 00000000000000..5e2422f5ebe018 --- /dev/null +++ b/src/app/clusters/commissioner-control-server/commissioner-control-server.h @@ -0,0 +1,182 @@ +/* + * + * Copyright (c) 2024 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. + */ +#pragma once + +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace CommissionerControl { + +// Spec indicates that IP Address is either 4 or 16 bytes. +static constexpr size_t kIpAddressBufferSize = 16; + +struct CommissioningApprovalRequest +{ + uint64_t requestId; + VendorId vendorId; + uint16_t productId; + NodeId clientNodeId; + FabricIndex fabricIndex; + Optional label; +}; + +struct CommissioningWindowParams +{ + uint32_t iterations; + uint16_t commissioningTimeout; + uint16_t discriminator; + ByteSpan PAKEPasscodeVerifier; + ByteSpan salt; +}; + +class ProtectedIPAddress +{ +public: + const Optional GetIPAddress() { return ipAddress; } + + CHIP_ERROR SetIPAddress(const Optional & address) + { + if (!address.HasValue()) + { + ipAddress.ClearValue(); + return CHIP_NO_ERROR; + } + + const ByteSpan & addressSpan = address.Value(); + size_t addressLength = addressSpan.size(); + if (addressLength != 4 && addressLength != 16) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + memcpy(ipAddressBuffer, addressSpan.data(), addressLength); + ipAddress.SetValue(ByteSpan(ipAddressBuffer, addressLength)); + return CHIP_NO_ERROR; + } + +private: + Optional ipAddress; + uint8_t ipAddressBuffer[kIpAddressBufferSize]; +}; + +struct CommissionNodeInfo +{ + CommissioningWindowParams params; + ProtectedIPAddress ipAddress; + Optional port; +}; + +class Delegate +{ +public: + /** + * @brief Handle a commissioning approval request. + * + * This command is sent by a client to request approval for a future CommissionNode call. + * The server SHALL always return SUCCESS to a correctly formatted RequestCommissioningApproval + * command, and then send a CommissioningRequestResult event once the result is ready. + * + * @param request The commissioning approval request to handle. + * @return CHIP_ERROR indicating the success or failure of the operation. + */ + virtual CHIP_ERROR HandleCommissioningApprovalRequest(const CommissioningApprovalRequest & request) = 0; + + /** + * @brief Validate a commission node command. + * + * This command is sent by a client to request that the server begins commissioning a previously + * approved request. + * + * The server SHALL return FAILURE if the CommissionNode command is not sent from the same + * NodeId as the RequestCommissioningApproval or if the provided RequestId to CommissionNode + * does not match the value provided to RequestCommissioningApproval. + * + * The validation SHALL fail if the client Node ID is kUndefinedNodeId, such as getting the NodeID from + * a group or PASE session. + * + * @param clientNodeId The NodeId of the client. + * @param requestId The request ID to validate. + * @return CHIP_ERROR indicating the success or failure of the operation. + */ + virtual CHIP_ERROR ValidateCommissionNodeCommand(NodeId clientNodeId, uint64_t requestId) = 0; + + /** + * @brief Get the parameters for the commissioning window. + * + * This method is called to retrieve the parameters needed for the commissioning window. + * + * @param[out] outParams The parameters for the commissioning window. + * @return CHIP_ERROR indicating the success or failure of the operation. + */ + virtual CHIP_ERROR GetCommissioningWindowParams(CommissioningWindowParams & outParams) = 0; + + /** + * @brief Reverse the commission node process. + * + * When received within the timeout specified by CommissionNode, the client SHALL open a + * commissioning window on the node which the client called RequestCommissioningApproval to + * have commissioned. + * + * @param params The parameters for the commissioning window. + * @param ipAddress Optional IP address for the commissioning window. + * @param port Optional port for the commissioning window. + * @return CHIP_ERROR indicating the success or failure of the operation. + */ + virtual CHIP_ERROR ReverseCommissionNode(const CommissioningWindowParams & params, const Optional & ipAddress, + const Optional & port) = 0; + + virtual ~Delegate() = default; +}; + +class CommissionerControlServer +{ +public: + static CommissionerControlServer & Instance(); + + CHIP_ERROR Init(Delegate & delegate); + + Delegate * GetDelegate() { return mDelegate; } + + Protocols::InteractionModel::Status + GetSupportedDeviceCategoriesValue(EndpointId endpoint, + BitMask * supportedDeviceCategories) const; + + Protocols::InteractionModel::Status + SetSupportedDeviceCategoriesValue(EndpointId endpoint, const BitMask supportedDeviceCategories); + + /** + * @brief + * Called after the server return SUCCESS to a correctly formatted RequestCommissioningApproval command. + */ + CHIP_ERROR GenerateCommissioningRequestResultEvent(const Events::CommissioningRequestResult::Type & result); + +private: + CommissionerControlServer() = default; + ~CommissionerControlServer() = default; + + static CommissionerControlServer sInstance; + + Delegate * mDelegate = nullptr; +}; + +} // namespace CommissionerControl +} // namespace Clusters +} // namespace app +} // namespace chip