| /* |
| * |
| * Copyright (c) 2026 Project CHIP Authors |
| * All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Commissioning a device onto a Wi-Fi network that uses Per-Device Credentials (PDC), i.e. Matter |
| // Core specification chapter 15. The work is split between the AutoCommissioner, which decides the |
| // stage sequence and owns the buffers the identities are copied into, and the DeviceCommissioner, |
| // which drives the NetworkIdentityRegistrar, verifies the commissionee's proof of possession, and |
| // owns the rollback; there is a fixture for each. |
| |
| #include <pw_unit_test/framework.h> |
| |
| #include <app/DeviceProxy.h> |
| #include <app/clusters/network-commissioning/constants.h> |
| #include <controller/AutoCommissioner.h> |
| #include <controller/CHIPDeviceController.h> |
| #include <controller/CommissioneeDeviceProxy.h> |
| #include <controller/CommissioningDelegate.h> |
| #include <controller/NetworkIdentityRegistrar.h> |
| #include <controller/tests/AutoCommissionerTestAccess.h> |
| #include <controller/tests/DeviceCommissionerTestAccess.h> |
| #include <credentials/CHIPCert.h> |
| #include <crypto/CHIPCryptoPAL.h> |
| #include <lib/core/StringBuilderAdapters.h> |
| #include <lib/support/CodeUtils.h> |
| #include <lib/support/Span.h> |
| |
| #include <cstring> |
| |
| // The commissioner and the Network Commissioning cluster have to agree on the size of the |
| // PossessionNonce, but the controller cannot include the cluster's constants.h (a server cluster |
| // implementation header), so CommissioningParameters declares its own copy. Pin the two together |
| // here, in a test that is free to include both. |
| static_assert(chip::Controller::CommissioningParameters::kPossessionNonceLen == |
| chip::app::Clusters::NetworkCommissioning::kPossessionNonceSize); |
| |
| using namespace chip; |
| using namespace chip::app::Clusters; |
| using namespace chip::Controller; |
| using namespace chip::Testing; |
| |
| namespace { |
| |
| // Records what the commissioner asked of it. GetNetworkIdentity() is never reached by either |
| // fixture, and RegisterClient() only by tests that opt in with AcceptRegistrations(), so those fail |
| // here to catch unexpected calls. |
| class MockNetworkIdentityRegistrar : public NetworkIdentityRegistrar |
| { |
| public: |
| void GetNetworkIdentity(Callback::Callback<OnNetworkIdentityAvailableFunct>::Owned onCompletion) override |
| { |
| ADD_FAILURE() << "unexpected GetNetworkIdentity()"; |
| onCompletion.Invoke(CHIP_ERROR_INCORRECT_STATE, ByteSpan()); |
| } |
| |
| void AcceptRegistrations() { mAcceptRegistrations = true; } |
| |
| /// Takes registration calls on but completes them with `status`. A determinate failure is one |
| /// where the registrar knows the AddClient it stands for did not take effect; an indeterminate |
| /// one leaves that open, which is what a lost response or a dropped session looks like. |
| void FailRegistrations(CHIP_ERROR status, bool determinate) |
| { |
| mAcceptRegistrations = true; |
| mRegistrationStatus = status; |
| mRegistrationIsDeterminate = determinate; |
| } |
| |
| void RegisterClient(ByteSpan clientIdentity, Callback::Callback<OnClientRegisteredFunct>::Owned onCompletion) override |
| { |
| if (!mAcceptRegistrations) |
| { |
| ADD_FAILURE() << "unexpected RegisterClient()"; |
| onCompletion.Invoke(CHIP_ERROR_INCORRECT_STATE, /* determinate = */ true); |
| return; |
| } |
| mRegisterCalls++; |
| VerifyOrReturn(clientIdentity.size() <= sizeof(mRegisteredIdentity), |
| onCompletion.Invoke(CHIP_ERROR_BUFFER_TOO_SMALL, /* determinate = */ true)); |
| memcpy(mRegisteredIdentity, clientIdentity.data(), clientIdentity.size()); |
| mRegisteredIdentityLen = clientIdentity.size(); |
| |
| // Completing synchronously is explicitly allowed, and there is no bookkeeping to it: the |
| // callback was never registered with us, so the caller has no opportunity to cancel it. |
| onCompletion.Invoke(mRegistrationStatus, mRegistrationIsDeterminate); |
| } |
| |
| /// Hold UnregisterClient() calls instead of completing them re-entrantly, so a test can drive the |
| /// points where the commissioner waits for a revocation. |
| void DeferRevocations() { mDeferRevocations = true; } |
| |
| bool HasPendingRevocation() const { return mPendingUnregister != nullptr; } |
| |
| /// Completes a held revocation, as a registrar does once it has heard back from the network. |
| void CompleteRevocation(CHIP_ERROR status = CHIP_NO_ERROR) |
| { |
| Callback::Cancelable * cancelable = mPendingUnregister; |
| ASSERT_NE(cancelable, nullptr); |
| mPendingUnregister = nullptr; |
| |
| // Surrender our registration before completing, so that the callback is a one-shot. Note this |
| // must not go through Cancel(): that runs OnRevocationCancelled(), which is how we recognise |
| // the commissioner giving up on a revocation, and a completion is the opposite of that. Clearing |
| // mCancel is enough -- the fields we borrowed are indeterminate again once we are unregistered. |
| cancelable->mCancel = nullptr; |
| |
| auto * onCompletion = Callback::Callback<OnClientUnregisteredFunct>::FromCancelable(cancelable); |
| onCompletion->Invoke(status); |
| } |
| |
| void UnregisterClient(Credentials::CertificateKeyId clientIdentifier, |
| Callback::Callback<OnClientUnregisteredFunct>::Owned onCompletion) override |
| { |
| mUnregisterCalls++; |
| memcpy(mUnregisteredIdentifier.data(), clientIdentifier.data(), mUnregisteredIdentifier.size()); |
| |
| if (!mDeferRevocations) |
| { |
| onCompletion.Invoke(CHIP_NO_ERROR); |
| return; |
| } |
| |
| // Enforce the contract rather than quietly taking the callback over, as a real registrar does: |
| // a caller only ever has one registration to give up, so this means it has lost track of one. |
| // (A caller reusing the same callback object cancels the previous revocation when it hands the |
| // callback over, and so is never caught by this.) |
| VerifyOrReturn(mPendingUnregister == nullptr, onCompletion.Invoke(CHIP_ERROR_INCORRECT_STATE)); |
| |
| // Take ownership of the Cancelable and install a cancel function, so that IsRegistered() |
| // holds while the request is in flight and we notice if the commissioner gives up on it. |
| Callback::Cancelable * cancelable = onCompletion.Take(); |
| cancelable->mContextA = this; |
| cancelable->mCancel = OnRevocationCancelled; |
| mPendingUnregister = cancelable; |
| } |
| |
| ByteSpan RegisteredIdentity() const { return ByteSpan(mRegisteredIdentity, mRegisteredIdentityLen); } |
| ByteSpan UnregisteredIdentifier() const { return ByteSpan(mUnregisteredIdentifier.data(), mUnregisteredIdentifier.size()); } |
| |
| int mRegisterCalls = 0; |
| int mUnregisterCalls = 0; |
| int mCancelledRevocations = 0; |
| |
| private: |
| static void OnRevocationCancelled(Callback::Cancelable * cancelable) |
| { |
| auto * self = static_cast<MockNetworkIdentityRegistrar *>(cancelable->mContextA); |
| self->mCancelledRevocations++; |
| self->mPendingUnregister = nullptr; |
| } |
| |
| bool mAcceptRegistrations = false; |
| CHIP_ERROR mRegistrationStatus = CHIP_NO_ERROR; |
| bool mRegistrationIsDeterminate = true; |
| bool mDeferRevocations = false; |
| Callback::Cancelable * mPendingUnregister = nullptr; |
| uint8_t mRegisteredIdentity[Credentials::kMaxCHIPCompactNetworkIdentityLength]; |
| size_t mRegisteredIdentityLen = 0; |
| Credentials::CertificateKeyIdStorage mUnregisteredIdentifier{}; |
| }; |
| |
| constexpr char kSsidChars[] = "test-network"; |
| constexpr char kPassphraseChars[] = "passphrase"; |
| const ByteSpan kSsid(reinterpret_cast<const uint8_t *>(kSsidChars), sizeof(kSsidChars) - 1); |
| const ByteSpan kPassphrase(reinterpret_cast<const uint8_t *>(kPassphraseChars), sizeof(kPassphraseChars) - 1); |
| |
| // Arbitrary fixed nonce, for tests that are not concerned with how the real one is generated. |
| constexpr uint8_t kPossessionNonceBytes[CommissioningParameters::kPossessionNonceLen] = { 0xa5 }; |
| const ByteSpan kPossessionNonce(kPossessionNonceBytes); |
| |
| // A Network (Client) Identity and the key needed to sign a possession nonce with it. |
| class TestNetworkIdentity |
| { |
| public: |
| TestNetworkIdentity() |
| { |
| VerifyOrDie(mKeypair.Initialize(Crypto::ECPKeyTarget::ECDSA) == CHIP_NO_ERROR); |
| MutableByteSpan identity(mIdentityBuffer); |
| VerifyOrDie(Credentials::NewChipNetworkIdentity(mKeypair, identity) == CHIP_NO_ERROR); |
| mIdentityLen = identity.size(); |
| } |
| |
| ByteSpan Identity() const { return ByteSpan(mIdentityBuffer, mIdentityLen); } |
| |
| // Produces a signature over (identity || nonce), i.e. the proof of possession the commissionee |
| // is required to return alongside its Network Client Identity. |
| CHIP_ERROR SignPossession(ByteSpan nonce, Crypto::P256ECDSASignature & signature) const |
| { |
| uint8_t tbs[sizeof(mIdentityBuffer) + CommissioningParameters::kPossessionNonceLen]; |
| VerifyOrReturnError(mIdentityLen + nonce.size() <= sizeof(tbs), CHIP_ERROR_BUFFER_TOO_SMALL); |
| memcpy(tbs, mIdentityBuffer, mIdentityLen); |
| memcpy(tbs + mIdentityLen, nonce.data(), nonce.size()); |
| return mKeypair.ECDSA_sign_msg(tbs, mIdentityLen + nonce.size(), signature); |
| } |
| |
| private: |
| Crypto::P256Keypair mKeypair; |
| uint8_t mIdentityBuffer[Credentials::kMaxCHIPCompactNetworkIdentityLength]; |
| size_t mIdentityLen = 0; |
| }; |
| |
| // --------------------------------------------------------------------------- |
| // AutoCommissioner |
| // --------------------------------------------------------------------------- |
| |
| // CommissioningStepFinished() processes the report, works out the next stage, and then performs it. |
| // This fixture has no device proxy to perform anything against, so PerformStep() bails out with |
| // this error. Reaching that point is what tells us the report was processed and the flow was |
| // routed, which is all these tests are about; a report the AutoCommissioner rejects fails earlier |
| // and with its own distinct error, which the tests covering that assert directly. |
| // |
| // Driving the steps for real is not an option here: DeviceCommissioner::PerformCommissioningStep() |
| // is not virtual, so it cannot be stubbed out, and performing it in earnest needs a live PASE |
| // session. End-to-end coverage belongs in the TestCommissioner harness in OpCredsBinding.cpp, which |
| // needs a NetworkIdentityRegistrar implementation and a PDC-capable commissionee to exist first. |
| constexpr CHIP_ERROR kStoppedAtPerformStep = CHIP_ERROR_INCORRECT_STATE; |
| |
| // The stage sequence PDC produces, and the buffers the identities are copied into on the way through. |
| // Records the completion of a commissioning attempt, to tell "finished" apart from "still waiting". |
| class CompletionRecordingPairingDelegate : public DevicePairingDelegate |
| { |
| public: |
| void OnCommissioningComplete(NodeId, CHIP_ERROR error) override |
| { |
| mCompletions++; |
| mLastError = error; |
| } |
| |
| int mCompletions = 0; |
| CHIP_ERROR mLastError = CHIP_NO_ERROR; |
| }; |
| |
| class AutoCommissionerPDCTest : public ::testing::Test |
| { |
| protected: |
| // Sets up a commissionee that only supports Wi-Fi, and applies the given parameters. |
| void Configure(const CommissioningParameters & params, bool supportsPDC) |
| { |
| ASSERT_EQ(mCommissioner.SetCommissioningParameters(params), CHIP_NO_ERROR); |
| mAccess.SetCommissioner(&mDeviceCommissioner); |
| |
| ReadCommissioningInfo & info = mAccess.GetDeviceCommissioningInfo(); |
| info.network.wifi.endpoint = kRootEndpointId; |
| info.network.wifi.supportsPerDeviceCredentials = supportsPDC; |
| info.network.thread.endpoint = kInvalidEndpointId; |
| } |
| |
| // Parameters carrying an SSID and a registrar, with no passphrase to fall back on. |
| CommissioningParameters PDCOnlyParams() |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, &mRegistrar)); |
| return params; |
| } |
| |
| // Parameters carrying an SSID, a registrar, and a passphrase (possibly empty) as a fallback. |
| CommissioningParameters PDCWithFallbackParams(ByteSpan passphrase) |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, &mRegistrar, passphrase)); |
| return params; |
| } |
| |
| // Drives the stage the AutoCommissioner would be in after a successful kPDCGetNetworkIdentity, |
| // which is what makes the network's Network Identity available. |
| void CompleteGetNetworkIdentity() |
| { |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kPDCGetNetworkIdentity; |
| report.Set<PDCNetworkIdentityInfo>(mNetworkIdentity.Identity()); |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_NO_ERROR, report), kStoppedAtPerformStep); |
| } |
| |
| // Feeds back the client identity and possession signature a PDC commissionee would return. |
| CHIP_ERROR CompleteWiFiNetworkSetup() |
| { |
| Crypto::P256ECDSASignature signature; |
| VerifyOrDie(mClientIdentity.SignPossession(PossessionNonce(), signature) == CHIP_NO_ERROR); |
| |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkSetup; |
| report.Set<PDCClientIdentityInfo>(mClientIdentity.Identity(), signature.Span()); |
| return mCommissioner.CommissioningStepFinished(CHIP_NO_ERROR, report); |
| } |
| |
| ByteSpan PossessionNonce() { return mAccess.AccessParams().GetPDCPossessionNonce().Value(); } |
| |
| void SetCompletionError(CHIP_ERROR err) |
| { |
| CompletionStatus status; |
| status.err = err; |
| mAccess.AccessParams().SetCompletionStatus(status); |
| } |
| |
| AutoCommissioner mCommissioner{}; |
| AutoCommissionerTestAccess mAccess{ &mCommissioner }; |
| DeviceCommissioner mDeviceCommissioner{}; |
| MockNetworkIdentityRegistrar mRegistrar; |
| TestNetworkIdentity mNetworkIdentity; |
| TestNetworkIdentity mClientIdentity; |
| }; |
| |
| // A registrar plus a PDC-capable commissionee takes the PDC path; once the Network Identity has |
| // been obtained the flow rejoins kWiFiNetworkSetup, with the client identity registered before the |
| // network is enabled. |
| TEST_F(AutoCommissionerPDCTest, StageGraphWithRegistrarAndPDCSupport) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kPDCGetNetworkIdentity); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| |
| CompleteGetNetworkIdentity(); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| |
| // Now that we have the identity, the fetch is not repeated. |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPDCGetNetworkIdentity, err), kWiFiNetworkSetup); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kWiFiNetworkSetup, err), kPDCRegisterClientIdentity); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPDCRegisterClientIdentity, err), kFailsafeBeforeWiFiEnable); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // Supplying the registrar late, in response to kNeedsNetworkCreds, must converge on the same graph. |
| TEST_F(AutoCommissionerPDCTest, StageGraphConvergesForLateSuppliedRegistrar) |
| { |
| // First pass: no credentials at all, so the commissioner asks for them. |
| Configure(CommissioningParameters(), /* supportsPDC = */ true); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kICDRegistration, err), kRequestWiFiCredentials); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kRequestWiFiCredentials, err), kNeedsNetworkCreds); |
| |
| // The application answers with an SSID and a registrar, exactly as it would up front. |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kNeedsNetworkCreds, err), kPDCGetNetworkIdentity); |
| |
| CompleteGetNetworkIdentity(); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPDCGetNetworkIdentity, err), kWiFiNetworkSetup); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kWiFiNetworkSetup, err), kPDCRegisterClientIdentity); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // A registrar is not usable against a commissionee without the PDC feature; a passphrase supplied |
| // alongside it is the fallback. |
| TEST_F(AutoCommissionerPDCTest, PassphraseIsUsedWhenCommissioneeLacksPDC) |
| { |
| Configure(PDCWithFallbackParams(kPassphrase), /* supportsPDC = */ false); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| // No Network Identity, so no client identity to register either. |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kWiFiNetworkSetup, err), kFailsafeBeforeWiFiEnable); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // An empty passphrase is a valid configuration -- an open network -- and must not be mistaken for |
| // "no passphrase supplied", which is what a PDC-only registrar means. |
| TEST_F(AutoCommissionerPDCTest, OpenNetworkIsUsedAsFallbackWhenCommissioneeLacksPDC) |
| { |
| Configure(PDCWithFallbackParams(ByteSpan()), /* supportsPDC = */ false); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| ASSERT_TRUE(mAccess.AccessParams().GetWiFiCredentials().HasValue()); |
| EXPECT_TRUE(mAccess.AccessParams().GetWiFiCredentials().Value().hasCredentials); |
| EXPECT_TRUE(mAccess.AccessParams().GetWiFiCredentials().Value().credentials.empty()); |
| } |
| |
| // An open network without any registrar involved must behave exactly as it did before PDC existed. |
| TEST_F(AutoCommissionerPDCTest, OpenNetworkWithoutRegistrarIsConfigured) |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, ByteSpan())); |
| Configure(params, /* supportsPDC = */ true); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kWiFiNetworkSetup, err), kFailsafeBeforeWiFiEnable); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // "PDC and nothing else" against a commissionee that cannot do PDC is unsatisfiable. Rather than |
| // deciding that during stage selection, we run kWiFiNetworkSetup anyway and let the commissioner |
| // reject it, which puts us on the ordinary step-failure path. With no other network to try, the |
| // attempt ends and the error is reported rather than swallowed. |
| TEST_F(AutoCommissionerPDCTest, PDCOnlyFailsWhenCommissioneeLacksPDC) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ false); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| // No Network Identity was obtained, so kWiFiNetworkSetup has nothing to configure the |
| // commissionee with; see DeviceCommissionerPDCTest.RejectsPDCOnlyCredentialsWithoutIdentity. |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkSetup; |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_INVALID_ARGUMENT, report), kStoppedAtPerformStep); |
| EXPECT_FALSE(mAccess.TryingSecondaryNetwork()); |
| EXPECT_EQ(mAccess.AccessParams().GetCompletionStatus().err, CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| // ...but if the application also supplied a Thread dataset, then "Wi-Fi via PDC or Thread" is what |
| // it asked for, so the rejected Wi-Fi stage fails over to the secondary network. |
| TEST_F(AutoCommissionerPDCTest, PDCOnlyFallsBackToSecondaryNetworkWhenCommissioneeLacksPDC) |
| { |
| CommissioningParameters params = PDCOnlyParams(); |
| params.SetThreadOperationalDataset(ByteSpan()); |
| params.SetSupportsConcurrentConnection(true); |
| ASSERT_EQ(mCommissioner.SetCommissioningParameters(params), CHIP_NO_ERROR); |
| mAccess.SetCommissioner(&mDeviceCommissioner); |
| |
| ReadCommissioningInfo & info = mAccess.GetDeviceCommissioningInfo(); |
| info.network.wifi.endpoint = kRootEndpointId; |
| info.network.wifi.supportsPerDeviceCredentials = false; |
| info.network.thread.endpoint = 1; |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| ASSERT_TRUE(mAccess.TryingPrimaryNetwork()); |
| |
| // The commissioner rejects the stage, and that is treated as any other network failure. |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkSetup; |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_INVALID_ARGUMENT, report), kStoppedAtPerformStep); |
| EXPECT_TRUE(mAccess.TryingSecondaryNetwork()); |
| |
| // We never wrote a configuration, so there is nothing to remove on the way to Thread. |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPrimaryOperationalNetworkFailed, err), kThreadNetworkSetup); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // A plain passphrase must produce exactly the graph it produced before PDC existed, whether or not |
| // the commissionee happens to support PDC. |
| TEST_F(AutoCommissionerPDCTest, PassphraseOnlyIsUnaffectedByPDCSupport) |
| { |
| for (bool supportsPDC : { false, true }) |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, kPassphrase)); |
| Configure(params, supportsPDC); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kWiFiNetworkSetup); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kWiFiNetworkSetup, err), kFailsafeBeforeWiFiEnable); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| } |
| |
| // A fresh possession nonce is generated for each PDC exchange, and an application-supplied one is |
| // honoured, as for the attestation and CSR nonces. |
| TEST_F(AutoCommissionerPDCTest, PossessionNonceIsGeneratedUnlessSupplied) |
| { |
| // Nothing is generated until we know we are actually going to configure a commissionee for PDC. |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCPossessionNonce().HasValue()); |
| |
| CompleteGetNetworkIdentity(); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCPossessionNonce().HasValue()); |
| ASSERT_EQ(PossessionNonce().size(), CommissioningParameters::kPossessionNonceLen); |
| |
| // Resetting the PDC state discards the nonce, so the next exchange uses a different challenge. |
| uint8_t previousNonce[CommissioningParameters::kPossessionNonceLen]; |
| memcpy(previousNonce, PossessionNonce().data(), sizeof(previousNonce)); |
| mAccess.ClearPDCParameters(); |
| CompleteGetNetworkIdentity(); |
| EXPECT_FALSE(PossessionNonce().data_equal(ByteSpan(previousNonce))); |
| |
| // An application-supplied nonce is copied up front and used as-is. |
| uint8_t suppliedNonce[CommissioningParameters::kPossessionNonceLen]; |
| memset(suppliedNonce, 0xA5, sizeof(suppliedNonce)); |
| CommissioningParameters params = PDCOnlyParams(); |
| params.SetPDCPossessionNonce(ByteSpan(suppliedNonce)); |
| Configure(params, /* supportsPDC = */ true); |
| EXPECT_TRUE(PossessionNonce().data_equal(ByteSpan(suppliedNonce))); |
| CompleteGetNetworkIdentity(); |
| EXPECT_TRUE(PossessionNonce().data_equal(ByteSpan(suppliedNonce))); |
| |
| // A wrongly sized nonce is rejected rather than silently truncated / extended |
| uint8_t shortNonce[CommissioningParameters::kPossessionNonceLen - 1] = {}; |
| CommissioningParameters badParams = PDCOnlyParams(); |
| badParams.SetPDCPossessionNonce(ByteSpan(shortNonce)); |
| EXPECT_EQ(mCommissioner.SetCommissioningParameters(badParams), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| // The AutoCommissioner takes its own copy of the client identity and signature, because the report's |
| // spans point into the response message buffer. Verification is the DeviceCommissioner's job. |
| TEST_F(AutoCommissionerPDCTest, CapturesClientIdentityAndPossessionSignature) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| CompleteGetNetworkIdentity(); |
| |
| EXPECT_EQ(CompleteWiFiNetworkSetup(), kStoppedAtPerformStep); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCClientIdentity().HasValue()); |
| EXPECT_TRUE(mAccess.AccessParams().GetPDCClientIdentity().Value().data_equal(mClientIdentity.Identity())); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCPossessionSignature().HasValue()); |
| EXPECT_EQ(mAccess.AccessParams().GetPDCPossessionSignature().Value().size(), CommissioningParameters::kPossessionSignatureLen); |
| } |
| |
| // ...and this is the one test that puts both halves together, because the handover is the only place |
| // the possession nonce the AutoCommissioner generated meets the verification the DeviceCommissioner |
| // does with it: neither fixture on its own can tell that those two agree. |
| // |
| // Only the stages that send a command to the commissionee are stood in for, by reporting them |
| // complete as the commissionee would. Everything in between really runs, in the real |
| // DeviceCommissioner, off the AutoCommissioner's own stage decisions -- including the rollback, |
| // which is what a DeviceCommissioner stage entails rather than something a test can ask for. Note it |
| // is kCleanup that entails it here, not kRemoveWiFiNetworkConfig: that stage sends RemoveNetwork, so |
| // performing it needs a session this fixture has no way to produce (see kStoppedAtPerformStep), and |
| // DeviceCommissionerRevocationTest drives its response handler directly instead. |
| TEST_F(AutoCommissionerPDCTest, ClientIdentityIsRegisteredAndRolledBackOnFailure) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| mRegistrar.AcceptRegistrations(); |
| CompleteGetNetworkIdentity(); |
| |
| // Give the AutoCommissioner a commissionee to hang the stages off, so that the steps it picks |
| // reach the DeviceCommissioner rather than being lost in PerformStep(). |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| // Reporting kWiFiNetworkSetup complete drives all of the following in one synchronous chain: |
| // kPDCRegisterClientIdentity registers the identity with the network, having checked the |
| // commissionee's proof of possession against our own nonce |
| // kFailsafeBeforeWiFiEnable fails, because this commissioner has no commissionee device of its |
| // own to extend the fail-safe on. Any mid-flow failure would do; this |
| // is the one the fixture can produce without sending a command. |
| // kCleanup rolls the registration back, the attempt having failed |
| ASSERT_EQ(CompleteWiFiNetworkSetup(), CHIP_NO_ERROR); |
| const CompletionStatus & status = mAccess.AccessParams().GetCompletionStatus(); |
| ASSERT_EQ(status.err, CHIP_ERROR_INCORRECT_STATE) << "the attempt failed somewhere unexpected"; |
| ASSERT_TRUE(status.failedStage.HasValue()); |
| ASSERT_EQ(status.failedStage.Value(), kFailsafeBeforeWiFiEnable); |
| |
| // The identity the network was given is the one the commissionee returned, so the possession |
| // signature was accepted over the nonce the AutoCommissioner generated and stored. |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.RegisteredIdentity().data_equal(mClientIdentity.Identity())); |
| |
| // ...and it was revoked again by its key identifier, which only the DeviceCommissioner computed. |
| Credentials::CertificateKeyIdStorage clientIdentifier{}; |
| ASSERT_EQ(Credentials::ExtractIdentifierFromChipNetworkIdentity(mClientIdentity.Identity(), |
| Credentials::MutableCertificateKeyId(clientIdentifier)), |
| CHIP_NO_ERROR); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ByteSpan(clientIdentifier))); |
| EXPECT_EQ(DeviceCommissionerTestAccess(&mDeviceCommissioner).GetNetworkClientRegistrar(), nullptr); |
| } |
| |
| // A registration that fails indeterminately is revoked from the cleanup of the attempt it just |
| // failed, which for a registrar that completes synchronously means the revocation starts while its |
| // own RegisterClient() call is still on the stack. Registrars are told to expect that; this is the |
| // flow that produces it. |
| TEST_F(AutoCommissionerPDCTest, AnIndeterminateRegistrationFailureIsRolledBackFromCleanup) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| mRegistrar.FailRegistrations(CHIP_ERROR_TIMEOUT, /* determinate = */ false); |
| CompleteGetNetworkIdentity(); |
| |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| // kPDCRegisterClientIdentity is the stage that fails this time, rather than the one after it. |
| ASSERT_EQ(CompleteWiFiNetworkSetup(), CHIP_NO_ERROR); |
| const CompletionStatus & status = mAccess.AccessParams().GetCompletionStatus(); |
| EXPECT_EQ(status.err, CHIP_ERROR_TIMEOUT); |
| ASSERT_TRUE(status.failedStage.HasValue()); |
| EXPECT_EQ(status.failedStage.Value(), kPDCRegisterClientIdentity); |
| |
| Credentials::CertificateKeyIdStorage clientIdentifier{}; |
| ASSERT_EQ(Credentials::ExtractIdentifierFromChipNetworkIdentity(mClientIdentity.Identity(), |
| Credentials::MutableCertificateKeyId(clientIdentifier)), |
| CHIP_NO_ERROR); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ByteSpan(clientIdentifier))); |
| EXPECT_EQ(DeviceCommissionerTestAccess(&mDeviceCommissioner).GetNetworkClientRegistrar(), nullptr); |
| } |
| |
| // Whereas a registrar that knows nothing was granted spares the failing attempt the round trip. |
| TEST_F(AutoCommissionerPDCTest, ADeterminateRegistrationFailureIsNotRolledBack) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| mRegistrar.FailRegistrations(CHIP_ERROR_TIMEOUT, /* determinate = */ true); |
| CompleteGetNetworkIdentity(); |
| |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| ASSERT_EQ(CompleteWiFiNetworkSetup(), CHIP_NO_ERROR); |
| EXPECT_EQ(mAccess.AccessParams().GetCompletionStatus().err, CHIP_ERROR_TIMEOUT); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| } |
| |
| // The same failure, but with a registrar that does not revoke synchronously: cleanup holds the |
| // attempt open until the revocation lands, so that a retry (possibly against a different network) |
| // finds the registrar idle rather than colliding with the identity being abandoned. |
| TEST_F(AutoCommissionerPDCTest, CleanupWaitsForAnOutstandingRevocation) |
| { |
| CompletionRecordingPairingDelegate pairingDelegate; |
| mDeviceCommissioner.RegisterPairingDelegate(&pairingDelegate); |
| |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| mRegistrar.AcceptRegistrations(); |
| mRegistrar.DeferRevocations(); |
| CompleteGetNetworkIdentity(); |
| |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| // Fails at kFailsafeBeforeWiFiEnable and reaches kCleanup, exactly as the test above. |
| ASSERT_EQ(CompleteWiFiNetworkSetup(), CHIP_NO_ERROR); |
| ASSERT_EQ(mRegistrar.mUnregisterCalls, 1); |
| |
| // The registration is given up immediately, but the attempt is not finished until the network |
| // has actually been told, so the application cannot retry into a half-revoked state. |
| ASSERT_TRUE(mRegistrar.HasPendingRevocation()); |
| EXPECT_EQ(DeviceCommissionerTestAccess(&mDeviceCommissioner).GetNetworkClientRegistrar(), nullptr); |
| EXPECT_EQ(pairingDelegate.mCompletions, 0) << "completed before the revocation did"; |
| |
| mRegistrar.CompleteRevocation(); |
| EXPECT_EQ(pairingDelegate.mCompletions, 1); |
| EXPECT_EQ(pairingDelegate.mLastError, CHIP_ERROR_INCORRECT_STATE) << "the original failure was lost"; |
| } |
| |
| // Why the PDC parameters are left alone when the Wi-Fi configuration is removed: that stage is only |
| // reachable from kPrimaryOperationalNetworkFailed with Wi-Fi on the root endpoint, so by the time it |
| // completes we are committed to the secondary network and the next selection is Thread, which reads |
| // no PDC parameters. They are stale but inert, and CleanupCommissioning() clears them at the end of |
| // the attempt. (Revoking the client identity registration is the DeviceCommissioner's job, off the |
| // back of the RemoveNetwork response.) |
| TEST_F(AutoCommissionerPDCTest, RemovingWiFiConfigProceedsToThreadNetworkSetup) |
| { |
| CommissioningParameters params = PDCOnlyParams(); |
| params.SetThreadOperationalDataset(ByteSpan()); |
| params.SetSupportsConcurrentConnection(true); |
| ASSERT_EQ(mCommissioner.SetCommissioningParameters(params), CHIP_NO_ERROR); |
| mAccess.SetCommissioner(&mDeviceCommissioner); |
| |
| ReadCommissioningInfo & info = mAccess.GetDeviceCommissioningInfo(); |
| info.network.wifi.endpoint = kRootEndpointId; |
| info.network.wifi.supportsPerDeviceCredentials = true; |
| info.network.thread.endpoint = 1; |
| |
| // Configure the commissionee for PDC on the primary (Wi-Fi) network. |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| EXPECT_EQ(mAccess.GetNextCommissioningStageNetworkSetup(kNeedsNetworkCreds, err), kPDCGetNetworkIdentity); |
| ASSERT_TRUE(mAccess.TryingPrimaryNetwork()); |
| CompleteGetNetworkIdentity(); |
| EXPECT_EQ(CompleteWiFiNetworkSetup(), kStoppedAtPerformStep); |
| |
| // ConnectNetwork failed outright, so the primary network is given up on. Note the absence of a |
| // NetworkCommissioningStatusInfo report: this is the failover path, not the retry path. |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkEnable; |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_INTERNAL, report), kStoppedAtPerformStep); |
| ASSERT_TRUE(mAccess.TryingSecondaryNetwork()); |
| |
| // That failover is the only way to reach kRemoveWiFiNetworkConfig, and from there the flow can |
| // only go to Thread; the Wi-Fi branch of the network selection is never entered again. |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPrimaryOperationalNetworkFailed, err), kRemoveWiFiNetworkConfig); |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kRemoveWiFiNetworkConfig, err), kThreadNetworkSetup); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // A failed network configuration that the application is allowed to retry walks the flow back to |
| // kScanNetworks, and this is the one case where the PDC parameters have to go: the retry re-enters |
| // the Wi-Fi branch, and reusing the Network Identity (never mind the possession nonce) would mean |
| // challenging the commissionee with a nonce it has already answered. |
| TEST_F(AutoCommissionerPDCTest, RetryingAfterNetworkEnableFailureClearsPDCParameters) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| |
| // The retry is gated on IsScanNeeded(), which cannot be arranged through the application-level |
| // parameters: SetWiFiCredentials() clears the scan flag, and AutoCommissioner:: |
| // SetCommissioningParameters() re-applies the credentials, so it has to be set on the |
| // AutoCommissioner's own copy afterwards. Scanning is the wrong condition in the first place -- |
| // see the TODO above the IsScanNeeded() check in CommissioningStepFinished() -- and this |
| // workaround goes away with the follow-up change to the restart flow. |
| mAccess.AccessParams().SetAttemptWiFiNetworkScan(true); |
| ASSERT_TRUE(mAccess.IsScanNeeded()); |
| |
| CompleteGetNetworkIdentity(); |
| EXPECT_EQ(CompleteWiFiNetworkSetup(), kStoppedAtPerformStep); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| |
| // Give the AutoCommissioner a commissionee to talk to, so that the stage it restarts at is |
| // actually driven rather than lost in PerformStep(). |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| // ConnectNetwork failed: the commissionee could not find the network we configured. |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkEnable; |
| report.Set<NetworkCommissioningStatusInfo>( |
| app::Clusters::NetworkCommissioning::NetworkCommissioningStatusEnum::kNetworkNotFound, CharSpan()); |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_INTERNAL, report), CHIP_NO_ERROR); |
| |
| // Walked back to kScanNetworks, whose successor asks the application for credentials again. |
| EXPECT_EQ(mDeviceCommissioner.GetCommissioningStage(), kNeedsNetworkCreds); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCPossessionNonce().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCClientIdentity().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCPossessionSignature().HasValue()); |
| } |
| |
| // What that walk-back does *not* do is give up the Network Client Identity registered for the |
| // configuration it is retrying: the registration is only rolled back on RemoveNetwork or at the end |
| // of the attempt, neither of which the walk-back goes through. So the retry runs into the |
| // single-outstanding-registration check in kPDCRegisterClientIdentity and fails the attempt, which is |
| // what finally revokes the stale registration -- see the second TODO above the IsScanNeeded() check |
| // in CommissioningStepFinished(). This test pins that behaviour rather than endorsing it; a retry |
| // that removed the old configuration first would not get here at all. |
| TEST_F(AutoCommissionerPDCTest, RetryingWithAnOutstandingRegistrationFailsTheAttempt) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| mAccess.AccessParams().SetAttemptWiFiNetworkScan(true); // see the test above |
| ASSERT_TRUE(mAccess.IsScanNeeded()); |
| |
| CompleteGetNetworkIdentity(); |
| EXPECT_EQ(CompleteWiFiNetworkSetup(), kStoppedAtPerformStep); |
| |
| // The registration this fixture cannot perform (kWiFiNetworkSetup was never sent, so neither was the |
| // step after it) would have left the commissioner holding exactly this. |
| Credentials::CertificateKeyIdStorage clientIdentifier{}; |
| ASSERT_EQ(Credentials::ExtractIdentifierFromChipNetworkIdentity(mClientIdentity.Identity(), |
| Credentials::MutableCertificateKeyId(clientIdentifier)), |
| CHIP_NO_ERROR); |
| DeviceCommissionerTestAccess deviceCommissionerAccess{ &mDeviceCommissioner }; |
| deviceCommissionerAccess.SetNetworkClientRegistration(&mRegistrar, ByteSpan(clientIdentifier)); |
| |
| CommissioneeDeviceProxy commissionee; |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| |
| // ConnectNetwork failed: the commissionee could not find the network we configured. |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kWiFiNetworkEnable; |
| report.Set<NetworkCommissioningStatusInfo>( |
| app::Clusters::NetworkCommissioning::NetworkCommissioningStatusEnum::kNetworkNotFound, CharSpan()); |
| ASSERT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_INTERNAL, report), CHIP_NO_ERROR); |
| ASSERT_EQ(mDeviceCommissioner.GetCommissioningStage(), kNeedsNetworkCreds); |
| |
| // Walking back is not an end to the attempt, so we are still on the hook for the registration. |
| EXPECT_EQ(deviceCommissionerAccess.GetNetworkClientRegistrar(), &mRegistrar); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| |
| // The application answers with credentials again and the flow runs up to the second registration, |
| // which the commissioner refuses. The registrar is left to fail an unexpected RegisterClient(). |
| mAccess.SetCommissioneeDeviceProxy(nullptr); // don't perform kWiFiNetworkSetup, we cannot send it |
| CompleteGetNetworkIdentity(); |
| mAccess.SetCommissioneeDeviceProxy(&commissionee); |
| ASSERT_EQ(CompleteWiFiNetworkSetup(), CHIP_NO_ERROR); |
| |
| const CompletionStatus & status = mAccess.AccessParams().GetCompletionStatus(); |
| EXPECT_EQ(status.err, CHIP_ERROR_INCORRECT_STATE); |
| ASSERT_TRUE(status.failedStage.HasValue()); |
| EXPECT_EQ(status.failedStage.Value(), kPDCRegisterClientIdentity); |
| |
| // Cleanup is what eventually revokes the identity the retry tripped over. |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ByteSpan(clientIdentifier))); |
| EXPECT_EQ(deviceCommissionerAccess.GetNetworkClientRegistrar(), nullptr); |
| } |
| |
| // Cleanup is the backstop, and the only reset a second commissioning attempt is guaranteed to get: |
| // DeviceCommissioner::Commission(NodeId) reuses the parameters from the previous attempt without |
| // going through SetCommissioningParameters(), so ClearExternalBufferDependentValues() does not |
| // necessarily run in between. Left behind, the Network Identity would be written to a commissionee |
| // that may be joining a different network, and -- worse -- the possession nonce would be reused as a |
| // challenge against a different commissionee. |
| TEST_F(AutoCommissionerPDCTest, CleanupClearsPDCParameters) |
| { |
| Configure(PDCOnlyParams(), /* supportsPDC = */ true); |
| CompleteGetNetworkIdentity(); |
| EXPECT_EQ(CompleteWiFiNetworkSetup(), kStoppedAtPerformStep); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| ASSERT_TRUE(mAccess.AccessParams().GetPDCPossessionNonce().HasValue()); |
| |
| mAccess.CleanupCommissioning(); |
| |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCNetworkIdentity().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCPossessionNonce().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCClientIdentity().HasValue()); |
| EXPECT_FALSE(mAccess.AccessParams().GetPDCPossessionSignature().HasValue()); |
| } |
| |
| // Cancellation and errors during either of the new stages must fall through to cleanup rather than |
| // advancing the flow. |
| TEST_F(AutoCommissionerPDCTest, NewStagesHonourStopAndError) |
| { |
| for (CommissioningStage stage : { kPDCGetNetworkIdentity, kPDCRegisterClientIdentity }) |
| { |
| { |
| AutoCommissioner commissioner; |
| AutoCommissionerTestAccess access{ &commissioner }; |
| CHIP_ERROR err = CHIP_ERROR_INTERNAL; |
| EXPECT_EQ(access.AccessGetNextCommissioningStageInternal(stage, err), kCleanup); |
| } |
| { |
| AutoCommissioner commissioner; |
| AutoCommissionerTestAccess access{ &commissioner }; |
| commissioner.StopCommissioning(); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| EXPECT_EQ(access.AccessGetNextCommissioningStageInternal(stage, err), kCleanup); |
| } |
| } |
| } |
| |
| // CleanupCommissioning() may defer completing an attempt until an outstanding revocation lands, and |
| // a StopPairing() in that window discards the continuation that would have completed it. What keeps |
| // the attempt from being stranded is that StopPairing() follows up with a cancelled stage completion |
| // that routes back into kCleanup -- if a failed kCleanup ever mapped to kError instead, the re-entry |
| // would be suppressed and no completion callback would ever be delivered. |
| TEST_F(AutoCommissionerPDCTest, FailedCleanupRoutesBackToCleanup) |
| { |
| for (CHIP_ERROR err : { CHIP_ERROR_CANCELLED, CHIP_ERROR_INTERNAL }) |
| { |
| AutoCommissioner commissioner; |
| AutoCommissionerTestAccess access{ &commissioner }; |
| EXPECT_EQ(access.AccessGetNextCommissioningStageInternal(kCleanup, err), kCleanup); |
| } |
| } |
| |
| // Being unable to reach the network's identity provider is a credentials-class failure, so the |
| // commissioner switches the attempt over to the secondary network rather than giving up. |
| TEST_F(AutoCommissionerPDCTest, GetNetworkIdentityFailureSwitchesToSecondaryNetwork) |
| { |
| CommissioningParameters params = PDCOnlyParams(); |
| params.SetThreadOperationalDataset(ByteSpan()); |
| params.SetSupportsConcurrentConnection(true); |
| ASSERT_EQ(mCommissioner.SetCommissioningParameters(params), CHIP_NO_ERROR); |
| mAccess.SetCommissioner(&mDeviceCommissioner); |
| |
| ReadCommissioningInfo & info = mAccess.GetDeviceCommissioningInfo(); |
| info.network.wifi.endpoint = kRootEndpointId; |
| info.network.wifi.supportsPerDeviceCredentials = true; |
| info.network.thread.endpoint = 1; |
| mAccess.TryPrimaryNetwork(); |
| |
| CommissioningDelegate::CommissioningReport report; |
| report.stageCompleted = kPDCGetNetworkIdentity; |
| // The error is swallowed and the flow is redirected to the secondary network. |
| EXPECT_EQ(mCommissioner.CommissioningStepFinished(CHIP_ERROR_TIMEOUT, report), kStoppedAtPerformStep); |
| EXPECT_TRUE(mAccess.TryingSecondaryNetwork()); |
| |
| // We never got as far as writing a Wi-Fi configuration, so there is nothing on the commissionee |
| // to remove and the flow goes straight to the secondary network. |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| EXPECT_EQ(mAccess.AccessGetNextCommissioningStageInternal(kPrimaryOperationalNetworkFailed, err), kThreadNetworkSetup); |
| EXPECT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // DeviceCommissioner |
| // --------------------------------------------------------------------------- |
| |
| // PerformCommissioningStep() hangs the stage off a device proxy, but kPDCRegisterClientIdentity only |
| // talks to the registrar, so nothing here is ever reached. |
| class StubDeviceProxy : public DeviceProxy |
| { |
| public: |
| void Disconnect() override {} |
| NodeId GetDeviceId() const override { return 0x1234; } |
| Messaging::ExchangeManager * GetExchangeManager() const override { return nullptr; } |
| Optional<SessionHandle> GetSecureSession() const override { return NullOptional; } |
| |
| private: |
| bool IsSecureConnected() const override { return false; } |
| }; |
| |
| // Captures the outcome of a stage driven directly via PerformCommissioningStep(), and stops there: |
| // returning an error would send the commissioner into cleanup instead. |
| class RecordingCommissioningDelegate : public CommissioningDelegate |
| { |
| public: |
| // Stages the parameters a step is performed with, or read against. Spans are the caller's to keep |
| // alive, which the fixtures below do. |
| CHIP_ERROR SetCommissioningParameters(const CommissioningParameters & params) override |
| { |
| mParams = params; |
| return CHIP_NO_ERROR; |
| } |
| const CommissioningParameters & GetCommissioningParameters() const override { return mParams; } |
| void SetOperationalCredentialsDelegate(OperationalCredentialsDelegate *) override {} |
| CHIP_ERROR StartCommissioning(DeviceCommissioner *, CommissioneeDeviceProxy *) override { return CHIP_ERROR_NOT_IMPLEMENTED; } |
| |
| CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningReport report) override |
| { |
| mCompletions++; |
| mLastError = err; |
| mLastStage = report.stageCompleted; |
| if (report.Is<PDCClientIdentityInfo>()) |
| { |
| mLastClientIdentityInfo.SetValue(report.Get<PDCClientIdentityInfo>()); |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| int mCompletions = 0; |
| CHIP_ERROR mLastError = CHIP_ERROR_INTERNAL; |
| CommissioningStage mLastStage = kError; |
| Optional<PDCClientIdentityInfo> mLastClientIdentityInfo; |
| |
| private: |
| CommissioningParameters mParams; |
| }; |
| |
| // Verifying the commissionee's proof of possession, and the rollback obligation the commissioner |
| // takes on when it registers a Network Client Identity. None of this involves the AutoCommissioner, |
| // so a fixed possession nonce stands in for the generated one. |
| class DeviceCommissionerPDCTest : public ::testing::Test |
| { |
| protected: |
| // Puts the commissioner into the state it would be in after a successful kPDCRegisterClientIdentity: |
| // holding an obligation to roll mClientIdentity's registration back. |
| void SimulateSuccessfulRegistration() { mAccess.SetNetworkClientRegistration(&mRegistrar, ClientIdentifier()); } |
| |
| // Parameters as they stand when kPDCRegisterClientIdentity runs: the registrar to register with, plus |
| // the client identity and proof of possession the commissionee returned from kWiFiNetworkSetup. |
| CommissioningParameters RegisterClientIdentityParams(const TestNetworkIdentity & clientIdentity) |
| { |
| VerifyOrDie(clientIdentity.SignPossession(kPossessionNonce, mPossessionSignature) == CHIP_NO_ERROR); |
| |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, &mRegistrar)); |
| params.SetPDCClientIdentity(clientIdentity.Identity()); |
| params.SetPDCPossessionNonce(kPossessionNonce); |
| params.SetPDCPossessionSignature(mPossessionSignature.Span()); |
| return params; |
| } |
| |
| // Runs the stage that registers a client identity with the network. The registrar completes |
| // synchronously, so the stage has finished by the time this returns. |
| void PerformRegisterClientIdentity(CommissioningParameters & params) |
| { |
| mRegistrar.AcceptRegistrations(); // a no-op for a test that called FailRegistrations() |
| mCommissioner.PerformCommissioningStep(&mDeviceProxy, kPDCRegisterClientIdentity, params, &mDelegate, kRootEndpointId, |
| NullOptional); |
| } |
| |
| // The key identifier of mClientIdentity, i.e. what a rollback of its registration must name. |
| ByteSpan ClientIdentifier() |
| { |
| VerifyOrDie(Credentials::ExtractIdentifierFromChipNetworkIdentity( |
| mClientIdentity.Identity(), Credentials::MutableCertificateKeyId(mClientIdentifier)) == CHIP_NO_ERROR); |
| return ByteSpan(mClientIdentifier); |
| } |
| |
| DeviceCommissioner mCommissioner{}; |
| DeviceCommissionerTestAccess mAccess{ &mCommissioner }; |
| MockNetworkIdentityRegistrar mRegistrar; |
| StubDeviceProxy mDeviceProxy; |
| RecordingCommissioningDelegate mDelegate; |
| TestNetworkIdentity mClientIdentity; |
| TestNetworkIdentity mOtherIdentity; |
| Credentials::CertificateKeyIdStorage mClientIdentifier{}; |
| Crypto::P256ECDSASignature mPossessionSignature; |
| }; |
| |
| // The AutoCommissioner deliberately runs kWiFiNetworkSetup with PDC-only credentials even when the |
| // commissionee cannot do PDC, and relies on being rejected here to reach the ordinary step-failure |
| // path. Nothing is sent to the commissionee. |
| TEST_F(DeviceCommissionerPDCTest, RejectsPDCOnlyCredentialsWithoutIdentity) |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, &mRegistrar)); |
| ASSERT_FALSE(params.GetPDCNetworkIdentity().HasValue()); |
| |
| mCommissioner.PerformCommissioningStep(&mDeviceProxy, kWiFiNetworkSetup, params, &mDelegate, kRootEndpointId, NullOptional); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| // The commissionee's proof of possession is checked before its identity is registered with the |
| // network, and validating the identity yields the key identifier a rollback needs. |
| TEST_F(DeviceCommissionerPDCTest, AcceptsValidPossessionSignature) |
| { |
| Crypto::P256ECDSASignature signature; |
| ASSERT_EQ(mClientIdentity.SignPossession(kPossessionNonce, signature), CHIP_NO_ERROR); |
| |
| Credentials::CertificateKeyIdStorage identifier{}; |
| EXPECT_EQ(DeviceCommissionerTestAccess::VerifyNetworkClientIdentity( |
| mClientIdentity.Identity(), signature.Span(), kPossessionNonce, Credentials::MutableCertificateKeyId(identifier)), |
| CHIP_NO_ERROR); |
| EXPECT_TRUE(ByteSpan(identifier).data_equal(ClientIdentifier())); |
| } |
| |
| TEST_F(DeviceCommissionerPDCTest, RejectsPossessionSignatureOverWrongNonce) |
| { |
| uint8_t wrongNonce[CommissioningParameters::kPossessionNonceLen] = {}; |
| Crypto::P256ECDSASignature signature; |
| ASSERT_EQ(mClientIdentity.SignPossession(ByteSpan(wrongNonce), signature), CHIP_NO_ERROR); |
| |
| Credentials::CertificateKeyIdStorage identifier{}; |
| EXPECT_EQ(DeviceCommissionerTestAccess::VerifyNetworkClientIdentity( |
| mClientIdentity.Identity(), signature.Span(), kPossessionNonce, Credentials::MutableCertificateKeyId(identifier)), |
| CHIP_ERROR_INVALID_SIGNATURE); |
| } |
| |
| // A signature made by a different key must not pass, even though it is well formed. |
| TEST_F(DeviceCommissionerPDCTest, RejectsPossessionSignatureFromAnotherKey) |
| { |
| Crypto::P256ECDSASignature signature; |
| ASSERT_EQ(mOtherIdentity.SignPossession(kPossessionNonce, signature), CHIP_NO_ERROR); |
| |
| Credentials::CertificateKeyIdStorage identifier{}; |
| EXPECT_EQ(DeviceCommissionerTestAccess::VerifyNetworkClientIdentity( |
| mClientIdentity.Identity(), signature.Span(), kPossessionNonce, Credentials::MutableCertificateKeyId(identifier)), |
| CHIP_ERROR_INVALID_SIGNATURE); |
| } |
| |
| TEST_F(DeviceCommissionerPDCTest, RejectsMalformedClientIdentity) |
| { |
| uint8_t garbage[64] = {}; |
| Crypto::P256ECDSASignature signature; |
| ASSERT_EQ(mClientIdentity.SignPossession(kPossessionNonce, signature), CHIP_NO_ERROR); |
| |
| Credentials::CertificateKeyIdStorage identifier{}; |
| EXPECT_NE(DeviceCommissionerTestAccess::VerifyNetworkClientIdentity(ByteSpan(garbage), signature.Span(), kPossessionNonce, |
| Credentials::MutableCertificateKeyId(identifier)), |
| CHIP_NO_ERROR); |
| } |
| |
| // Rolling back revokes the identity we registered, naming it by the key identifier we recorded when |
| // we took the obligation on. |
| TEST_F(DeviceCommissionerPDCTest, RollsBackRegistration) |
| { |
| SimulateSuccessfulRegistration(); |
| |
| mAccess.RollBackNetworkClientIdentity(); |
| |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ClientIdentifier())); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), nullptr); |
| } |
| |
| // Rolling back is idempotent: the first one discharges the obligation, so a second is a no-op. Not |
| // hypothetical -- a mid-flight rollback is always followed by the one in cleanup. |
| TEST_F(DeviceCommissionerPDCTest, RollbackIsIdempotent) |
| { |
| SimulateSuccessfulRegistration(); |
| |
| mAccess.RollBackNetworkClientIdentity(); |
| mAccess.RollBackNetworkClientIdentity(); |
| |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| } |
| |
| // Nothing to roll back if we never got as far as registering an identity. |
| TEST_F(DeviceCommissionerPDCTest, RollingBackWithoutARegistrationIsANoOp) |
| { |
| mAccess.RollBackNetworkClientIdentity(); |
| |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| } |
| |
| // Registering an identity while the commissioner owns the rollback arms the obligation, so a later |
| // failure revokes exactly the identity that was registered. |
| TEST_F(DeviceCommissionerPDCTest, RegisteringAnIdentityArmsTheRollbackByDefault) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(mClientIdentity); |
| ASSERT_TRUE(params.GetManagePDCClientIdentityRollback()); |
| PerformRegisterClientIdentity(params); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kPDCRegisterClientIdentity); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_NO_ERROR); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.RegisteredIdentity().data_equal(mClientIdentity.Identity())); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), &mRegistrar); |
| |
| mAccess.RollBackNetworkClientIdentity(); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ClientIdentifier())); |
| } |
| |
| // A registration that fails without a determinate outcome arms the rollback all the same: the |
| // AddClient it stands for may have reached the network with only the response going missing, and |
| // revoking an identity that was never registered is the cheaper of the two ways to be wrong. |
| TEST_F(DeviceCommissionerPDCTest, IndeterminateRegistrationFailureArmsTheRollback) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(mClientIdentity); |
| mRegistrar.FailRegistrations(CHIP_ERROR_TIMEOUT, /* determinate = */ false); |
| PerformRegisterClientIdentity(params); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kPDCRegisterClientIdentity); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_TIMEOUT) << "the failure was swallowed"; |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), &mRegistrar); |
| |
| mAccess.RollBackNetworkClientIdentity(); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.UnregisteredIdentifier().data_equal(ClientIdentifier())); |
| } |
| |
| // A registrar that can say for certain that nothing was granted -- because it never got the command |
| // out, or because the network turned it down -- saves the attempt a pointless revocation. This is the |
| // only failure that leaves us owing nothing. |
| TEST_F(DeviceCommissionerPDCTest, DeterminateRegistrationFailureArmsNothing) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(mClientIdentity); |
| mRegistrar.FailRegistrations(CHIP_ERROR_TIMEOUT, /* determinate = */ true); |
| PerformRegisterClientIdentity(params); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kPDCRegisterClientIdentity); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_TIMEOUT); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), nullptr); |
| |
| mAccess.RollBackNetworkClientIdentity(); // as CleanupCommissioning would on failure |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| } |
| |
| // A delegate that manages the registration itself owns the decision for a failed registration too, |
| // determinate or not, so there is still nothing for us to roll back. |
| TEST_F(DeviceCommissionerPDCTest, FailedRegistrationArmsNothingWhenTheDelegateManagesRollback) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(mClientIdentity); |
| params.SetManagePDCClientIdentityRollback(false); |
| mRegistrar.FailRegistrations(CHIP_ERROR_TIMEOUT, /* determinate = */ false); |
| PerformRegisterClientIdentity(params); |
| |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_TIMEOUT); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), nullptr); |
| |
| mAccess.RollBackNetworkClientIdentity(); // as CleanupCommissioning would on failure |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| } |
| |
| // A delegate that manages the registration itself still gets the client registered with the network, |
| // but leaves us with nothing to roll back: the identity is now that delegate's to keep or revoke. |
| TEST_F(DeviceCommissionerPDCTest, RegisteringAnIdentityArmsNothingWhenTheDelegateManagesRollback) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(mClientIdentity); |
| params.SetManagePDCClientIdentityRollback(false); |
| PerformRegisterClientIdentity(params); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kPDCRegisterClientIdentity); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_NO_ERROR); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 1); |
| EXPECT_TRUE(mRegistrar.RegisteredIdentity().data_equal(mClientIdentity.Identity())); |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), nullptr); |
| |
| mAccess.RollBackNetworkClientIdentity(); // as CleanupCommissioning would on failure |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| } |
| |
| // Why the decision is latched at registration time rather than taken back afterwards: with the |
| // commissioner out of the picture nothing is ever outstanding, so a delegate that keeps several |
| // Network Client Identities alive is not blocked by the single-slot check on the second registration. |
| TEST_F(DeviceCommissionerPDCTest, DelegateManagedRollbackAllowsSeveralRegistrations) |
| { |
| for (const TestNetworkIdentity * identity : { &mClientIdentity, &mOtherIdentity }) |
| { |
| CommissioningParameters params = RegisterClientIdentityParams(*identity); |
| params.SetManagePDCClientIdentityRollback(false); |
| PerformRegisterClientIdentity(params); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_NO_ERROR); |
| } |
| |
| EXPECT_EQ(mDelegate.mCompletions, 2); |
| EXPECT_EQ(mRegistrar.mRegisterCalls, 2); |
| } |
| |
| // The shape of the NetworkConfigResponse a commissionee configured for Per-Device Credentials has to |
| // return. The commissioner checks this as it reads the response, rather than leaving it to the |
| // delegate, so that a non-conformant commissionee fails kWiFiNetworkSetup like any other Network |
| // Commissioning problem -- and so gets the same failover to the secondary network. |
| class DeviceCommissionerPDCResponseTest : public ::testing::Test |
| { |
| protected: |
| void SetUp() override |
| { |
| ASSERT_EQ(mDelegate.SetCommissioningParameters(PDCParams()), CHIP_NO_ERROR); |
| mAccess.SetCommissioningDelegate(&mDelegate); |
| mAccess.SetDeviceBeingCommissioned(&mDeviceProxy); |
| mAccess.SetCommissioningStage(kWiFiNetworkSetup); |
| } |
| |
| // Parameters as they stand once kWiFiNetworkSetup has configured the commissionee for PDC, which |
| // is what obliges it to return a client identity and a proof of possession. |
| CommissioningParameters PDCParams() |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, &mRegistrar)); |
| params.SetPDCNetworkIdentity(mNetworkIdentity.Identity()); |
| params.SetPDCPossessionNonce(kPossessionNonce); |
| return params; |
| } |
| |
| // Delivers a successful AddOrUpdateWiFiNetwork response carrying the given PDC fields. |
| void DeliverResponse(Optional<ByteSpan> clientIdentity, Optional<ByteSpan> possessionSignature) |
| { |
| NetworkCommissioning::Commands::NetworkConfigResponse::DecodableType data; |
| data.networkingStatus = NetworkCommissioning::NetworkCommissioningStatusEnum::kSuccess; |
| data.clientIdentity = clientIdentity; |
| data.possessionSignature = possessionSignature; |
| DeviceCommissionerTestAccess::OnNetworkConfigResponse(&mCommissioner, data); |
| } |
| |
| // The proof of possession a conformant commissionee returns over our nonce. |
| ByteSpan PossessionSignature() |
| { |
| VerifyOrDie(mClientIdentity.SignPossession(kPossessionNonce, mPossessionSignature) == CHIP_NO_ERROR); |
| return mPossessionSignature.Span(); |
| } |
| |
| DeviceCommissioner mCommissioner{}; |
| DeviceCommissionerTestAccess mAccess{ &mCommissioner }; |
| MockNetworkIdentityRegistrar mRegistrar; |
| StubDeviceProxy mDeviceProxy; |
| RecordingCommissioningDelegate mDelegate; |
| TestNetworkIdentity mNetworkIdentity; |
| TestNetworkIdentity mClientIdentity; |
| Crypto::P256ECDSASignature mPossessionSignature; |
| }; |
| |
| TEST_F(DeviceCommissionerPDCResponseTest, AcceptsAConformantResponse) |
| { |
| DeliverResponse(MakeOptional(mClientIdentity.Identity()), MakeOptional(PossessionSignature())); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_NO_ERROR); |
| EXPECT_EQ(mDelegate.mCompletions, 1); |
| ASSERT_TRUE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| EXPECT_TRUE(mDelegate.mLastClientIdentityInfo.Value().clientIdentity.data_equal(mClientIdentity.Identity())); |
| EXPECT_TRUE(mDelegate.mLastClientIdentityInfo.Value().possessionSignature.data_equal(mPossessionSignature.Span())); |
| } |
| |
| // Each of the ways the response can fail to carry a usable identity fails the stage, and passes no |
| // PDC report on to the delegate. Failing the stage -- rather than the delegate refusing the report -- |
| // is what puts these on the ordinary network-failure path, which |
| // AutoCommissionerPDCTest.PDCOnlyFallsBackToSecondaryNetworkWhenCommissioneeLacksPDC covers. Note the |
| // signature itself is not checked here: that needs the possession nonce, and happens during |
| // kPDCRegisterClientIdentity. |
| TEST_F(DeviceCommissionerPDCResponseTest, RejectsAMissingClientIdentity) |
| { |
| DeliverResponse(NullOptional, MakeOptional(PossessionSignature())); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_MISSING_TLV_ELEMENT); |
| EXPECT_FALSE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| } |
| |
| TEST_F(DeviceCommissionerPDCResponseTest, RejectsAnOversizedClientIdentity) |
| { |
| uint8_t oversized[Credentials::kMaxCHIPCompactNetworkIdentityLength + 1] = {}; |
| DeliverResponse(MakeOptional(ByteSpan(oversized)), MakeOptional(PossessionSignature())); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_MESSAGE_TOO_LONG); |
| EXPECT_FALSE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| } |
| |
| TEST_F(DeviceCommissionerPDCResponseTest, RejectsAMissingPossessionSignature) |
| { |
| DeliverResponse(MakeOptional(mClientIdentity.Identity()), NullOptional); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_MISSING_TLV_ELEMENT); |
| EXPECT_FALSE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| } |
| |
| TEST_F(DeviceCommissionerPDCResponseTest, RejectsAWrongLengthPossessionSignature) |
| { |
| uint8_t truncated[CommissioningParameters::kPossessionSignatureLen - 1] = {}; |
| DeliverResponse(MakeOptional(mClientIdentity.Identity()), MakeOptional(ByteSpan(truncated))); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_ERROR_INVALID_SIGNATURE); |
| EXPECT_FALSE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| } |
| |
| // Whether the response is held to the PDC shape is decided by what we asked the commissionee for, not |
| // by what it happened to send: a commissionee volunteering PDC fields we did not configure it for is |
| // not held to them, and nothing is reported that the delegate has no nonce to check. |
| TEST_F(DeviceCommissionerPDCResponseTest, IgnoresPDCFieldsWhenPDCWasNotRequested) |
| { |
| CommissioningParameters params; |
| params.SetWiFiCredentials(WiFiCredentials(kSsid, kPassphrase)); |
| ASSERT_EQ(mDelegate.SetCommissioningParameters(params), CHIP_NO_ERROR); |
| |
| uint8_t truncated[CommissioningParameters::kPossessionSignatureLen - 1] = {}; |
| DeliverResponse(MakeOptional(mClientIdentity.Identity()), MakeOptional(ByteSpan(truncated))); |
| |
| EXPECT_EQ(mDelegate.mLastStage, kWiFiNetworkSetup); |
| EXPECT_EQ(mDelegate.mLastError, CHIP_NO_ERROR); |
| EXPECT_FALSE(mDelegate.mLastClientIdentityInfo.HasValue()); |
| } |
| |
| // Waiting for a revocation. Revoking a Network Client Identity is best-effort and its outcome changes |
| // nothing, so the commissioner normally lets the call run on unwatched. It waits only where the |
| // ordering is observable: removing a Wi-Fi configuration, because a retry may want to register another |
| // identity, against this registrar or a different one. |
| class DeviceCommissionerRevocationTest : public ::testing::Test |
| { |
| protected: |
| void SetUp() override |
| { |
| mCommissioner.RegisterPairingDelegate(&mPairingDelegate); |
| mAccess.SetDeviceBeingCommissioned(&mDeviceProxy); |
| |
| // As after a successful kPDCRegisterClientIdentity. |
| VerifyOrDie(Credentials::ExtractIdentifierFromChipNetworkIdentity( |
| mClientIdentity.Identity(), Credentials::MutableCertificateKeyId(mClientIdentifier)) == CHIP_NO_ERROR); |
| mAccess.SetNetworkClientRegistration(&mRegistrar, ByteSpan(mClientIdentifier)); |
| } |
| |
| // Delivers a successful NetworkConfigResponse as though it had arrived during the given stage. |
| void DeliverSuccessResponse(CommissioningStage stage) |
| { |
| mAccess.SetCommissioningStage(stage); |
| |
| NetworkCommissioning::Commands::NetworkConfigResponse::DecodableType data; |
| data.networkingStatus = NetworkCommissioning::NetworkCommissioningStatusEnum::kSuccess; |
| DeviceCommissionerTestAccess::OnNetworkConfigResponse(&mCommissioner, data); |
| } |
| |
| class RecordingPairingDelegate : public DevicePairingDelegate |
| { |
| public: |
| void OnCommissioningStatusUpdate(PeerId, CommissioningStage stageCompleted, CHIP_ERROR error) override |
| { |
| mStatusUpdates++; |
| mLastStage = stageCompleted; |
| mLastError = error; |
| } |
| |
| int mStatusUpdates = 0; |
| CommissioningStage mLastStage = kError; |
| CHIP_ERROR mLastError = CHIP_ERROR_INTERNAL; |
| }; |
| |
| DeviceCommissioner mCommissioner{}; |
| DeviceCommissionerTestAccess mAccess{ &mCommissioner }; |
| MockNetworkIdentityRegistrar mRegistrar; |
| StubDeviceProxy mDeviceProxy; |
| RecordingPairingDelegate mPairingDelegate; |
| TestNetworkIdentity mClientIdentity; |
| Credentials::CertificateKeyIdStorage mClientIdentifier{}; |
| }; |
| |
| // A registrar that completes re-entrantly leaves nothing to wait for, so the stage is completed inline |
| // as it would be without PDC -- exactly once, not once here and again from the continuation. This is |
| // the case a warm cache produces, and the one all the other rollback tests exercise. |
| TEST_F(DeviceCommissionerRevocationTest, ReentrantRevocationCompletesTheStageExactlyOnce) |
| { |
| DeliverSuccessResponse(kRemoveWiFiNetworkConfig); |
| |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_FALSE(mRegistrar.HasPendingRevocation()); |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 1); |
| EXPECT_EQ(mPairingDelegate.mLastStage, kRemoveWiFiNetworkConfig); |
| EXPECT_EQ(mPairingDelegate.mLastError, CHIP_NO_ERROR); |
| } |
| |
| // The obligation is discharged when the revocation is handed over, not when it completes: there is |
| // nothing further we could do about a failure, and a second rollback must not repeat the call. |
| TEST_F(DeviceCommissionerRevocationTest, ObligationIsGivenUpBeforeTheRevocationCompletes) |
| { |
| mRegistrar.DeferRevocations(); |
| |
| mAccess.RollBackNetworkClientIdentity(); |
| ASSERT_TRUE(mRegistrar.HasPendingRevocation()); |
| |
| EXPECT_EQ(mAccess.GetNetworkClientRegistrar(), nullptr); |
| mAccess.RollBackNetworkClientIdentity(); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 1); |
| } |
| |
| // Removing the Wi-Fi configuration finishes only once the identity it was using has been given up, so |
| // that a retry can register another one without tripping over the one we are abandoning. |
| TEST_F(DeviceCommissionerRevocationTest, WiFiConfigRemovalWaitsForTheRevocation) |
| { |
| mRegistrar.DeferRevocations(); |
| |
| DeliverSuccessResponse(kRemoveWiFiNetworkConfig); |
| ASSERT_EQ(mRegistrar.mUnregisterCalls, 1); |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 0) << "stage completed before the revocation did"; |
| |
| mRegistrar.CompleteRevocation(); |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 1); |
| EXPECT_EQ(mPairingDelegate.mLastStage, kRemoveWiFiNetworkConfig); |
| EXPECT_EQ(mPairingDelegate.mLastError, CHIP_NO_ERROR); |
| } |
| |
| // A revocation we could not complete is not a reason to fail the removal: the entry is left for an |
| // out-of-band audit, and the commissionee no longer has the configuration that used it either way. |
| TEST_F(DeviceCommissionerRevocationTest, WiFiConfigRemovalSucceedsDespiteAFailedRevocation) |
| { |
| mRegistrar.DeferRevocations(); |
| |
| DeliverSuccessResponse(kRemoveWiFiNetworkConfig); |
| mRegistrar.CompleteRevocation(CHIP_ERROR_TIMEOUT); |
| |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 1); |
| EXPECT_EQ(mPairingDelegate.mLastError, CHIP_NO_ERROR); |
| } |
| |
| // Removing the Thread configuration has no Network Client Identity to give up, so nothing is waited |
| // for -- and nothing is revoked, because a Wi-Fi registration is not ours to withdraw here. |
| TEST_F(DeviceCommissionerRevocationTest, ThreadConfigRemovalDoesNotWait) |
| { |
| mRegistrar.DeferRevocations(); |
| |
| DeliverSuccessResponse(kRemoveThreadNetworkConfig); |
| |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 0); |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 1); |
| EXPECT_EQ(mPairingDelegate.mLastStage, kRemoveThreadNetworkConfig); |
| } |
| |
| // Giving up on a revocation we are no longer waiting for must not keep the callback a new one needs. |
| // A registrar holds us to one revocation at a time, so without the commissioner cancelling first the |
| // second one would simply be refused. Cancelling only drops our tracking of it; the RemoveClient may |
| // still take effect at the network, which is no worse than one we never waited for in the first place. |
| TEST_F(DeviceCommissionerRevocationTest, StaleRevocationIsAbandonedForANewOne) |
| { |
| mRegistrar.DeferRevocations(); |
| mAccess.RollBackNetworkClientIdentity(); |
| ASSERT_TRUE(mRegistrar.HasPendingRevocation()); |
| |
| mAccess.SetNetworkClientRegistration(&mRegistrar, ByteSpan(mClientIdentifier)); |
| mAccess.RollBackNetworkClientIdentity(); |
| |
| EXPECT_EQ(mRegistrar.mCancelledRevocations, 1); |
| EXPECT_EQ(mRegistrar.mUnregisterCalls, 2); |
| EXPECT_TRUE(mRegistrar.HasPendingRevocation()) << "the registrar refused the second revocation"; |
| } |
| |
| // Cancelling the interactions of the attempt drops the continuation waiting on the revocation, but |
| // deliberately not the revocation itself: it undoes a registration we gave up when we issued it, so |
| // abandoning it would strand the entry on the network for no reason. This is why StopPairing() does |
| // not leave the registrar idle, as NetworkIdentityRegistrar documents. |
| TEST_F(DeviceCommissionerRevocationTest, CancellingInteractionsLeavesTheRevocationRunning) |
| { |
| mRegistrar.DeferRevocations(); |
| |
| DeliverSuccessResponse(kRemoveWiFiNetworkConfig); |
| ASSERT_TRUE(mRegistrar.HasPendingRevocation()); |
| |
| mAccess.CancelCommissioningInteractions(); |
| |
| EXPECT_EQ(mRegistrar.mCancelledRevocations, 0); |
| EXPECT_TRUE(mRegistrar.HasPendingRevocation()); |
| |
| // The stage we were holding open is not completed behind whoever cancelled us: they are finishing |
| // the attempt themselves, and completing it here would report a stage they have moved past. So |
| // letting the revocation land must run the base variant, not the continuation we just dropped. |
| mRegistrar.CompleteRevocation(); |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 0); |
| } |
| |
| // ...and the wait must not be left lying around either, or the next revocation -- which nobody is |
| // waiting for -- would complete whatever stage the cancelled one was holding open. |
| TEST_F(DeviceCommissionerRevocationTest, CancellingAWaitDoesNotLeaveItForTheNextRevocation) |
| { |
| mRegistrar.DeferRevocations(); |
| DeliverSuccessResponse(kRemoveWiFiNetworkConfig); |
| mAccess.CancelCommissioningInteractions(); |
| ASSERT_EQ(mPairingDelegate.mStatusUpdates, 0); |
| |
| // A fresh attempt gets as far as registering an identity, and then gives it up again. |
| mAccess.SetNetworkClientRegistration(&mRegistrar, ByteSpan(mClientIdentifier)); |
| mAccess.RollBackNetworkClientIdentity(); |
| ASSERT_TRUE(mRegistrar.HasPendingRevocation()); |
| mRegistrar.CompleteRevocation(); |
| |
| EXPECT_EQ(mPairingDelegate.mStatusUpdates, 0) << "a stage was completed by a revocation nobody waited for"; |
| } |
| } // namespace |