diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cba40591a9e4fc..fd82cc12e1f0fc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -450,6 +450,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_DA_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_TIMESYNC_3_1.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_DA_1_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TestCommissioningTimeSync.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_decode 1" --script "src/python_testing/TC_IDM_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py"' - name: Uploading core files diff --git a/examples/platform/linux/CommissionerMain.cpp b/examples/platform/linux/CommissionerMain.cpp index d9c1b5557ebd50..73d9a146879b42 100644 --- a/examples/platform/linux/CommissionerMain.cpp +++ b/examples/platform/linux/CommissionerMain.cpp @@ -248,6 +248,7 @@ class PairingCommand : public Controller::DevicePairingDelegate void OnCommissioningStatusUpdate(PeerId peerId, CommissioningStage stageCompleted, CHIP_ERROR error) override; void OnReadCommissioningInfo(const ReadCommissioningInfo & info) override; + void OnFabricCheck(const MatchingFabricInfo & info) override; private: #if CHIP_DEVICE_CONFIG_APP_PLATFORM_ENABLED @@ -344,7 +345,10 @@ void PairingCommand::OnReadCommissioningInfo(const ReadCommissioningInfo & info) { ChipLogProgress(AppServer, "OnReadCommissioningInfo - vendorId=0x%04X productId=0x%04X", info.basic.vendorId, info.basic.productId); +} +void PairingCommand::OnFabricCheck(const MatchingFabricInfo & info) +{ if (info.nodeId != kUndefinedNodeId) { ChipLogProgress(AppServer, "ALREADY ON FABRIC WITH nodeId=0x" ChipLogFormatX64, ChipLogValueX64(info.nodeId)); diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp index ff61e851a8547c..72470edec40edc 100644 --- a/src/controller/AutoCommissioner.cpp +++ b/src/controller/AutoCommissioner.cpp @@ -242,10 +242,53 @@ CommissioningStage AutoCommissioner::GetNextCommissioningStageInternal(Commissio // Per the spec, we restart from after adding the NOC. return GetNextCommissioningStage(CommissioningStage::kSendNOC, lastErr); } + if (mParams.GetCheckForMatchingFabric()) + { + return CommissioningStage::kCheckForMatchingFabric; + } + return CommissioningStage::kArmFailsafe; + case CommissioningStage::kCheckForMatchingFabric: return CommissioningStage::kArmFailsafe; case CommissioningStage::kArmFailsafe: return CommissioningStage::kConfigRegulatory; case CommissioningStage::kConfigRegulatory: + if (mDeviceCommissioningInfo.requiresUTC) + { + return CommissioningStage::kConfigureUTCTime; + } + else + { + // Time cluster is not supported, move right to DA + return CommissioningStage::kSendPAICertificateRequest; + } + case CommissioningStage::kConfigureUTCTime: + if (mDeviceCommissioningInfo.requiresTimeZone && mParams.GetTimeZone().HasValue()) + { + return kConfigureTimeZone; + } + else + { + return GetNextCommissioningStageInternal(CommissioningStage::kConfigureTimeZone, lastErr); + } + case CommissioningStage::kConfigureTimeZone: + if (mNeedsDST && mParams.GetDSTOffsets().HasValue()) + { + return CommissioningStage::kConfigureDSTOffset; + } + else + { + return GetNextCommissioningStageInternal(CommissioningStage::kConfigureDSTOffset, lastErr); + } + case CommissioningStage::kConfigureDSTOffset: + if (mDeviceCommissioningInfo.requiresDefaultNTP && mParams.GetDefaultNTP().HasValue()) + { + return CommissioningStage::kConfigureDefaultNTP; + } + else + { + return GetNextCommissioningStageInternal(CommissioningStage::kConfigureDefaultNTP, lastErr); + } + case CommissioningStage::kConfigureDefaultNTP: return CommissioningStage::kSendPAICertificateRequest; case CommissioningStage::kSendPAICertificateRequest: return CommissioningStage::kSendDACCertificateRequest; @@ -264,6 +307,15 @@ CommissioningStage AutoCommissioner::GetNextCommissioningStageInternal(Commissio case CommissioningStage::kSendTrustedRootCert: return CommissioningStage::kSendNOC; case CommissioningStage::kSendNOC: + if (mDeviceCommissioningInfo.requiresTrustedTimeSource && mParams.GetTrustedTimeSource().HasValue()) + { + return CommissioningStage::kConfigureTrustedTimeSource; + } + else + { + return GetNextCommissioningStageInternal(CommissioningStage::kConfigureTrustedTimeSource, lastErr); + } + case CommissioningStage::kConfigureTrustedTimeSource: // TODO(cecille): device attestation casues operational cert provisioning to happen, This should be a separate stage. // For thread and wifi, this should go to network setup then enable. For on-network we can skip right to finding the // operational network because the provisioning of certificates will trigger the device to start operational advertising. @@ -580,11 +632,20 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio .SetRemoteProductId(mDeviceCommissioningInfo.basic.productId) .SetDefaultRegulatoryLocation(mDeviceCommissioningInfo.general.currentRegulatoryLocation) .SetLocationCapability(mDeviceCommissioningInfo.general.locationCapability); - if (mDeviceCommissioningInfo.nodeId != kUndefinedNodeId) + // Don't send DST unless the device says it needs it + mNeedsDST = false; + break; + case CommissioningStage::kCheckForMatchingFabric: { + chip::NodeId nodeId = report.Get().nodeId; + if (nodeId != kUndefinedNodeId) { - mParams.SetRemoteNodeId(mDeviceCommissioningInfo.nodeId); + mParams.SetRemoteNodeId(nodeId); } break; + } + case CommissioningStage::kConfigureTimeZone: + mNeedsDST = report.Get().requiresDSTOffsets; + break; case CommissioningStage::kSendPAICertificateRequest: SetPAI(report.Get().certificate); break; @@ -650,6 +711,7 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio mCommissioneeDeviceProxy = nullptr; mOperationalDeviceProxy = OperationalDeviceProxy(); mDeviceCommissioningInfo = ReadCommissioningInfo(); + mNeedsDST = false; return CHIP_NO_ERROR; default: break; @@ -692,6 +754,26 @@ CHIP_ERROR AutoCommissioner::PerformStep(CommissioningStage nextStage) ChipLogError(Controller, "Invalid device for commissioning"); return CHIP_ERROR_INCORRECT_STATE; } + // Perform any last minute parameter adjustments before calling the commissioner object + switch (nextStage) + { + case CommissioningStage::kConfigureTimeZone: + if (mParams.GetTimeZone().Value().size() > mDeviceCommissioningInfo.maxTimeZoneSize) + { + mParams.SetTimeZone(app::DataModel::List( + mParams.GetTimeZone().Value().SubSpan(0, mDeviceCommissioningInfo.maxTimeZoneSize))); + } + break; + case CommissioningStage::kConfigureDSTOffset: + if (mParams.GetDSTOffsets().Value().size() > mDeviceCommissioningInfo.maxDSTSize) + { + mParams.SetDSTOffsets(app::DataModel::List( + mParams.GetDSTOffsets().Value().SubSpan(0, mDeviceCommissioningInfo.maxDSTSize))); + } + break; + default: + break; + } mCommissioner->PerformCommissioningStep(proxy, nextStage, mParams, this, GetEndpoint(nextStage), GetCommandTimeout(proxy, nextStage)); diff --git a/src/controller/AutoCommissioner.h b/src/controller/AutoCommissioner.h index 297946f3fe86d0..66dafeef6851cb 100644 --- a/src/controller/AutoCommissioner.h +++ b/src/controller/AutoCommissioner.h @@ -54,6 +54,7 @@ class AutoCommissioner : public CommissioningDelegate * be used for sending the relevant command. */ Optional GetCommandTimeout(DeviceProxy * device, CommissioningStage stage) const; + CommissioningParameters mParams = CommissioningParameters(); private: DeviceProxy * GetDeviceProxyForStep(CommissioningStage nextStage); @@ -94,7 +95,6 @@ class AutoCommissioner : public CommissioningDelegate DeviceCommissioner * mCommissioner = nullptr; CommissioneeDeviceProxy * mCommissioneeDeviceProxy = nullptr; OperationalCredentialsDelegate * mOperationalCredentialsDelegate = nullptr; - CommissioningParameters mParams = CommissioningParameters(); OperationalDeviceProxy mOperationalDeviceProxy; // Memory space for the commisisoning parameters that come in as ByteSpans - the caller is not guaranteed to retain this memory uint8_t mSsid[CommissioningParameters::kMaxSsidLen]; @@ -104,6 +104,7 @@ class AutoCommissioner : public CommissioningDelegate bool mNeedsNetworkSetup = false; ReadCommissioningInfo mDeviceCommissioningInfo; + bool mNeedsDST = false; // TODO: Why were the nonces statically allocated, but the certs dynamically allocated? uint8_t * mDAC = nullptr; diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 42ffc4ec1d317f..037c058d033786 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -1590,6 +1590,12 @@ void DeviceCommissioner::OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData mSetUpCodePairer.NotifyCommissionableDeviceDiscovered(nodeData); } +void OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &) +{ + DeviceCommissioner * commissioner = static_cast(context); + commissioner->CommissioningStageComplete(CHIP_NO_ERROR); +} + void OnBasicFailure(void * context, CHIP_ERROR error) { ChipLogProgress(Controller, "Received failure response %s\n", chip::ErrorStr(error)); @@ -1852,6 +1858,22 @@ void DeviceCommissioner::OnDeviceConnectionRetryFn(void * context, const ScopedN // ClusterStateCache::Callback impl void DeviceCommissioner::OnDone(app::ReadClient *) +{ + switch (mCommissioningStage) + { + case CommissioningStage::kReadCommissioningInfo: + ParseCommissioningInfo(); + break; + case CommissioningStage::kCheckForMatchingFabric: + ParseFabrics(); + break; + default: + // We're not trying to read anything here, just exit + break; + } +} + +void DeviceCommissioner::ParseCommissioningInfo() { CHIP_ERROR err; CHIP_ERROR return_err = CHIP_NO_ERROR; @@ -1907,64 +1929,6 @@ void DeviceCommissioner::OnDone(app::ReadClient *) err = mAttributeCache->Get(kRootEndpointId, info.basic.productId); return_err = err == CHIP_NO_ERROR ? return_err : err; } - - // We might not have requested a Fabrics attribute at all, so not having a - // value for it is not an error. - err = mAttributeCache->ForEachAttribute(OperationalCredentials::Id, [this, &info](const app::ConcreteAttributePath & path) { - using namespace chip::app::Clusters::OperationalCredentials::Attributes; - // this code is checking if the device is already on the commissioner's fabric. - // if a matching fabric is found, then remember the nodeId so that the commissioner - // can, if it decides to, cancel commissioning (before it fails in AddNoc) and know - // the device's nodeId on its fabric. - switch (path.mAttributeId) - { - case Fabrics::Id: { - Fabrics::TypeInfo::DecodableType fabrics; - ReturnErrorOnFailure(this->mAttributeCache->Get(path, fabrics)); - // this is a best effort attempt to find a matching fabric, so no error checking on iter - auto iter = fabrics.begin(); - while (iter.Next()) - { - auto & fabricDescriptor = iter.GetValue(); - ChipLogProgress(Controller, - "DeviceCommissioner::OnDone - fabric.vendorId=0x%04X fabric.fabricId=0x" ChipLogFormatX64 - " fabric.nodeId=0x" ChipLogFormatX64, - fabricDescriptor.vendorID, ChipLogValueX64(fabricDescriptor.fabricID), - ChipLogValueX64(fabricDescriptor.nodeID)); - if (GetFabricId() == fabricDescriptor.fabricID) - { - ChipLogProgress(Controller, "DeviceCommissioner::OnDone - found a matching fabric id"); - chip::ByteSpan rootKeySpan = fabricDescriptor.rootPublicKey; - if (rootKeySpan.size() != Crypto::kP256_PublicKey_Length) - { - ChipLogError(Controller, "DeviceCommissioner::OnDone - fabric root key size mismatch %u != %u", - static_cast(rootKeySpan.size()), - static_cast(Crypto::kP256_PublicKey_Length)); - continue; - } - P256PublicKeySpan rootPubKeySpan(rootKeySpan.data()); - Crypto::P256PublicKey deviceRootPublicKey(rootPubKeySpan); - - Crypto::P256PublicKey commissionerRootPublicKey; - if (CHIP_NO_ERROR != GetRootPublicKey(commissionerRootPublicKey)) - { - ChipLogError(Controller, "DeviceCommissioner::OnDone - error reading commissioner root public key"); - } - else if (commissionerRootPublicKey.Matches(deviceRootPublicKey)) - { - ChipLogProgress(Controller, "DeviceCommissioner::OnDone - fabric root keys match"); - info.nodeId = fabricDescriptor.nodeID; - } - } - } - - return CHIP_NO_ERROR; - } - default: - return CHIP_NO_ERROR; - } - }); - // Try to parse as much as we can here before returning, even if this is an error. return_err = err == CHIP_NO_ERROR ? return_err : err; @@ -2047,6 +2011,8 @@ void DeviceCommissioner::OnDone(app::ReadClient *) }); return_err = err == CHIP_NO_ERROR ? return_err : err; + ParseTimeSyncInfo(info); + if (return_err != CHIP_NO_ERROR) { ChipLogError(Controller, "Error parsing commissioning information"); @@ -2064,6 +2030,138 @@ void DeviceCommissioner::OnDone(app::ReadClient *) CommissioningStageComplete(return_err, report); } +void DeviceCommissioner::ParseTimeSyncInfo(ReadCommissioningInfo & info) +{ + using namespace app::Clusters; + + CHIP_ERROR err; + // If we fail to get the feature map, there's no viable time cluster, don't set anything. + TimeSynchronization::Attributes::FeatureMap::TypeInfo::DecodableType featureMap; + err = mAttributeCache->Get(kRootEndpointId, featureMap); + if (err != CHIP_NO_ERROR) + { + info.requiresUTC = false; + info.requiresTimeZone = false; + info.requiresDefaultNTP = false; + info.requiresTrustedTimeSource = false; + return; + } + info.requiresUTC = true; + info.requiresTimeZone = featureMap & chip::to_underlying(TimeSynchronization::Feature::kTimeZone); + info.requiresDefaultNTP = featureMap & chip::to_underlying(TimeSynchronization::Feature::kNTPClient); + info.requiresTrustedTimeSource = featureMap & chip::to_underlying(TimeSynchronization::Feature::kTimeSyncClient); + + if (info.requiresTimeZone) + { + err = mAttributeCache->Get(kRootEndpointId, + info.maxTimeZoneSize); + if (err != CHIP_NO_ERROR) + { + // This information should be available, let's do our best with what we have, but we can't set + // the time zone without this information + info.requiresTimeZone = false; + } + err = + mAttributeCache->Get(kRootEndpointId, info.maxDSTSize); + if (err != CHIP_NO_ERROR) + { + info.requiresTimeZone = false; + } + } + if (info.requiresDefaultNTP) + { + TimeSynchronization::Attributes::DefaultNTP::TypeInfo::DecodableType defaultNTP; + err = mAttributeCache->Get(kRootEndpointId, defaultNTP); + if (err == CHIP_NO_ERROR && (!defaultNTP.IsNull()) && (defaultNTP.Value().size() != 0)) + { + info.requiresDefaultNTP = false; + } + } + if (info.requiresTrustedTimeSource) + { + TimeSynchronization::Attributes::TrustedTimeSource::TypeInfo::DecodableType trustedTimeSource; + err = + mAttributeCache->Get(kRootEndpointId, trustedTimeSource); + + if (err == CHIP_NO_ERROR && !trustedTimeSource.IsNull()) + { + info.requiresTrustedTimeSource = false; + } + } +} + +void DeviceCommissioner::ParseFabrics() +{ + CHIP_ERROR err; + CHIP_ERROR return_err = CHIP_NO_ERROR; + MatchingFabricInfo info; + // We might not have requested a Fabrics attribute at all, so not having a + // value for it is not an error. + err = mAttributeCache->ForEachAttribute(OperationalCredentials::Id, [this, &info](const app::ConcreteAttributePath & path) { + using namespace chip::app::Clusters::OperationalCredentials::Attributes; + // this code is checking if the device is already on the commissioner's fabric. + // if a matching fabric is found, then remember the nodeId so that the commissioner + // can, if it decides to, cancel commissioning (before it fails in AddNoc) and know + // the device's nodeId on its fabric. + switch (path.mAttributeId) + { + case Fabrics::Id: { + Fabrics::TypeInfo::DecodableType fabrics; + ReturnErrorOnFailure(this->mAttributeCache->Get(path, fabrics)); + // this is a best effort attempt to find a matching fabric, so no error checking on iter + auto iter = fabrics.begin(); + while (iter.Next()) + { + auto & fabricDescriptor = iter.GetValue(); + ChipLogProgress(Controller, + "DeviceCommissioner::OnDone - fabric.vendorId=0x%04X fabric.fabricId=0x" ChipLogFormatX64 + " fabric.nodeId=0x" ChipLogFormatX64, + fabricDescriptor.vendorID, ChipLogValueX64(fabricDescriptor.fabricID), + ChipLogValueX64(fabricDescriptor.nodeID)); + if (GetFabricId() == fabricDescriptor.fabricID) + { + ChipLogProgress(Controller, "DeviceCommissioner::OnDone - found a matching fabric id"); + chip::ByteSpan rootKeySpan = fabricDescriptor.rootPublicKey; + if (rootKeySpan.size() != Crypto::kP256_PublicKey_Length) + { + ChipLogError(Controller, "DeviceCommissioner::OnDone - fabric root key size mismatch %u != %u", + static_cast(rootKeySpan.size()), + static_cast(Crypto::kP256_PublicKey_Length)); + continue; + } + P256PublicKeySpan rootPubKeySpan(rootKeySpan.data()); + Crypto::P256PublicKey deviceRootPublicKey(rootPubKeySpan); + + Crypto::P256PublicKey commissionerRootPublicKey; + if (CHIP_NO_ERROR != GetRootPublicKey(commissionerRootPublicKey)) + { + ChipLogError(Controller, "DeviceCommissioner::OnDone - error reading commissioner root public key"); + } + else if (commissionerRootPublicKey.Matches(deviceRootPublicKey)) + { + ChipLogProgress(Controller, "DeviceCommissioner::OnDone - fabric root keys match"); + info.nodeId = fabricDescriptor.nodeID; + } + } + } + + return CHIP_NO_ERROR; + } + default: + return CHIP_NO_ERROR; + } + }); + + if (mPairingDelegate != nullptr) + { + mPairingDelegate->OnFabricCheck(info); + } + + CommissioningDelegate::CommissioningReport report; + report.Set(info); + CommissioningStageComplete(return_err, report); +} + void DeviceCommissioner::OnArmFailSafe(void * context, const GeneralCommissioning::Commands::ArmFailSafeResponse::DecodableType & data) { @@ -2097,6 +2195,25 @@ void DeviceCommissioner::OnSetRegulatoryConfigResponse( commissioner->CommissioningStageComplete(err, report); } +void DeviceCommissioner::OnSetTimeZoneResponse(void * context, + const TimeSynchronization::Commands::SetTimeZoneResponse::DecodableType & data) +{ + CommissioningDelegate::CommissioningReport report; + CHIP_ERROR err = CHIP_NO_ERROR; + DeviceCommissioner * commissioner = static_cast(context); + TimeZoneResponseInfo info; + info.requiresDSTOffsets = data.DSTOffsetRequired; + report.Set(info); + commissioner->CommissioningStageComplete(err, report); +} + +void DeviceCommissioner::OnSetUTCError(void * context, CHIP_ERROR error) +{ + // For SetUTCTime, we don't actually care if the commissionee didn't want out time, that's its choice + DeviceCommissioner * commissioner = static_cast(context); + commissioner->CommissioningStageComplete(CHIP_NO_ERROR); +} + void DeviceCommissioner::OnScanNetworksFailure(void * context, CHIP_ERROR error) { ChipLogProgress(Controller, "Received ScanNetworks failure response %" CHIP_ERROR_FORMAT, error.Format()); @@ -2191,6 +2308,33 @@ void DeviceCommissioner::OnCommissioningCompleteResponse( commissioner->CommissioningStageComplete(err, report); } +void DeviceCommissioner::SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, + app::AttributePathParams * readPaths, size_t readPathsSize) +{ + app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); + app::ReadPrepareParams readParams(proxy->GetSecureSession().Value()); + readParams.mIsFabricFiltered = false; + if (timeout.HasValue()) + { + readParams.mTimeout = timeout.Value(); + } + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = readPathsSize; + + auto attributeCache = Platform::MakeUnique(*this); + auto readClient = chip::Platform::MakeUnique( + engine, proxy->GetExchangeManager(), attributeCache->GetBufferedCallback(), app::ReadClient::InteractionType::Read); + CHIP_ERROR err = readClient->SendRequest(readParams); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "Failed to send read request for networking clusters"); + CommissioningStageComplete(err); + return; + } + mAttributeCache = std::move(attributeCache); + mReadClient = std::move(readClient); +} + void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, CommissioningStage step, CommissioningParameters & params, CommissioningDelegate * delegate, EndpointId endpoint, Optional timeout) @@ -2228,9 +2372,6 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio break; case CommissioningStage::kReadCommissioningInfo: { ChipLogProgress(Controller, "Sending request for commissioning information"); - app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance(); - app::ReadPrepareParams readParams(proxy->GetSecureSession().Value()); - // NOTE: this array cannot have more than 9 entries, since the spec mandates that server only needs to support 9 app::AttributePathParams readPaths[9]; // Read all the feature maps for all the networking clusters on any endpoint to determine what is supported @@ -2254,36 +2395,81 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio // Read the requested minimum connection times from all network commissioning clusters readPaths[7] = app::AttributePathParams(app::Clusters::NetworkCommissioning::Id, app::Clusters::NetworkCommissioning::Attributes::ConnectMaxTimeSeconds::Id); + // Read everything from the time cluster so we can assess what information needs to be set. + readPaths[8] = app::AttributePathParams(endpoint, app::Clusters::TimeSynchronization::Id); - readParams.mpAttributePathParamsList = readPaths; - readParams.mAttributePathParamsListSize = 8; - + SendCommissioningReadRequest(proxy, timeout, readPaths, 9); + } + break; + case CommissioningStage::kCheckForMatchingFabric: { // Read the current fabrics if (params.GetCheckForMatchingFabric()) { - readParams.mAttributePathParamsListSize = 9; - readPaths[8] = app::AttributePathParams(OperationalCredentials::Id, OperationalCredentials::Attributes::Fabrics::Id); + // We don't actually want to do this step, so just bypass it + ChipLogProgress(Controller, "kCheckForMatchingFabric step called without parameter set, skipping"); + CommissioningStageComplete(CHIP_NO_ERROR); } - readParams.mIsFabricFiltered = false; - if (timeout.HasValue()) + // This is done in a separate step since we've already used up all the available read paths in the previous read step + app::AttributePathParams readPaths[1]; + readPaths[0] = app::AttributePathParams(OperationalCredentials::Id, OperationalCredentials::Attributes::Fabrics::Id); + SendCommissioningReadRequest(proxy, timeout, readPaths, 1); + } + break; + case CommissioningStage::kConfigureUTCTime: { + TimeSynchronization::Commands::SetUTCTime::Type request; + uint64_t kChipEpochUsSinceUnixEpoch = static_cast(kChipEpochSecondsSinceUnixEpoch) * chip::kMicrosecondsPerSecond; + System::Clock::Microseconds64 utcTime; + if (System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR && utcTime.count() > kChipEpochUsSinceUnixEpoch) { - readParams.mTimeout = timeout.Value(); + request.UTCTime = utcTime.count() - kChipEpochUsSinceUnixEpoch; + // For now, we assume a seconds granularity + request.granularity = TimeSynchronization::GranularityEnum::kSecondsGranularity; + SendCommand(proxy, request, OnBasicSuccess, OnSetUTCError, endpoint, timeout); } - auto attributeCache = Platform::MakeUnique(*this); - auto readClient = chip::Platform::MakeUnique( - engine, proxy->GetExchangeManager(), attributeCache->GetBufferedCallback(), app::ReadClient::InteractionType::Read); - CHIP_ERROR err = readClient->SendRequest(readParams); - if (err != CHIP_NO_ERROR) + else { - ChipLogError(Controller, "Failed to send read request for networking clusters"); - CommissioningStageComplete(err); + // We have no time to give, but that's OK, just complete this stage + CommissioningStageComplete(CHIP_NO_ERROR); + } + break; + } + case CommissioningStage::kConfigureTimeZone: { + if (!params.GetTimeZone().HasValue()) + { + ChipLogError(Controller, "ConfigureTimeZone stage called with no time zone data"); + CommissioningStageComplete(CHIP_ERROR_INVALID_ARGUMENT); return; } - mAttributeCache = std::move(attributeCache); - mReadClient = std::move(readClient); + TimeSynchronization::Commands::SetTimeZone::Type request; + request.timeZone = params.GetTimeZone().Value(); + SendCommand(proxy, request, OnSetTimeZoneResponse, OnBasicFailure, endpoint, timeout); + break; + } + case CommissioningStage::kConfigureDSTOffset: { + if (!params.GetDSTOffsets().HasValue()) + { + ChipLogError(Controller, "ConfigureDSTOffset stage called with no DST data"); + CommissioningStageComplete(CHIP_ERROR_INVALID_ARGUMENT); + return; + } + TimeSynchronization::Commands::SetDSTOffset::Type request; + request.DSTOffset = params.GetDSTOffsets().Value(); + SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + break; + } + case CommissioningStage::kConfigureDefaultNTP: { + if (!params.GetDefaultNTP().HasValue()) + { + ChipLogError(Controller, "ConfigureDefaultNTP stage called with no default NTP data"); + CommissioningStageComplete(CHIP_ERROR_INVALID_ARGUMENT); + return; + } + TimeSynchronization::Commands::SetDefaultNTP::Type request; + request.defaultNTP = params.GetDefaultNTP().Value(); + SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + break; } - break; case CommissioningStage::kScanNetworks: { NetworkCommissioning::Commands::ScanNetworks::Type request; if (params.GetWiFiCredentials().HasValue()) @@ -2300,12 +2486,6 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio break; } case CommissioningStage::kConfigRegulatory: { - // To set during config phase: - // UTC time - // time zone - // dst offset - // Regulatory config - // TODO(cecille): Set time as well once the time cluster is implemented // TODO(cecille): Worthwhile to keep this around as part of the class? // TODO(cecille): Where is the country config actually set? ChipLogProgress(Controller, "Setting Regulatory Config"); @@ -2490,6 +2670,18 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio SendOperationalCertificate(proxy, params.GetNoc().Value(), params.GetIcac(), params.GetIpk().Value(), params.GetAdminSubject().Value(), timeout); break; + case CommissioningStage::kConfigureTrustedTimeSource: { + if (!params.GetTrustedTimeSource().HasValue()) + { + ChipLogError(Controller, "ConfigureTrustedTimeSource stage called with no trusted time source data"); + CommissioningStageComplete(CHIP_ERROR_INVALID_ARGUMENT); + return; + } + TimeSynchronization::Commands::SetTrustedTimeSource::Type request; + request.trustedTimeSource.SetNull(); + SendCommand(proxy, request, OnBasicSuccess, OnBasicFailure, endpoint, timeout); + break; + } case CommissioningStage::kWiFiNetworkSetup: { if (!params.GetWiFiCredentials().HasValue()) { diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h index 2f686ac455160a..9fd8d1cfe7ee03 100644 --- a/src/controller/CHIPDeviceController.h +++ b/src/controller/CHIPDeviceController.h @@ -85,6 +85,7 @@ constexpr uint16_t kNumMaxActiveDevices = CHIP_CONFIG_CONTROLLER_MAX_ACTIVE_DEVI // Raw functions for cluster callbacks void OnBasicFailure(void * context, CHIP_ERROR err); +void OnBasicSuccess(void * context, const chip::app::DataModel::NullObjectType &); struct ControllerInitParams { @@ -827,6 +828,11 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, static void OnSetRegulatoryConfigResponse( void * context, const chip::app::Clusters::GeneralCommissioning::Commands::SetRegulatoryConfigResponse::DecodableType & data); + static void OnSetUTCError(void * context, CHIP_ERROR error); + static void + OnSetTimeZoneResponse(void * context, + const chip::app::Clusters::TimeSynchronization::Commands::SetTimeZoneResponse::DecodableType & data); + static void OnScanNetworksResponse(void * context, const app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & data); @@ -907,6 +913,14 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, return cluster.InvokeCommand(request, this, successCb, failureCb); } + void SendCommissioningReadRequest(DeviceProxy * proxy, Optional timeout, + app::AttributePathParams * readPaths, size_t readPathsSize); + // Parsers for the two different read clients + void ParseCommissioningInfo(); + void ParseFabrics(); + // Called by ParseCommissioningInfo + void ParseTimeSyncInfo(ReadCommissioningInfo & info); + static CHIP_ERROR ConvertFromOperationalCertStatus(chip::app::Clusters::OperationalCredentials::NodeOperationalCertStatusEnum err); diff --git a/src/controller/CommissioningDelegate.cpp b/src/controller/CommissioningDelegate.cpp index 7fcdeec16a1e32..7100ee0865b392 100644 --- a/src/controller/CommissioningDelegate.cpp +++ b/src/controller/CommissioningDelegate.cpp @@ -37,6 +37,10 @@ const char * StageToString(CommissioningStage stage) return "ReadCommissioningInfo"; break; + case kCheckForMatchingFabric: + return "CheckForMatchingFabric"; + break; + case kArmFailsafe: return "ArmFailSafe"; break; @@ -49,6 +53,22 @@ const char * StageToString(CommissioningStage stage) return "ConfigRegulatory"; break; + case kConfigureUTCTime: + return "ConfigureUTCTime"; + break; + + case kConfigureTimeZone: + return "ConfigureTimeZone"; + break; + + case kConfigureDSTOffset: + return "ConfigureDSTOffset"; + break; + + case kConfigureDefaultNTP: + return "ConfigureDefaultNTP"; + break; + case kSendPAICertificateRequest: return "SendPAICertificateRequest"; break; @@ -85,6 +105,10 @@ const char * StageToString(CommissioningStage stage) return "SendNOC"; break; + case kConfigureTrustedTimeSource: + return "ConfigureTrustedTimeSource"; + break; + case kWiFiNetworkSetup: return "WiFiNetworkSetup"; break; diff --git a/src/controller/CommissioningDelegate.h b/src/controller/CommissioningDelegate.h index e1c3f7266dbb5d..85724b1927a3b7 100644 --- a/src/controller/CommissioningDelegate.h +++ b/src/controller/CommissioningDelegate.h @@ -34,9 +34,14 @@ enum CommissioningStage : uint8_t { kError, kSecurePairing, ///< Establish a PASE session with the device - kReadCommissioningInfo, ///< Query General Commissioning Attributes and Network Features + kReadCommissioningInfo, ///< Query General Commissioning Attributes, Network Features and Time Synchronization Cluster + kCheckForMatchingFabric, ///< Read the current fabrics on the commissionee kArmFailsafe, ///< Send ArmFailSafe (0x30:0) command to the device kConfigRegulatory, ///< Send SetRegulatoryConfig (0x30:2) command to the device + kConfigureUTCTime, ///< SetUTCTime if the DUT has a time cluster + kConfigureTimeZone, ///< Configure a time zone if one is required and available + kConfigureDSTOffset, ///< Configure DST offset if one is required and available + kConfigureDefaultNTP, ///< Configure a default NTP server if one is required and available kSendPAICertificateRequest, ///< Send PAI CertificateChainRequest (0x3E:2) command to the device kSendDACCertificateRequest, ///< Send DAC CertificateChainRequest (0x3E:2) command to the device kSendAttestationRequest, ///< Send AttestationRequest (0x3E:0) command to the device @@ -46,6 +51,7 @@ enum CommissioningStage : uint8_t kGenerateNOCChain, ///< TLV encode Node Operational Credentials (NOC) chain certs kSendTrustedRootCert, ///< Send AddTrustedRootCertificate (0x3E:11) command to the device kSendNOC, ///< Send AddNOC (0x3E:6) command to the device + kConfigureTrustedTimeSource, ///< Configure a trusted time source if one is required and available (must be done after SendNOC) kWiFiNetworkSetup, ///< Send AddOrUpdateWiFiNetwork (0x31:2) command to the device kThreadNetworkSetup, ///< Send AddOrUpdateThreadNetwork (0x31:3) command to the device kFailsafeBeforeWiFiEnable, ///< Extend the fail-safe before doing kWiFiNetworkEnable @@ -134,6 +140,34 @@ class CommissioningParameters // The country code to be used for the node, if set. Optional GetCountryCode() const { return mCountryCode; } + // Time zone to set for the node + // If required, this will be truncated to fit the max size allowable on the node + Optional> GetTimeZone() const + { + return mTimeZone; + } + + // DST offset list. If required, this will be truncated to fit the max size allowable on the node + // DST list will only be sent if the commissionee requires DST offsets, as indicated in the SetTimeZone response + Optional> GetDSTOffsets() const + { + return mDSTOffsets; + } + + // Default NTP to set on the node if supported and required + // Default implementation will not overide a value already set on the commissionee + // TODO: Add a force option? + Optional> GetDefaultNTP() const { return mDefaultNTP; } + + // Trusted time source + // Default implementation will not override a value already set on the commissionee + // TODO: Add a force option? + Optional> + GetTrustedTimeSource() const + { + return mTrustedTimeSource; + } + // Nonce sent to the node to use during the CSR request. // When using the AutoCommissioner, this value will be ignored in favour of the value supplied by the // OperationalCredentialsDelegate ObtainCsrNonce function. If the credential delegate is not supplied, the value supplied here @@ -272,6 +306,37 @@ class CommissioningParameters return *this; } + // The lifetime of the list buffer needs to exceed the lifetime of the CommissioningParameters object. + CommissioningParameters & + SetTimeZone(app::DataModel::List timeZone) + { + mTimeZone.SetValue(timeZone); + return *this; + } + + // The lifetime of the list buffer needs to exceed the lifetime of the CommissioningParameters object. + CommissioningParameters & + SetDSTOffsets(app::DataModel::List dstOffsets) + { + mDSTOffsets.SetValue(dstOffsets); + return *this; + } + + // The lifetime of the char span needs to exceed the lifetime of the CommissioningParameters + CommissioningParameters & SetDefaultNTP(app::DataModel::Nullable defaultNTP) + { + mDefaultNTP.SetValue(defaultNTP); + return *this; + } + + CommissioningParameters & SetTrustedTimeSource( + app::DataModel::Nullable + trustedTimeSource) + { + mTrustedTimeSource.SetValue(trustedTimeSource); + return *this; + } + // The lifetime of the buffer csrNonce is pointing to, should exceed the lifetime of CommissioningParameters object. CommissioningParameters & SetCSRNonce(ByteSpan csrNonce) { @@ -453,6 +518,11 @@ class CommissioningParameters Optional mFailsafeTimerSeconds; Optional mCASEFailsafeTimerSeconds; Optional mDeviceRegulatoryLocation; + Optional> mTimeZone; + Optional> mDSTOffsets; + Optional> mDefaultNTP; + Optional> + mTrustedTimeSource; Optional mCSRNonce; Optional mAttestationNonce; Optional mWiFiCreds; @@ -555,9 +625,23 @@ struct ReadCommissioningInfo NetworkClusters network; BasicClusterInfo basic; GeneralCommissioningInfo general; + bool requiresUTC = false; + bool requiresTimeZone = false; + bool requiresDefaultNTP = false; + bool requiresTrustedTimeSource = false; + uint8_t maxTimeZoneSize = 1; + uint8_t maxDSTSize = 1; +}; +struct MatchingFabricInfo +{ NodeId nodeId = kUndefinedNodeId; }; +struct TimeZoneResponseInfo +{ + bool requiresDSTOffsets; +}; + struct AttestationErrorInfo { AttestationErrorInfo(Credentials::AttestationVerificationResult result) : attestationResult(result) {} @@ -584,8 +668,13 @@ class CommissioningDelegate virtual ~CommissioningDelegate(){}; /* CommissioningReport is returned after each commissioning step is completed. The reports for each step are: * kReadCommissioningInfo - ReadCommissioningInfo + * kCheckForMatchingFabric = MatchingFabricInfo * kArmFailsafe: CommissioningErrorInfo if there is an error * kConfigRegulatory: CommissioningErrorInfo if there is an error + * kConfigureUTCTime: None + * kConfigureTimeZone: TimeZoneResponseInfo + * kConfigureDSTOffset: None + * kConfigureDefaultNTP: None * kSendPAICertificateRequest: RequestedCertificate * kSendDACCertificateRequest: RequestedCertificate * kSendAttestationRequest: AttestationResponse @@ -594,6 +683,7 @@ class CommissioningDelegate * kGenerateNOCChain: NocChain * kSendTrustedRootCert: None * kSendNOC: none + * kConfigureTrustedTimeSource: None * kWiFiNetworkSetup: NetworkCommissioningStatusInfo if there is an error * kThreadNetworkSetup: NetworkCommissioningStatusInfo if there is an error * kWiFiNetworkEnable: NetworkCommissioningStatusInfo if there is an error @@ -602,9 +692,9 @@ class CommissioningDelegate * kSendComplete: CommissioningErrorInfo if there is an error * kCleanup: none */ - struct CommissioningReport - : Variant + struct CommissioningReport : Variant { CommissioningReport() : stageCompleted(CommissioningStage::kError) {} CommissioningStage stageCompleted; diff --git a/src/controller/DevicePairingDelegate.h b/src/controller/DevicePairingDelegate.h index 7f8b98dc6c2b4b..e46d7c158ffb62 100644 --- a/src/controller/DevicePairingDelegate.h +++ b/src/controller/DevicePairingDelegate.h @@ -83,6 +83,12 @@ class DLL_EXPORT DevicePairingDelegate */ virtual void OnReadCommissioningInfo(const ReadCommissioningInfo & info) {} + /** + * @brief + * Called when MatchingFabricInfo returned from target + */ + virtual void OnFabricCheck(const MatchingFabricInfo & info) {} + /** * @brief * Called with the NetworkScanResponse returned from the target. diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp index e69ad2d2850d39..f58ee12e592216 100644 --- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp +++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp @@ -97,6 +97,9 @@ namespace { chip::Platform::ScopedMemoryBuffer sSsidBuf; chip::Platform::ScopedMemoryBuffer sCredsBuf; chip::Platform::ScopedMemoryBuffer sThreadBuf; +chip::Platform::ScopedMemoryBuffer sDefaultNTPBuf; +app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type sDSTBuf; +app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type sTimeZoneBuf; chip::Controller::CommissioningParameters sCommissioningParameters; } // namespace @@ -137,6 +140,11 @@ PyChipError pychip_DeviceController_UnpairDevice(chip::Controller::DeviceCommiss DeviceUnpairingCompleteFunct callback); PyChipError pychip_DeviceController_SetThreadOperationalDataset(const char * threadOperationalDataset, uint32_t size); PyChipError pychip_DeviceController_SetWiFiCredentials(const char * ssid, const char * credentials); +PyChipError pychip_DeviceController_SetTimeZone(int32_t offset, uint64_t validAt); +PyChipError pychip_DeviceController_SetDSTOffset(int32_t offset, uint64_t validStarting, uint64_t validUntil); +PyChipError pychip_DeviceController_SetDefaultNtp(const char * defaultNTP); +PyChipError pychip_DeviceController_SetTrustedTimeSource(chip::NodeId nodeId, chip::EndpointId endpoint); +PyChipError pychip_DeviceController_ResetCommissioningParameters(); PyChipError pychip_DeviceController_CloseSession(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid); PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::DeviceCommissioner * devCtrl, const char * peerAddrStr, uint32_t setupPINCode, chip::NodeId nodeid, uint16_t port); @@ -494,6 +502,47 @@ PyChipError pychip_DeviceController_SetWiFiCredentials(const char * ssid, const return ToPyChipError(CHIP_NO_ERROR); } +PyChipError pychip_DeviceController_SetTimeZone(int32_t offset, uint64_t validAt) +{ + sTimeZoneBuf.offset = offset; + sTimeZoneBuf.validAt = validAt; + app::DataModel::List list(&sTimeZoneBuf, 1); + sCommissioningParameters.SetTimeZone(list); + return ToPyChipError(CHIP_NO_ERROR); +} +PyChipError pychip_DeviceController_SetDSTOffset(int32_t offset, uint64_t validStarting, uint64_t validUntil) +{ + sDSTBuf.offset = offset; + sDSTBuf.validStarting = validStarting; + sDSTBuf.validUntil = chip::app::DataModel::MakeNullable(validUntil); + app::DataModel::List list(&sDSTBuf, 1); + sCommissioningParameters.SetDSTOffsets(list); + return ToPyChipError(CHIP_NO_ERROR); +} +PyChipError pychip_DeviceController_SetDefaultNtp(const char * defaultNTP) +{ + size_t len = strlen(defaultNTP); + ReturnErrorCodeIf(!sDefaultNTPBuf.Alloc(len), ToPyChipError(CHIP_ERROR_NO_MEMORY)); + memcpy(sDefaultNTPBuf.Get(), defaultNTP, len); + sCommissioningParameters.SetDefaultNTP(chip::app::DataModel::MakeNullable(CharSpan(sDefaultNTPBuf.Get(), len))); + return ToPyChipError(CHIP_NO_ERROR); +} + +PyChipError pychip_DeviceController_SetTrustedTimeSource(chip::NodeId nodeId, chip::EndpointId endpoint) +{ + chip::app::Clusters::TimeSynchronization::Structs::FabricScopedTrustedTimeSourceStruct::Type timeSource = { .nodeID = nodeId, + .endpoint = + endpoint }; + sCommissioningParameters.SetTrustedTimeSource(chip::app::DataModel::MakeNullable(timeSource)); + return ToPyChipError(CHIP_NO_ERROR); +} + +PyChipError pychip_DeviceController_ResetCommissioningParameters() +{ + sCommissioningParameters = CommissioningParameters(); + return ToPyChipError(CHIP_NO_ERROR); +} + PyChipError pychip_DeviceController_CloseSession(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid) { // diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index 55bb8094863682..f78b0135e1f2c4 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -160,6 +160,14 @@ class TestCommissioner : public chip::Controller::AutoCommissioner mCompletionError = err; } } + if (report.stageCompleted == chip::Controller::CommissioningStage::kReadCommissioningInfo) + { + mReadCommissioningInfo = report.Get(); + } + if (report.stageCompleted == chip::Controller::CommissioningStage::kConfigureTimeZone) + { + mNeedsDST = report.Get().requiresDSTOffsets; + } return chip::Controller::AutoCommissioner::CommissioningStepFinished(err, report); } @@ -233,6 +241,7 @@ class TestCommissioner : public chip::Controller::AutoCommissioner return paseShouldBeOpen == paseIsOpen; } + bool CheckStageSuccessful(uint8_t stage) { return mReceivedStageSuccess[stage]; } void Reset() { mTestCommissionerUsed = false; @@ -246,6 +255,8 @@ class TestCommissioner : public chip::Controller::AutoCommissioner mSimulateFailureOnStage = chip::Controller::CommissioningStage::kError; mFailOnReportAfterStage = chip::Controller::CommissioningStage::kError; mPrematureCompleteAfter = chip::Controller::CommissioningStage::kError; + mReadCommissioningInfo = chip::Controller::ReadCommissioningInfo(); + mNeedsDST = false; } bool GetTestCommissionerUsed() { return mTestCommissionerUsed; } void OnCommissioningSuccess(chip::PeerId peerId) { mReceivedCommissioningSuccess = true; } @@ -293,27 +304,39 @@ class TestCommissioner : public chip::Controller::AutoCommissioner bool mIsWifi = false; bool mIsThread = false; CHIP_ERROR mCompletionError = CHIP_NO_ERROR; + // Contains information about whether the device needs time sync + chip::Controller::ReadCommissioningInfo mReadCommissioningInfo; + bool mNeedsDST = false; bool ValidStage(chip::Controller::CommissioningStage stage) { - if (!mIsWifi && - (stage == chip::Controller::CommissioningStage::kWiFiNetworkEnable || - stage == chip::Controller::CommissioningStage::kFailsafeBeforeWiFiEnable || - stage == chip::Controller::CommissioningStage::kWiFiNetworkSetup)) - { - return false; - } - if (!mIsThread && - (stage == chip::Controller::CommissioningStage::kThreadNetworkEnable || - stage == chip::Controller::CommissioningStage::kFailsafeBeforeThreadEnable || - stage == chip::Controller::CommissioningStage::kThreadNetworkSetup)) + switch (stage) { + case chip::Controller::CommissioningStage::kCheckForMatchingFabric: + return mParams.GetCheckForMatchingFabric(); + case chip::Controller::CommissioningStage::kWiFiNetworkEnable: + case chip::Controller::CommissioningStage::kFailsafeBeforeWiFiEnable: + case chip::Controller::CommissioningStage::kWiFiNetworkSetup: + return mIsWifi; + case chip::Controller::CommissioningStage::kThreadNetworkEnable: + case chip::Controller::CommissioningStage::kFailsafeBeforeThreadEnable: + case chip::Controller::CommissioningStage::kThreadNetworkSetup: + return mIsThread; + case chip::Controller::CommissioningStage::kConfigureUTCTime: + return mReadCommissioningInfo.requiresUTC; + case chip::Controller::CommissioningStage::kConfigureTimeZone: + return mReadCommissioningInfo.requiresTimeZone && mParams.GetTimeZone().HasValue(); + case chip::Controller::CommissioningStage::kConfigureTrustedTimeSource: + return mReadCommissioningInfo.requiresTrustedTimeSource && mParams.GetTrustedTimeSource().HasValue(); + case chip::Controller::CommissioningStage::kConfigureDefaultNTP: + return mReadCommissioningInfo.requiresDefaultNTP && mParams.GetDefaultNTP().HasValue(); + case chip::Controller::CommissioningStage::kConfigureDSTOffset: + return mNeedsDST && mParams.GetDSTOffsets().HasValue(); + case chip::Controller::CommissioningStage::kError: + case chip::Controller::CommissioningStage::kSecurePairing: return false; + default: + return true; } - if (stage == chip::Controller::CommissioningStage::kError || stage == chip::Controller::CommissioningStage::kSecurePairing) - { - return false; - } - return true; } bool StatusUpdatesOk(chip::Controller::CommissioningStage failedStage) { @@ -612,6 +635,11 @@ bool pychip_TestCommissioningCallbacks() return sTestCommissioner.CheckCallbacks(); } +bool pychip_TestCommissioningStageSuccessful(uint8_t stage) +{ + return sTestCommissioner.CheckStageSuccessful(stage); +} + bool pychip_TestPaseConnection(NodeId nodeId) { return sTestCommissioner.CheckPaseConnection(nodeId); diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 37437eac71886c..e162df18d9fb07 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -37,8 +37,8 @@ import threading import time import typing -from ctypes import (CDLL, CFUNCTYPE, POINTER, byref, c_bool, c_char, c_char_p, c_int, c_size_t, c_uint8, c_uint16, c_uint32, - c_uint64, c_void_p, create_string_buffer, pointer, py_object, resize, string_at) +from ctypes import (CDLL, CFUNCTYPE, POINTER, byref, c_bool, c_char, c_char_p, c_int, c_int32, c_size_t, c_uint8, c_uint16, + c_uint32, c_uint64, c_void_p, create_string_buffer, pointer, py_object, resize, string_at) from dataclasses import dataclass import dacite @@ -507,6 +507,11 @@ def CheckTestCommissionerCallbacks(self): lambda: self._dmLib.pychip_TestCommissioningCallbacks() ) + def CheckStageSuccessful(self, stage: int): + return self._ChipStack.Call( + lambda: self._dmLib.pychip_TestCommissioningStageSuccessful(stage) + ) + def CheckTestCommissionerPaseConnection(self, nodeid): return self._dmLib.pychip_TestPaseConnection(nodeid) @@ -1327,6 +1332,25 @@ def _InitLib(self): c_char_p, c_char_p] self._dmLib.pychip_DeviceController_SetWiFiCredentials.restype = PyChipError + # Currently only supports 1 list item, no name + self._dmLib.pychip_DeviceController_SetTimeZone.restype = PyChipError + self._dmLib.pychip_DeviceController_SetTimeZone.argtypes = [ + c_int32, c_uint64] + + # Currently only supports 1 list item + self._dmLib.pychip_DeviceController_SetDSTOffset.restype = PyChipError + self._dmLib.pychip_DeviceController_SetDSTOffset.argtypes = [ + c_int32, c_uint64, c_uint64] + + self._dmLib.pychip_DeviceController_SetDefaultNtp.restype = PyChipError + self._dmLib.pychip_DeviceController_SetDefaultNtp.argtypes = [c_char_p] + + self._dmLib.pychip_DeviceController_SetTrustedTimeSource.restype = PyChipError + self._dmLib.pychip_DeviceController_SetTrustedTimeSource.argtypes = [c_uint64, c_uint16] + + self._dmLib.pychip_DeviceController_ResetCommissioningParameters.restype = PyChipError + self._dmLib.pychip_DeviceController_ResetCommissioningParameters.argtypes = [] + self._dmLib.pychip_DeviceController_Commission.argtypes = [ c_void_p, c_uint64] self._dmLib.pychip_DeviceController_Commission.restype = PyChipError @@ -1451,6 +1475,9 @@ def _InitLib(self): self._dmLib.pychip_TestCommissioningCallbacks.argtypes = [] self._dmLib.pychip_TestCommissioningCallbacks.restype = c_bool + self._dmLib.pychip_TestCommissioningStageSuccessful.argtypes = [c_uint8] + self._dmLib.pychip_TestCommissioningStageSuccessful.restype = c_bool + self._dmLib.pychip_ResetCommissioningTests.argtypes = [] self._dmLib.pychip_TestPaseConnection.argtypes = [c_uint64] @@ -1604,6 +1631,36 @@ def SetThreadOperationalDataset(self, threadOperationalDataset): threadOperationalDataset, len(threadOperationalDataset)) ).raise_on_error() + def ResetCommissioningParameters(self): + self.CheckIsActive() + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_ResetCommissioningParameters() + ).raise_on_error() + + def SetTimeZone(self, offset: int, validAt: int): + self.CheckIsActive() + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetTimeZone(offset, validAt) + ).raise_on_error() + + def SetDSTOffset(self, offset: int, validStarting: int, validUntil: int): + self.CheckIsActive() + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetDSTOffset(offset, validStarting, validUntil) + ).raise_on_error() + + def SetDefaultNTP(self, defaultNTP: str): + self.CheckIsActive() + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetDefaultNtp(defaultNTP.encode("utf-8")) + ).raise_on_error() + + def SetTrustedTimeSource(self, nodeId: int, endpoint: int): + self.CheckIsActive() + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetTrustedTimeSource(nodeId, endpoint) + ).raise_on_error() + def CommissionOnNetwork(self, nodeId: int, setupPinCode: int, filterType: DiscoveryFilterType = DiscoveryFilterType.NONE, filter: typing.Any = None) -> PyChipError: ''' diff --git a/src/python_testing/TC_CGEN_2_4.py b/src/python_testing/TC_CGEN_2_4.py index 1f0e86b5cd172d..0810f196f4c5fb 100644 --- a/src/python_testing/TC_CGEN_2_4.py +++ b/src/python_testing/TC_CGEN_2_4.py @@ -27,6 +27,16 @@ from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main from mobly import asserts +# Commissioning stage numbers - we should find a better way to match these to the C++ code +kArmFailsafe = 4 +kConfigRegulatory = 5 +kSendPAICertificateRequest = 10 +kSendDACCertificateRequest = 11 +kSendAttestationRequest = 12 +kSendOpCertSigningRequest = 14 +kSendTrustedRootCert = 17 +kSendNOC = 18 + class TC_CGEN_2_4(MatterBaseTest): @@ -68,22 +78,22 @@ async def test_TC_CGEN_2_4(self): th2_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() th2_fabric_admin = th2_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=self.th1.fabricId + 1) self.th2 = th2_fabric_admin.NewController(nodeId=2, useTestCommissioner=True) - # Stage 3 = kArmFailsafe, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(3, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 4 = kConfigRegulatory, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(4, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 5 = kSendPAICertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(5, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 6 = kSendDACCertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(6, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 7 = kSendAttestationRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(7, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 9 = kSendOpCertSigningRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(9, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 12 = kSendTrustedRootCert, expect General error 0x7e (UNSUPPORTED_ACCESS) - await self.CommissionToStageSendCompleteAndCleanup(12, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) - # Stage 13 = kSendNOC, expect cluster error InvalidAuthentication - await self.CommissionToStageSendCompleteAndCleanup(13, chip.native.ErrorSDKPart.IM_CLUSTER_STATUS, 0x02) + # kArmFailsafe, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kArmFailsafe, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kConfigRegulatory, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kConfigRegulatory, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendPAICertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kSendPAICertificateRequest, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendDACCertificateRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kSendDACCertificateRequest, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendAttestationRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kSendAttestationRequest, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendOpCertSigningRequest, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kSendOpCertSigningRequest, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendTrustedRootCert, expect General error 0x7e (UNSUPPORTED_ACCESS) + await self.CommissionToStageSendCompleteAndCleanup(kSendTrustedRootCert, chip.native.ErrorSDKPart.IM_GLOBAL_STATUS, 0x7e) + # kSendNOC, expect cluster error InvalidAuthentication + await self.CommissionToStageSendCompleteAndCleanup(kSendNOC, chip.native.ErrorSDKPart.IM_CLUSTER_STATUS, 0x02) logging.info('Step 15 - TH1 opens a commissioning window') params = self.OpenCommissioningWindow() diff --git a/src/python_testing/TestCommissioningTimeSync.py b/src/python_testing/TestCommissioningTimeSync.py new file mode 100644 index 00000000000000..5ff06e4271fbe6 --- /dev/null +++ b/src/python_testing/TestCommissioningTimeSync.py @@ -0,0 +1,192 @@ +# +# Copyright (c) 2023 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. +# +import logging + +import chip.clusters as Clusters +from chip import ChipDeviceCtrl +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError, Status +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main, utc_time_in_matter_epoch +from mobly import asserts + +# We don't have a good pipe between the c++ enums in CommissioningDelegate and python +# so this is hardcoded. +# I realize this is dodgy, not sure how to cross the enum from c++ to python cleanly +kConfigureUTCTime = 6 +kConfigureTimeZone = 7 +kConfigureDSTOffset = 8 +kConfigureDefaultNTP = 9 +kConfigureTrustedTimeSource = 19 + + +class TestCommissioningTimeSync(MatterBaseTest): + def setup_class(self): + self.commissioner = None + self.commissioned = False + return super().setup_class() + + async def destroy_current_commissioner(self): + if self.commissioner: + if self.commissioned: + fabricidx = await self.read_single_attribute_check_success(dev_ctrl=self.commissioner, cluster=Clusters.OperationalCredentials, attribute=Clusters.OperationalCredentials.Attributes.CurrentFabricIndex) + cmd = Clusters.OperationalCredentials.Commands.RemoveFabric(fabricIndex=fabricidx) + await self.send_single_cmd(cmd=cmd) + self.commissioner.Shutdown() + self.commissioner = None + self.commissioned = False + + @async_test_body + async def teardown_test(self): + await self.destroy_current_commissioner() + return super().teardown_test() + + async def commission_and_base_checks(self): + params = self.default_controller.OpenCommissioningWindow( + nodeid=self.dut_node_id, timeout=600, iteration=10000, discriminator=1234, option=1) + errcode = self.commissioner.CommissionOnNetwork( + nodeId=self.dut_node_id, setupPinCode=params.setupPinCode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=1234) + asserts.assert_true(errcode.is_success, 'Commissioning did not complete successfully') + self.commissioned = True + + # Check the feature map - if we have a time cluster, we want UTC time to be set + features = await self.read_single_attribute(dev_ctrl=self.default_controller, node_id=self.dut_node_id, + endpoint=0, attribute=Clusters.TimeSynchronization.Attributes.FeatureMap) + self.supports_time = True + if isinstance(features, Clusters.Attribute.ValueDecodeFailure): + asserts.assert_true(isinstance(features.Reason, InteractionModelError), InteractionModelError, + f'Unexpected exception from reading time cluster feature map {features.Reason}') + asserts.assert_equal(features.Reason.status, Status.UnsupportedCluster, + f'Unexpected error response from reading time cluster feature map {features.Reason}') + self.supports_time = False + + asserts.assert_equal(self.commissioner.CheckStageSuccessful( + kConfigureUTCTime), self.supports_time, 'UTC time stage incorrect') + + if self.supports_time: + self.supports_time_zone = bool(features & Clusters.TimeSynchronization.Bitmaps.Feature.kTimeZone) + self.supports_default_ntp = bool(features & Clusters.TimeSynchronization.Bitmaps.Feature.kNTPClient) + self.supports_trusted_time_source = bool(features & Clusters.TimeSynchronization.Bitmaps.Feature.kTimeSyncClient) + else: + self.supports_time_zone = False + self.supports_default_ntp = False + self.supports_trusted_time_source = False + + async def create_commissioner(self): + if self.commissioner: + self.destroy_current_commissioner() + new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() + new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) + self.commissioner = new_fabric_admin.NewController(nodeId=112233, useTestCommissioner=True) + + self.commissioner.ResetCommissioningParameters() + self.commissioner.ResetTestCommissioner() + + # If the app we're testing against doesn't have a time cluster, we still want to run this + # tests and we want it to succeed, so catch the unsupported cluster error here and ignore + try: + cmd = Clusters.TimeSynchronization.Commands.SetDefaultNTP(defaultNTP=NullValue) + await self.send_single_cmd(cmd=cmd) + cmd = Clusters.TimeSynchronization.Commands.SetTrustedTimeSource(NullValue) + await self.send_single_cmd(cmd=cmd) + except InteractionModelError as e: + if e.status == Status.UnsupportedCluster: + pass + + async def commission_stages(self, time_zone: bool, dst: bool, default_ntp: bool, trusted_time_source: bool): + await self.create_commissioner() + + logging.info( + f'Running Commissioning test - time_zone: {time_zone}, dst: {dst}, default_ntp: {default_ntp}, trusted_time_source: {trusted_time_source}') + + if time_zone: + self.commissioner.SetTimeZone(offset=3600, validAt=0) + if dst: + six_months = 1.577e+13 # in us + self.commissioner.SetDSTOffset(offset=3600, validStarting=0, validUntil=utc_time_in_matter_epoch() + int(six_months)) + if default_ntp: + self.commissioner.SetDefaultNTP("fe80::1") + if trusted_time_source: + self.commissioner.SetTrustedTimeSource(self.commissioner.nodeId, 0) + + await self.commission_and_base_checks() + + should_set_time_zone = bool(self.supports_time_zone and time_zone) + should_set_dst = bool(self.supports_time_zone and time_zone and dst) + should_set_default_ntp = bool(self.supports_default_ntp and default_ntp) + should_set_trusted_time = bool(self.supports_trusted_time_source and trusted_time_source) + + print(f'{should_set_time_zone} {should_set_dst} {should_set_default_ntp} {should_set_trusted_time}') + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureTimeZone), + should_set_time_zone, 'Incorrect value for time zone stage check') + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureDSTOffset), + should_set_dst, 'Incorrect value for kConfigureDSTOffset stage') + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureDefaultNTP), + should_set_default_ntp, 'Incorrect value for kConfigureDefaultNTP stage') + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureTrustedTimeSource), + should_set_trusted_time, 'Incorrect value for kConfigureTrustedTimeSource stage') + + @async_test_body + async def test_CommissioningAllBasic(self): + # We want to assess all combos (ie, all flags in the range of 0b0000 to 0b1111) + for i in range(0, 0xF): + time_zone = bool(i & 0x1) + dst = bool(i & 0x2) + default_ntp = bool(i & 0x4) + trusted_time_source = bool(i & 0x8) + await self.commission_stages(time_zone, dst, default_ntp, trusted_time_source) + + @async_test_body + async def test_CommissioningPreSetValues(self): + + await self.create_commissioner() + + # If we're running this against a node that doesn't have a time cluster, this test doesn't apply + # and the remaining cases are covered in the base case above. + try: + trusted_time_source = Clusters.TimeSynchronization.Structs.FabricScopedTrustedTimeSourceStruct( + nodeID=0x5555, endpoint=0) + cmd = Clusters.TimeSynchronization.Commands.SetTrustedTimeSource(trusted_time_source) + await self.send_single_cmd(cmd) + + cmd = Clusters.TimeSynchronization.Commands.SetDefaultNTP("fe80::02") + await self.send_single_cmd(cmd) + except InteractionModelError as e: + if e.status == Status.UnsupportedCluster: + await self.destroy_current_commissioner() + return + + self.commissioner.SetTimeZone(offset=3600, validAt=0) + six_months = 1.577e+13 # in us + self.commissioner.SetDSTOffset(offset=3600, validStarting=0, validUntil=utc_time_in_matter_epoch() + int(six_months)) + self.commissioner.SetDefaultNTP("fe80::1") + self.commissioner.SetTrustedTimeSource(self.commissioner.nodeId, 0) + + await self.commission_and_base_checks() + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureTimeZone), + self.supports_time_zone, 'Incorrect value for time zone stage check') + asserts.assert_equal(self.commissioner.CheckStageSuccessful(kConfigureDSTOffset), + self.supports_time_zone, 'Incorrect value for kConfigureDSTOffset stage') + asserts.assert_false(self.commissioner.CheckStageSuccessful(kConfigureDefaultNTP), 'kConfigureDefaultNTP incorrectly set') + asserts.assert_false(self.commissioner.CheckStageSuccessful( + kConfigureTrustedTimeSource), 'kConfigureTrustedTimeSource incorrectly set') + + +# TODO(cecille): Test - Add hooks to change the time zone response to indicate no DST is needed +# TODO(cecille): Test - Set commissioningParameters TimeZone and DST list size to > node list size to ensure they get truncated +if __name__ == "__main__": + default_matter_test_main()