| /* |
| * |
| * Copyright (c) 2021 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 <controller/AutoCommissioner.h> |
| |
| #include <cstring> |
| |
| #include <app/InteractionModelTimeout.h> |
| #include <controller/CHIPDeviceController.h> |
| #include <credentials/CHIPCert.h> |
| #include <lib/support/SafeInt.h> |
| |
| namespace chip { |
| namespace Controller { |
| |
| using namespace chip::app::Clusters; |
| using chip::app::DataModel::MakeNullable; |
| using chip::app::DataModel::NullNullable; |
| |
| AutoCommissioner::AutoCommissioner() |
| { |
| SetCommissioningParameters(CommissioningParameters()); |
| } |
| |
| AutoCommissioner::~AutoCommissioner() |
| { |
| ReleaseDAC(); |
| ReleasePAI(); |
| } |
| |
| void AutoCommissioner::SetOperationalCredentialsDelegate(OperationalCredentialsDelegate * operationalCredentialsDelegate) |
| { |
| mOperationalCredentialsDelegate = operationalCredentialsDelegate; |
| } |
| |
| // Returns true if maybeUnsafeSpan is pointing to a buffer that we're not sure |
| // will live for long enough. knownSafeSpan, if it has a value, points to a |
| // buffer that we _are_ sure will live for long enough. |
| template <typename SpanType> |
| static bool IsUnsafeSpan(const Optional<SpanType> & maybeUnsafeSpan, const Optional<SpanType> & knownSafeSpan) |
| { |
| if (!maybeUnsafeSpan.HasValue()) |
| { |
| return false; |
| } |
| |
| if (!knownSafeSpan.HasValue()) |
| { |
| return true; |
| } |
| |
| return maybeUnsafeSpan.Value().data() != knownSafeSpan.Value().data(); |
| } |
| |
| CHIP_ERROR AutoCommissioner::VerifyICDRegistrationInfo(const CommissioningParameters & params) |
| { |
| ChipLogProgress(Controller, "Checking ICD registration parameters"); |
| if (!params.GetICDSymmetricKey().HasValue()) |
| { |
| ChipLogError(Controller, "Missing ICD symmetric key!"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| if (params.GetICDSymmetricKey().Value().size() != sizeof(mICDSymmetricKey)) |
| { |
| ChipLogError(Controller, "Invalid ICD symmetric key length!"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| if (!params.GetICDCheckInNodeId().HasValue()) |
| { |
| ChipLogError(Controller, "Missing ICD check-in node id!"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| if (!params.GetICDMonitoredSubject().HasValue()) |
| { |
| ChipLogError(Controller, "Missing ICD monitored subject!"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParameters & params) |
| { |
| // Make sure any members that point to buffers that we are not pointing to |
| // our own buffers are not going to dangle. We can skip this step if all |
| // the buffers pointers that we don't plan to re-point to our own buffers |
| // below are already pointing to the same things as our own buffer pointers |
| // (so that we know they have to be safe somehow). |
| // |
| // The checks are a bit painful, because Span does not have a usable |
| // operator==, and in any case, we want to compare for pointer equality, not |
| // data equality. |
| bool haveMaybeDanglingBufferPointers = |
| ((params.GetNOCChainGenerationParameters().HasValue() && |
| (!mParams.GetNOCChainGenerationParameters().HasValue() || |
| params.GetNOCChainGenerationParameters().Value().nocsrElements.data() != |
| mParams.GetNOCChainGenerationParameters().Value().nocsrElements.data() || |
| params.GetNOCChainGenerationParameters().Value().signature.data() != |
| mParams.GetNOCChainGenerationParameters().Value().signature.data())) || |
| IsUnsafeSpan(params.GetRootCert(), mParams.GetRootCert()) || IsUnsafeSpan(params.GetNoc(), mParams.GetNoc()) || |
| IsUnsafeSpan(params.GetIcac(), mParams.GetIcac()) || IsUnsafeSpan(params.GetIpk(), mParams.GetIpk()) || |
| IsUnsafeSpan(params.GetAttestationElements(), mParams.GetAttestationElements()) || |
| IsUnsafeSpan(params.GetAttestationSignature(), mParams.GetAttestationSignature()) || |
| IsUnsafeSpan(params.GetPAI(), mParams.GetPAI()) || IsUnsafeSpan(params.GetDAC(), mParams.GetDAC()) || |
| IsUnsafeSpan(params.GetTimeZone(), mParams.GetTimeZone()) || |
| IsUnsafeSpan(params.GetDSTOffsets(), mParams.GetDSTOffsets()) || |
| IsUnsafeSpan(params.GetICDSymmetricKey(), mParams.GetICDSymmetricKey()) || |
| (params.GetDefaultNTP().HasValue() && !params.GetDefaultNTP().Value().IsNull() && |
| params.GetDefaultNTP().Value().Value().data() != mDefaultNtp)); |
| |
| mParams = params; |
| |
| if (haveMaybeDanglingBufferPointers) |
| { |
| mParams.ClearExternalBufferDependentValues(); |
| } |
| |
| // For members of params that point to some sort of buffer, we have to copy |
| // the data over into our own buffers. |
| |
| if (params.GetThreadOperationalDataset().HasValue()) |
| { |
| ByteSpan dataset = params.GetThreadOperationalDataset().Value(); |
| if (dataset.size() > CommissioningParameters::kMaxThreadDatasetLen) |
| { |
| ChipLogError(Controller, "Thread operational data set is too large"); |
| // Make sure our buffer pointers don't dangle. |
| mParams.ClearExternalBufferDependentValues(); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| memcpy(mThreadOperationalDataset, dataset.data(), dataset.size()); |
| ChipLogProgress(Controller, "Setting thread operational dataset from parameters"); |
| mParams.SetThreadOperationalDataset(ByteSpan(mThreadOperationalDataset, dataset.size())); |
| } |
| |
| if (params.GetWiFiCredentials().HasValue()) |
| { |
| WiFiCredentials creds = params.GetWiFiCredentials().Value(); |
| if (creds.ssid.size() > CommissioningParameters::kMaxSsidLen || |
| creds.credentials.size() > CommissioningParameters::kMaxCredentialsLen) |
| { |
| ChipLogError(Controller, "Wifi credentials are too large"); |
| // Make sure our buffer pointers don't dangle. |
| mParams.ClearExternalBufferDependentValues(); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| memcpy(mSsid, creds.ssid.data(), creds.ssid.size()); |
| memcpy(mCredentials, creds.credentials.data(), creds.credentials.size()); |
| ChipLogProgress(Controller, "Setting wifi credentials from parameters"); |
| mParams.SetWiFiCredentials( |
| WiFiCredentials(ByteSpan(mSsid, creds.ssid.size()), ByteSpan(mCredentials, creds.credentials.size()))); |
| } |
| |
| if (params.GetCountryCode().HasValue()) |
| { |
| auto code = params.GetCountryCode().Value(); |
| MutableCharSpan copiedCode(mCountryCode); |
| if (CopyCharSpanToMutableCharSpan(code, copiedCode) == CHIP_NO_ERROR) |
| { |
| mParams.SetCountryCode(copiedCode); |
| } |
| else |
| { |
| ChipLogError(Controller, "Country code is too large: %u", static_cast<unsigned>(code.size())); |
| // Make sure our buffer pointers don't dangle. |
| mParams.ClearExternalBufferDependentValues(); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| } |
| |
| // If the AttestationNonce is passed in, using that else using a random one.. |
| if (params.GetAttestationNonce().HasValue()) |
| { |
| ChipLogProgress(Controller, "Setting attestation nonce from parameters"); |
| VerifyOrReturnError(params.GetAttestationNonce().Value().size() == sizeof(mAttestationNonce), CHIP_ERROR_INVALID_ARGUMENT); |
| memcpy(mAttestationNonce, params.GetAttestationNonce().Value().data(), params.GetAttestationNonce().Value().size()); |
| } |
| else |
| { |
| ChipLogProgress(Controller, "Setting attestation nonce to random value"); |
| Crypto::DRBG_get_bytes(mAttestationNonce, sizeof(mAttestationNonce)); |
| } |
| mParams.SetAttestationNonce(ByteSpan(mAttestationNonce, sizeof(mAttestationNonce))); |
| |
| if (params.GetCSRNonce().HasValue()) |
| { |
| ChipLogProgress(Controller, "Setting CSR nonce from parameters"); |
| VerifyOrReturnError(params.GetCSRNonce().Value().size() == sizeof(mCSRNonce), CHIP_ERROR_INVALID_ARGUMENT); |
| memcpy(mCSRNonce, params.GetCSRNonce().Value().data(), params.GetCSRNonce().Value().size()); |
| } |
| else |
| { |
| ChipLogProgress(Controller, "Setting CSR nonce to random value"); |
| Crypto::DRBG_get_bytes(mCSRNonce, sizeof(mCSRNonce)); |
| } |
| mParams.SetCSRNonce(ByteSpan(mCSRNonce, sizeof(mCSRNonce))); |
| |
| if (params.GetDSTOffsets().HasValue()) |
| { |
| ChipLogProgress(Controller, "Setting DST offsets from parameters"); |
| size_t size = std::min(params.GetDSTOffsets().Value().size(), kMaxSupportedDstStructs); |
| for (size_t i = 0; i < size; ++i) |
| { |
| mDstOffsetsBuf[i] = params.GetDSTOffsets().Value()[i]; |
| } |
| auto list = app::DataModel::List<app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type>(mDstOffsetsBuf, size); |
| mParams.SetDSTOffsets(list); |
| } |
| if (params.GetTimeZone().HasValue()) |
| { |
| ChipLogProgress(Controller, "Setting Time Zone from parameters"); |
| size_t size = std::min(params.GetTimeZone().Value().size(), kMaxSupportedTimeZones); |
| for (size_t i = 0; i < size; ++i) |
| { |
| mTimeZoneBuf[i] = params.GetTimeZone().Value()[i]; |
| if (params.GetTimeZone().Value()[i].name.HasValue() && |
| params.GetTimeZone().Value()[i].name.Value().size() <= kMaxTimeZoneNameLen) |
| { |
| auto span = MutableCharSpan(mTimeZoneNames[i], kMaxTimeZoneNameLen); |
| // The buffer backing "span" is statically allocated and is of size kMaxSupportedTimeZones, so this should never |
| // fail. |
| CopyCharSpanToMutableCharSpan(params.GetTimeZone().Value()[i].name.Value(), span); |
| mTimeZoneBuf[i].name.SetValue(span); |
| } |
| else |
| { |
| mTimeZoneBuf[i].name.ClearValue(); |
| } |
| } |
| auto list = app::DataModel::List<app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>(mTimeZoneBuf, size); |
| mParams.SetTimeZone(list); |
| } |
| if (params.GetDefaultNTP().HasValue()) |
| { |
| ChipLogProgress(Controller, "Setting Default NTP from parameters"); |
| // This parameter is an optional nullable, so we need to go two levels deep here. |
| if (!params.GetDefaultNTP().Value().IsNull() && params.GetDefaultNTP().Value().Value().size() <= kMaxDefaultNtpSize) |
| { |
| // The buffer backing "span" is statically allocated and is of size kMaxDefaultNtpSize. |
| auto span = MutableCharSpan(mDefaultNtp, kMaxDefaultNtpSize); |
| CopyCharSpanToMutableCharSpan(params.GetDefaultNTP().Value().Value(), span); |
| auto default_ntp = MakeNullable(CharSpan(mDefaultNtp, params.GetDefaultNTP().Value().Value().size())); |
| mParams.SetDefaultNTP(default_ntp); |
| } |
| } |
| |
| if (params.GetICDRegistrationStrategy() != ICDRegistrationStrategy::kIgnore && params.GetICDSymmetricKey().HasValue()) |
| { |
| ReturnErrorOnFailure(VerifyICDRegistrationInfo(params)); |
| |
| // The values must be valid now. |
| memcpy(mICDSymmetricKey, params.GetICDSymmetricKey().Value().data(), params.GetICDSymmetricKey().Value().size()); |
| mParams.SetICDSymmetricKey(ByteSpan(mICDSymmetricKey)); |
| mParams.SetICDCheckInNodeId(params.GetICDCheckInNodeId().Value()); |
| mParams.SetICDMonitoredSubject(params.GetICDMonitoredSubject().Value()); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| const CommissioningParameters & AutoCommissioner::GetCommissioningParameters() const |
| { |
| return mParams; |
| } |
| |
| CommissioningStage AutoCommissioner::GetNextCommissioningStage(CommissioningStage currentStage, CHIP_ERROR & lastErr) |
| { |
| auto nextStage = GetNextCommissioningStageInternal(currentStage, lastErr); |
| if (lastErr == CHIP_NO_ERROR) |
| { |
| ChipLogProgress(Controller, "Commissioning stage next step: '%s' -> '%s'", StageToString(currentStage), |
| StageToString(nextStage)); |
| } |
| else |
| { |
| ChipLogProgress(Controller, "Going from commissioning step '%s' with lastErr = '%s' -> '%s'", StageToString(currentStage), |
| lastErr.AsString(), StageToString(nextStage)); |
| } |
| return nextStage; |
| } |
| |
| CommissioningStage AutoCommissioner::GetNextCommissioningStageNetworkSetup(CommissioningStage currentStage, CHIP_ERROR & lastErr) |
| { |
| if (mParams.GetWiFiCredentials().HasValue() && mDeviceCommissioningInfo.network.wifi.endpoint != kInvalidEndpointId) |
| { |
| return CommissioningStage::kWiFiNetworkSetup; |
| } |
| if (mParams.GetThreadOperationalDataset().HasValue() && mDeviceCommissioningInfo.network.thread.endpoint != kInvalidEndpointId) |
| { |
| return CommissioningStage::kThreadNetworkSetup; |
| } |
| |
| ChipLogError(Controller, "Required network information not provided in commissioning parameters"); |
| ChipLogError(Controller, "Parameters supplied: wifi (%s) thread (%s)", mParams.GetWiFiCredentials().HasValue() ? "yes" : "no", |
| mParams.GetThreadOperationalDataset().HasValue() ? "yes" : "no"); |
| ChipLogError(Controller, "Device supports: wifi (%s) thread(%s)", |
| mDeviceCommissioningInfo.network.wifi.endpoint == kInvalidEndpointId ? "no" : "yes", |
| mDeviceCommissioningInfo.network.thread.endpoint == kInvalidEndpointId ? "no" : "yes"); |
| lastErr = CHIP_ERROR_INVALID_ARGUMENT; |
| return CommissioningStage::kCleanup; |
| } |
| |
| CommissioningStage AutoCommissioner::GetNextCommissioningStageInternal(CommissioningStage currentStage, CHIP_ERROR & lastErr) |
| { |
| if (mStopCommissioning) |
| { |
| return CommissioningStage::kCleanup; |
| } |
| if (lastErr != CHIP_NO_ERROR) |
| { |
| return CommissioningStage::kCleanup; |
| } |
| |
| switch (currentStage) |
| { |
| case CommissioningStage::kSecurePairing: |
| return CommissioningStage::kReadCommissioningInfo; |
| case CommissioningStage::kReadCommissioningInfo: |
| if (mDeviceCommissioningInfo.general.breadcrumb > 0) |
| { |
| // If the breadcrumb is 0, the failsafe was disarmed. |
| // We failed on network setup or later, the node failsafe has not been re-armed and the breadcrumb has not been reset. |
| // Per the spec, we restart from after adding the NOC. |
| return GetNextCommissioningStage(CommissioningStage::kSendNOC, lastErr); |
| } |
| return CommissioningStage::kReadCommissioningInfo2; |
| case CommissioningStage::kReadCommissioningInfo2: |
| 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; |
| case CommissioningStage::kSendDACCertificateRequest: |
| return CommissioningStage::kSendAttestationRequest; |
| case CommissioningStage::kSendAttestationRequest: |
| return CommissioningStage::kAttestationVerification; |
| case CommissioningStage::kAttestationVerification: |
| return CommissioningStage::kSendOpCertSigningRequest; |
| case CommissioningStage::kSendOpCertSigningRequest: |
| return CommissioningStage::kValidateCSR; |
| case CommissioningStage::kValidateCSR: |
| return CommissioningStage::kGenerateNOCChain; |
| case CommissioningStage::kGenerateNOCChain: |
| return CommissioningStage::kSendTrustedRootCert; |
| 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: |
| if (mNeedIcdRegistration) |
| { |
| if (mParams.GetICDCheckInNodeId().HasValue() && mParams.GetICDMonitoredSubject().HasValue() && |
| mParams.GetICDSymmetricKey().HasValue()) |
| { |
| return CommissioningStage::kICDRegistration; |
| } |
| return CommissioningStage::kICDGetRegistrationInfo; |
| } |
| return GetNextCommissioningStageInternal(CommissioningStage::kICDSendStayActive, lastErr); |
| case CommissioningStage::kICDGetRegistrationInfo: |
| return CommissioningStage::kICDRegistration; |
| case CommissioningStage::kICDRegistration: |
| // TODO(#24259): StayActiveRequest is not supported by server. We may want to SendStayActive after OpDiscovery. |
| return CommissioningStage::kICDSendStayActive; |
| case CommissioningStage::kICDSendStayActive: |
| // 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. |
| if (mNeedsNetworkSetup) |
| { |
| // if there is a WiFi or a Thread endpoint, then perform scan |
| if (IsScanNeeded()) |
| { |
| // Perform Scan (kScanNetworks) and collect credentials (kNeedsNetworkCreds) right before configuring network. |
| // This order of steps allows the workflow to return to collect credentials again if network enablement fails. |
| return CommissioningStage::kScanNetworks; |
| } |
| ChipLogProgress(Controller, "No NetworkScan enabled or WiFi/Thread endpoint not specified, skipping ScanNetworks"); |
| |
| return GetNextCommissioningStageNetworkSetup(currentStage, lastErr); |
| } |
| else |
| { |
| SetCASEFailsafeTimerIfNeeded(); |
| if (mParams.GetSkipCommissioningComplete().ValueOr(false)) |
| { |
| return CommissioningStage::kCleanup; |
| } |
| return CommissioningStage::kFindOperational; |
| } |
| case CommissioningStage::kScanNetworks: |
| return CommissioningStage::kNeedsNetworkCreds; |
| case CommissioningStage::kNeedsNetworkCreds: |
| return GetNextCommissioningStageNetworkSetup(currentStage, lastErr); |
| case CommissioningStage::kWiFiNetworkSetup: |
| if (mParams.GetThreadOperationalDataset().HasValue() && |
| mDeviceCommissioningInfo.network.thread.endpoint != kInvalidEndpointId) |
| { |
| return CommissioningStage::kThreadNetworkSetup; |
| } |
| else |
| { |
| return CommissioningStage::kFailsafeBeforeWiFiEnable; |
| } |
| case CommissioningStage::kThreadNetworkSetup: |
| if (mParams.GetWiFiCredentials().HasValue() && mDeviceCommissioningInfo.network.wifi.endpoint != kInvalidEndpointId) |
| { |
| return CommissioningStage::kFailsafeBeforeWiFiEnable; |
| } |
| else |
| { |
| return CommissioningStage::kFailsafeBeforeThreadEnable; |
| } |
| case CommissioningStage::kFailsafeBeforeWiFiEnable: |
| return CommissioningStage::kWiFiNetworkEnable; |
| case CommissioningStage::kFailsafeBeforeThreadEnable: |
| return CommissioningStage::kThreadNetworkEnable; |
| case CommissioningStage::kWiFiNetworkEnable: |
| if (mParams.GetThreadOperationalDataset().HasValue() && |
| mDeviceCommissioningInfo.network.thread.endpoint != kInvalidEndpointId) |
| { |
| return CommissioningStage::kThreadNetworkEnable; |
| } |
| else if (mParams.GetSkipCommissioningComplete().ValueOr(false)) |
| { |
| SetCASEFailsafeTimerIfNeeded(); |
| return CommissioningStage::kCleanup; |
| } |
| else |
| { |
| SetCASEFailsafeTimerIfNeeded(); |
| return CommissioningStage::kFindOperational; |
| } |
| case CommissioningStage::kThreadNetworkEnable: |
| SetCASEFailsafeTimerIfNeeded(); |
| if (mParams.GetSkipCommissioningComplete().ValueOr(false)) |
| { |
| return CommissioningStage::kCleanup; |
| } |
| return CommissioningStage::kFindOperational; |
| case CommissioningStage::kFindOperational: |
| return CommissioningStage::kSendComplete; |
| case CommissioningStage::kSendComplete: |
| return CommissioningStage::kCleanup; |
| |
| // Neither of these have a next stage so return kError; |
| case CommissioningStage::kCleanup: |
| case CommissioningStage::kError: |
| return CommissioningStage::kError; |
| } |
| return CommissioningStage::kError; |
| } |
| |
| // No specific actions to take when an error happens since this command can fail and commissioning can still succeed. |
| static void OnFailsafeFailureForCASE(void * context, CHIP_ERROR error) |
| { |
| ChipLogProgress(Controller, "ExtendFailsafe received failure response %s\n", chip::ErrorStr(error)); |
| } |
| |
| // No specific actions to take upon success. |
| static void |
| OnExtendFailsafeSuccessForCASE(void * context, |
| const app::Clusters::GeneralCommissioning::Commands::ArmFailSafeResponse::DecodableType & data) |
| { |
| ChipLogProgress(Controller, "ExtendFailsafe received ArmFailSafe response errorCode=%u", to_underlying(data.errorCode)); |
| } |
| |
| void AutoCommissioner::SetCASEFailsafeTimerIfNeeded() |
| { |
| // if there is a final fail-safe timer configured then, send it |
| if (mParams.GetCASEFailsafeTimerSeconds().HasValue() && mCommissioneeDeviceProxy != nullptr) |
| { |
| // send the command via the PASE session (mCommissioneeDeviceProxy) since the CASE portion of commissioning |
| // might be done by a different service (ex. PASE is done by a phone app and CASE is done by a Hub). |
| // Also, we want the CASE failsafe timer to apply for the time it takes the Hub to perform operational discovery, |
| // CASE establishment, and receipt of the commissioning complete command. |
| // We know that the mCommissioneeDeviceProxy is still valid at this point since it gets cleared during cleanup |
| // and SetCASEFailsafeTimerIfNeeded is always called before that stage. |
| // |
| // A false return from ExtendArmFailSafe is fine; we don't want to make |
| // the fail-safe shorter here. |
| mCommissioner->ExtendArmFailSafe(mCommissioneeDeviceProxy, CommissioningStage::kFindOperational, |
| mParams.GetCASEFailsafeTimerSeconds().Value(), |
| GetCommandTimeout(mCommissioneeDeviceProxy, CommissioningStage::kArmFailsafe), |
| OnExtendFailsafeSuccessForCASE, OnFailsafeFailureForCASE); |
| } |
| } |
| |
| EndpointId AutoCommissioner::GetEndpoint(const CommissioningStage & stage) const |
| { |
| switch (stage) |
| { |
| case CommissioningStage::kWiFiNetworkSetup: |
| case CommissioningStage::kWiFiNetworkEnable: |
| return mDeviceCommissioningInfo.network.wifi.endpoint; |
| case CommissioningStage::kThreadNetworkSetup: |
| case CommissioningStage::kThreadNetworkEnable: |
| return mDeviceCommissioningInfo.network.thread.endpoint; |
| default: |
| return kRootEndpointId; |
| } |
| } |
| |
| CHIP_ERROR AutoCommissioner::StartCommissioning(DeviceCommissioner * commissioner, CommissioneeDeviceProxy * proxy) |
| { |
| if (commissioner == nullptr) |
| { |
| ChipLogError(Controller, "Invalid DeviceCommissioner"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| |
| if (proxy == nullptr || !proxy->GetSecureSession().HasValue()) |
| { |
| ChipLogError(Controller, "Device proxy secure session error"); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| mStopCommissioning = false; |
| mCommissioner = commissioner; |
| mCommissioneeDeviceProxy = proxy; |
| mNeedsNetworkSetup = |
| mCommissioneeDeviceProxy->GetSecureSession().Value()->AsSecureSession()->GetPeerAddress().GetTransportType() == |
| Transport::Type::kBle; |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| CommissioningStage nextStage = GetNextCommissioningStage(CommissioningStage::kSecurePairing, err); |
| mCommissioner->PerformCommissioningStep(mCommissioneeDeviceProxy, nextStage, mParams, this, GetEndpoint(nextStage), |
| GetCommandTimeout(mCommissioneeDeviceProxy, nextStage)); |
| return CHIP_NO_ERROR; |
| } |
| |
| Optional<System::Clock::Timeout> AutoCommissioner::GetCommandTimeout(DeviceProxy * device, CommissioningStage stage) const |
| { |
| // Network clusters can indicate the time required to connect, so if we are |
| // connecting, use that time as our "how long it takes to process server |
| // side" time. Otherwise pick a time that should be enough for the command |
| // processing: 7s for slow steps that can involve crypto, the default IM |
| // timeout otherwise. |
| // TODO: is this a reasonable estimate for the slow-crypto cases? |
| constexpr System::Clock::Timeout kSlowCryptoProcessingTime = System::Clock::Seconds16(7); |
| |
| System::Clock::Timeout timeout; |
| switch (stage) |
| { |
| case CommissioningStage::kWiFiNetworkEnable: |
| ChipLogProgress(Controller, "Setting wifi connection time min = %u", |
| mDeviceCommissioningInfo.network.wifi.minConnectionTime); |
| timeout = System::Clock::Seconds16(mDeviceCommissioningInfo.network.wifi.minConnectionTime); |
| break; |
| case CommissioningStage::kThreadNetworkEnable: |
| timeout = System::Clock::Seconds16(mDeviceCommissioningInfo.network.thread.minConnectionTime); |
| break; |
| case CommissioningStage::kSendNOC: |
| case CommissioningStage::kSendOpCertSigningRequest: |
| timeout = kSlowCryptoProcessingTime; |
| break; |
| default: |
| timeout = app::kExpectedIMProcessingTime; |
| break; |
| } |
| |
| // Adjust the timeout for our session transport latency, if we have access |
| // to a session. |
| auto sessionHandle = device->GetSecureSession(); |
| if (sessionHandle.HasValue()) |
| { |
| timeout = sessionHandle.Value()->ComputeRoundTripTimeout(timeout); |
| } |
| |
| // Enforce the spec minimal timeout. Maybe this enforcement should live in |
| // the DeviceCommissioner? |
| if (timeout < kMinimumCommissioningStepTimeout) |
| { |
| timeout = kMinimumCommissioningStepTimeout; |
| } |
| |
| return MakeOptional(timeout); |
| } |
| |
| CHIP_ERROR AutoCommissioner::NOCChainGenerated(ByteSpan noc, ByteSpan icac, ByteSpan rcac, IdentityProtectionKeySpan ipk, |
| NodeId adminSubject) |
| { |
| // Reuse ICA Cert buffer for temporary store Root Cert. |
| MutableByteSpan rootCert = MutableByteSpan(mICACertBuffer); |
| ReturnErrorOnFailure(Credentials::ConvertX509CertToChipCert(rcac, rootCert)); |
| mParams.SetRootCert(rootCert); |
| |
| MutableByteSpan noCert = MutableByteSpan(mNOCertBuffer); |
| ReturnErrorOnFailure(Credentials::ConvertX509CertToChipCert(noc, noCert)); |
| mParams.SetNoc(noCert); |
| |
| CommissioningStage nextStage = CommissioningStage::kSendTrustedRootCert; |
| mCommissioner->PerformCommissioningStep(mCommissioneeDeviceProxy, nextStage, mParams, this, 0, |
| GetCommandTimeout(mCommissioneeDeviceProxy, nextStage)); |
| |
| // Trusted root cert has been sent, so we can re-use the icac buffer for the icac. |
| if (!icac.empty()) |
| { |
| MutableByteSpan icaCert = MutableByteSpan(mICACertBuffer); |
| ReturnErrorOnFailure(Credentials::ConvertX509CertToChipCert(icac, icaCert)); |
| mParams.SetIcac(icaCert); |
| } |
| else |
| { |
| mParams.SetIcac(ByteSpan()); |
| } |
| |
| mParams.SetIpk(ipk); |
| mParams.SetAdminSubject(adminSubject); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, CommissioningDelegate::CommissioningReport report) |
| { |
| CompletionStatus completionStatus; |
| completionStatus.err = err; |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(Controller, "Error on commissioning step '%s': '%s'", StageToString(report.stageCompleted), err.AsString()); |
| completionStatus.failedStage = MakeOptional(report.stageCompleted); |
| if (report.Is<AttestationErrorInfo>()) |
| { |
| completionStatus.attestationResult = MakeOptional(report.Get<AttestationErrorInfo>().attestationResult); |
| if ((report.Get<AttestationErrorInfo>().attestationResult == |
| Credentials::AttestationVerificationResult::kDacProductIdMismatch) || |
| (report.Get<AttestationErrorInfo>().attestationResult == |
| Credentials::AttestationVerificationResult::kDacVendorIdMismatch)) |
| { |
| ChipLogError(Controller, |
| "Failed device attestation. Device vendor and/or product ID do not match the IDs expected. " |
| "Verify DAC certificate chain and certification declaration to ensure spec rules followed."); |
| } |
| } |
| else if (report.Is<CommissioningErrorInfo>()) |
| { |
| completionStatus.commissioningError = MakeOptional(report.Get<CommissioningErrorInfo>().commissioningError); |
| } |
| else if (report.Is<NetworkCommissioningStatusInfo>()) |
| { |
| // This report type is used when an error happens in either NetworkConfig or ConnectNetwork commands |
| completionStatus.networkCommissioningStatus = |
| MakeOptional(report.Get<NetworkCommissioningStatusInfo>().networkCommissioningStatus); |
| |
| // If we are configured to scan networks, then don't error out. |
| // Instead, allow the app to try another network. |
| if (IsScanNeeded()) |
| { |
| if (completionStatus.err == CHIP_NO_ERROR) |
| { |
| completionStatus.err = err; |
| } |
| err = CHIP_NO_ERROR; |
| // Walk back the completed stage to kScanNetworks. |
| // This will allow the app to try another network. |
| report.stageCompleted = CommissioningStage::kScanNetworks; |
| } |
| } |
| } |
| else |
| { |
| ChipLogProgress(Controller, "Successfully finished commissioning step '%s'", StageToString(report.stageCompleted)); |
| switch (report.stageCompleted) |
| { |
| case CommissioningStage::kReadCommissioningInfo: |
| break; |
| case CommissioningStage::kReadCommissioningInfo2: { |
| mDeviceCommissioningInfo = report.Get<ReadCommissioningInfo>(); |
| |
| if (!mParams.GetFailsafeTimerSeconds().HasValue() && mDeviceCommissioningInfo.general.recommendedFailsafe > 0) |
| { |
| mParams.SetFailsafeTimerSeconds(mDeviceCommissioningInfo.general.recommendedFailsafe); |
| } |
| mParams.SetRemoteVendorId(mDeviceCommissioningInfo.basic.vendorId) |
| .SetRemoteProductId(mDeviceCommissioningInfo.basic.productId) |
| .SetDefaultRegulatoryLocation(mDeviceCommissioningInfo.general.currentRegulatoryLocation) |
| .SetLocationCapability(mDeviceCommissioningInfo.general.locationCapability); |
| // Don't send DST unless the device says it needs it |
| mNeedsDST = false; |
| |
| mParams.SetSupportsConcurrentConnection(mDeviceCommissioningInfo.supportsConcurrentConnection); |
| |
| if (mParams.GetCheckForMatchingFabric()) |
| { |
| chip::NodeId nodeId = mDeviceCommissioningInfo.remoteNodeId; |
| if (nodeId != kUndefinedNodeId) |
| { |
| mParams.SetRemoteNodeId(nodeId); |
| } |
| } |
| |
| if (mParams.GetICDRegistrationStrategy() != ICDRegistrationStrategy::kIgnore) |
| { |
| if (mDeviceCommissioningInfo.icd.isLIT && mDeviceCommissioningInfo.icd.checkInProtocolSupport) |
| { |
| mNeedIcdRegistration = true; |
| ChipLogDetail(Controller, "AutoCommissioner: ICD supports the check-in protocol."); |
| } |
| } |
| break; |
| } |
| case CommissioningStage::kConfigureTimeZone: |
| mNeedsDST = report.Get<TimeZoneResponseInfo>().requiresDSTOffsets; |
| break; |
| case CommissioningStage::kSendPAICertificateRequest: |
| SetPAI(report.Get<RequestedCertificate>().certificate); |
| break; |
| case CommissioningStage::kSendDACCertificateRequest: |
| SetDAC(report.Get<RequestedCertificate>().certificate); |
| break; |
| case CommissioningStage::kSendAttestationRequest: { |
| auto & elements = report.Get<AttestationResponse>().attestationElements; |
| auto & signature = report.Get<AttestationResponse>().signature; |
| if (elements.size() > sizeof(mAttestationElements)) |
| { |
| ChipLogError(Controller, "AutoCommissioner attestationElements buffer size %u larger than cache size %u", |
| static_cast<unsigned>(elements.size()), static_cast<unsigned>(sizeof(mAttestationElements))); |
| return CHIP_ERROR_MESSAGE_TOO_LONG; |
| } |
| memcpy(mAttestationElements, elements.data(), elements.size()); |
| mAttestationElementsLen = static_cast<uint16_t>(elements.size()); |
| mParams.SetAttestationElements(ByteSpan(mAttestationElements, elements.size())); |
| ChipLogDetail(Controller, "AutoCommissioner setting attestationElements buffer size %u/%u", |
| static_cast<unsigned>(elements.size()), |
| static_cast<unsigned>(mParams.GetAttestationElements().Value().size())); |
| |
| if (signature.size() > sizeof(mAttestationSignature)) |
| { |
| ChipLogError(Controller, |
| "AutoCommissioner attestationSignature buffer size %u larger than " |
| "cache size %u", |
| static_cast<unsigned>(signature.size()), static_cast<unsigned>(sizeof(mAttestationSignature))); |
| return CHIP_ERROR_MESSAGE_TOO_LONG; |
| } |
| memcpy(mAttestationSignature, signature.data(), signature.size()); |
| mAttestationSignatureLen = static_cast<uint16_t>(signature.size()); |
| mParams.SetAttestationSignature(ByteSpan(mAttestationSignature, signature.size())); |
| |
| // TODO: Does this need to be done at runtime? Seems like this could be done earlier and we wouldn't need to hold a |
| // reference to the operational credential delegate here |
| if (mOperationalCredentialsDelegate != nullptr) |
| { |
| MutableByteSpan nonce(mCSRNonce); |
| ReturnErrorOnFailure(mOperationalCredentialsDelegate->ObtainCsrNonce(nonce)); |
| mParams.SetCSRNonce(ByteSpan(mCSRNonce, sizeof(mCSRNonce))); |
| } |
| break; |
| } |
| case CommissioningStage::kSendOpCertSigningRequest: { |
| NOCChainGenerationParameters nocParams; |
| nocParams.nocsrElements = report.Get<CSRResponse>().nocsrElements; |
| nocParams.signature = report.Get<CSRResponse>().signature; |
| mParams.SetNOCChainGenerationParameters(nocParams); |
| } |
| break; |
| case CommissioningStage::kGenerateNOCChain: |
| // For NOC chain generation, we re-use the buffers. NOCChainGenerated triggers the next stage before |
| // storing the returned certs, so just return here without triggering the next stage. |
| return NOCChainGenerated(report.Get<NocChain>().noc, report.Get<NocChain>().icac, report.Get<NocChain>().rcac, |
| report.Get<NocChain>().ipk, report.Get<NocChain>().adminSubject); |
| case CommissioningStage::kICDGetRegistrationInfo: |
| // Noting to do. The ICD registation info is handled elsewhere. |
| break; |
| case CommissioningStage::kICDRegistration: |
| // Noting to do. DevicePairingDelegate will handle this. |
| break; |
| case CommissioningStage::kICDSendStayActive: |
| // Nothing to do. |
| break; |
| case CommissioningStage::kFindOperational: |
| mOperationalDeviceProxy = report.Get<OperationalNodeFoundData>().operationalProxy; |
| break; |
| case CommissioningStage::kCleanup: |
| ReleasePAI(); |
| ReleaseDAC(); |
| mCommissioneeDeviceProxy = nullptr; |
| mOperationalDeviceProxy = OperationalDeviceProxy(); |
| mDeviceCommissioningInfo = ReadCommissioningInfo(); |
| mNeedsDST = false; |
| return CHIP_NO_ERROR; |
| default: |
| break; |
| } |
| } |
| |
| CommissioningStage nextStage = GetNextCommissioningStage(report.stageCompleted, err); |
| if (nextStage == CommissioningStage::kError) |
| { |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| |
| // If GetNextCommissioningStage indicated a failure, don't lose track of |
| // that. But don't overwrite any existing failures we had hanging |
| // around. |
| if (completionStatus.err == CHIP_NO_ERROR) |
| { |
| completionStatus.err = err; |
| } |
| mParams.SetCompletionStatus(completionStatus); |
| |
| return PerformStep(nextStage); |
| } |
| |
| DeviceProxy * AutoCommissioner::GetDeviceProxyForStep(CommissioningStage nextStage) |
| { |
| if (nextStage == CommissioningStage::kSendComplete || |
| (nextStage == CommissioningStage::kCleanup && mOperationalDeviceProxy.GetDeviceId() != kUndefinedNodeId)) |
| { |
| return &mOperationalDeviceProxy; |
| } |
| return mCommissioneeDeviceProxy; |
| } |
| |
| CHIP_ERROR AutoCommissioner::PerformStep(CommissioningStage nextStage) |
| { |
| DeviceProxy * proxy = GetDeviceProxyForStep(nextStage); |
| if (proxy == nullptr) |
| { |
| 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<app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>( |
| mParams.GetTimeZone().Value().SubSpan(0, mDeviceCommissioningInfo.maxTimeZoneSize))); |
| } |
| break; |
| case CommissioningStage::kConfigureDSTOffset: |
| if (mParams.GetDSTOffsets().Value().size() > mDeviceCommissioningInfo.maxDSTSize) |
| { |
| mParams.SetDSTOffsets(app::DataModel::List<app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type>( |
| mParams.GetDSTOffsets().Value().SubSpan(0, mDeviceCommissioningInfo.maxDSTSize))); |
| } |
| break; |
| default: |
| break; |
| } |
| |
| mCommissioner->PerformCommissioningStep(proxy, nextStage, mParams, this, GetEndpoint(nextStage), |
| GetCommandTimeout(proxy, nextStage)); |
| return CHIP_NO_ERROR; |
| } |
| |
| void AutoCommissioner::ReleaseDAC() |
| { |
| if (mDAC != nullptr) |
| { |
| Platform::MemoryFree(mDAC); |
| } |
| mDACLen = 0; |
| mDAC = nullptr; |
| } |
| |
| CHIP_ERROR AutoCommissioner::SetDAC(const ByteSpan & dac) |
| { |
| if (dac.size() == 0) |
| { |
| ReleaseDAC(); |
| return CHIP_NO_ERROR; |
| } |
| |
| VerifyOrReturnError(dac.size() <= Credentials::kMaxDERCertLength, CHIP_ERROR_INVALID_ARGUMENT); |
| if (mDACLen != 0) |
| { |
| ReleaseDAC(); |
| } |
| |
| VerifyOrReturnError(CanCastTo<uint16_t>(dac.size()), CHIP_ERROR_INVALID_ARGUMENT); |
| if (mDAC == nullptr) |
| { |
| mDAC = static_cast<uint8_t *>(chip::Platform::MemoryAlloc(dac.size())); |
| } |
| VerifyOrReturnError(mDAC != nullptr, CHIP_ERROR_NO_MEMORY); |
| mDACLen = static_cast<uint16_t>(dac.size()); |
| memcpy(mDAC, dac.data(), mDACLen); |
| mParams.SetDAC(ByteSpan(mDAC, mDACLen)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| void AutoCommissioner::ReleasePAI() |
| { |
| if (mPAI != nullptr) |
| { |
| chip::Platform::MemoryFree(mPAI); |
| } |
| mPAILen = 0; |
| mPAI = nullptr; |
| } |
| |
| CHIP_ERROR AutoCommissioner::SetPAI(const chip::ByteSpan & pai) |
| { |
| if (pai.size() == 0) |
| { |
| ReleasePAI(); |
| return CHIP_NO_ERROR; |
| } |
| |
| VerifyOrReturnError(pai.size() <= Credentials::kMaxDERCertLength, CHIP_ERROR_INVALID_ARGUMENT); |
| if (mPAILen != 0) |
| { |
| ReleasePAI(); |
| } |
| |
| VerifyOrReturnError(CanCastTo<uint16_t>(pai.size()), CHIP_ERROR_INVALID_ARGUMENT); |
| if (mPAI == nullptr) |
| { |
| mPAI = static_cast<uint8_t *>(chip::Platform::MemoryAlloc(pai.size())); |
| } |
| VerifyOrReturnError(mPAI != nullptr, CHIP_ERROR_NO_MEMORY); |
| mPAILen = static_cast<uint16_t>(pai.size()); |
| memcpy(mPAI, pai.data(), mPAILen); |
| mParams.SetPAI(ByteSpan(mPAI, mPAILen)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| } // namespace Controller |
| } // namespace chip |