diff --git a/src/app/clusters/bindings/BindingManager.cpp b/src/app/clusters/bindings/BindingManager.cpp index ce52f52867920d..26304df7a5e88b 100644 --- a/src/app/clusters/bindings/BindingManager.cpp +++ b/src/app/clusters/bindings/BindingManager.cpp @@ -48,6 +48,9 @@ class BindingFabricTableDelegate : public chip::FabricTable::Delegate // Intentionally left blank void OnFabricPersistedToStorage(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override {} + + // Intentionally left blank + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override {} }; BindingFabricTableDelegate gFabricTableDelegate; diff --git a/src/app/clusters/door-lock-server/door-lock-server.cpp b/src/app/clusters/door-lock-server/door-lock-server.cpp index 93f83a529953b5..f8d1fe8634f821 100644 --- a/src/app/clusters/door-lock-server/door-lock-server.cpp +++ b/src/app/clusters/door-lock-server/door-lock-server.cpp @@ -73,6 +73,9 @@ class DoorLockClusterFabricDelegate : public chip::FabricTable::Delegate // Intentionally left blank void OnFabricPersistedToStorage(FabricTable & fabricTable, FabricIndex fabricIndex) override {} + + // Intentionally left blank + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override {} }; static DoorLockClusterFabricDelegate gFabricDelegate; diff --git a/src/app/clusters/operational-credentials-server/operational-credentials-server.cpp b/src/app/clusters/operational-credentials-server/operational-credentials-server.cpp index 3c361a020ba9a0..41058931c2801e 100644 --- a/src/app/clusters/operational-credentials-server/operational-credentials-server.cpp +++ b/src/app/clusters/operational-credentials-server/operational-credentials-server.cpp @@ -460,6 +460,9 @@ class OpCredsFabricTableDelegate : public chip::FabricTable::Delegate ChipLogValueX64(fabric->GetNodeId()), fabric->GetVendorId()); fabricListChanged(); } + + // This is triggered by operation credential server so there is nothing additional that we need to do. + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override {} }; OpCredsFabricTableDelegate gFabricDelegate; diff --git a/src/app/server/Server.h b/src/app/server/Server.h index 03c75082005be6..bc07651b664ed7 100644 --- a/src/app/server/Server.h +++ b/src/app/server/Server.h @@ -437,6 +437,12 @@ class Server (void) fabricIndex; } + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override + { + (void) fabricTable; + (void) fabricIndex; + } + private: Server * mServer = nullptr; }; diff --git a/src/controller/CHIPDeviceControllerFactory.h b/src/controller/CHIPDeviceControllerFactory.h index e090926811d94b..5acbd03fb1193b 100644 --- a/src/controller/CHIPDeviceControllerFactory.h +++ b/src/controller/CHIPDeviceControllerFactory.h @@ -200,6 +200,12 @@ class DeviceControllerFactory (void) fabricIndex; } + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override + { + (void) fabricTable; + (void) fabricIndex; + } + private: SessionManager * mSessionManager = nullptr; Credentials::GroupDataProvider * mGroupDataProvider = nullptr; diff --git a/src/credentials/FabricTable.cpp b/src/credentials/FabricTable.cpp index 222ea1c278defb..418e97566965e9 100644 --- a/src/credentials/FabricTable.cpp +++ b/src/credentials/FabricTable.cpp @@ -754,6 +754,20 @@ class NotBeforeCollector : public Credentials::CertificateValidityPolicy System::Clock::Seconds32 mLatestNotBefore; }; +CHIP_ERROR FabricTable::NotifyNOCUpdatedOnFabric(FabricIndex fabricIndex) +{ + FabricTable::Delegate * delegate = mDelegateListRoot; + while (delegate) + { + // It is possible that delegate will remove itself from the list in OnFabricNOCUpdated, + // so we grab the next delegate in the list now. + FabricTable::Delegate * nextDelegate = delegate->next; + delegate->OnFabricNOCUpdated(*this, fabricIndex); + delegate = nextDelegate; + } + return CHIP_NO_ERROR; +} + CHIP_ERROR FabricTable::UpdateFabric(FabricIndex fabricIndex, FabricInfo & newFabricInfo) { FabricInfo * fabricInfo = FindFabricWithIndex(fabricIndex); @@ -766,6 +780,9 @@ CHIP_ERROR FabricTable::UpdateFabric(FabricIndex fabricIndex, FabricInfo & newFa // for CASE and current time is also unknown, the certificate // validity policy will see this condition and can act appropriately. mLastKnownGoodTime.UpdateLastKnownGoodChipEpochTime(notBeforeCollector.mLatestNotBefore); + + NotifyNOCUpdatedOnFabric(fabricIndex); + return CHIP_NO_ERROR; } @@ -892,8 +909,11 @@ CHIP_ERROR FabricTable::Delete(FabricIndex fabricIndex) FabricTable::Delegate * delegate = mDelegateListRoot; while (delegate) { + // It is possible that delegate will remove itself from the list in OnFabricDeletedFromStorage, + // so we grab the next delegate in the list now. + FabricTable::Delegate * nextDelegate = delegate->next; delegate->OnFabricDeletedFromStorage(*this, fabricIndex); - delegate = delegate->next; + delegate = nextDelegate; } } return CHIP_NO_ERROR; diff --git a/src/credentials/FabricTable.h b/src/credentials/FabricTable.h index 23968dc684093c..a17882cd08a8a3 100644 --- a/src/credentials/FabricTable.h +++ b/src/credentials/FabricTable.h @@ -364,6 +364,11 @@ class DLL_EXPORT FabricTable **/ virtual void OnFabricPersistedToStorage(FabricTable & fabricTable, FabricIndex fabricIndex) = 0; + /** + * Gets called when operational credentials are changed. + **/ + virtual void OnFabricNOCUpdated(FabricTable & fabricTable, FabricIndex fabricIndex) = 0; + // Intrusive list pointer for FabricTable to manage the entries. Delegate * next = nullptr; }; @@ -404,6 +409,12 @@ class DLL_EXPORT FabricTable */ CHIP_ERROR UpdateFabric(FabricIndex fabricIndex, FabricInfo & fabricInfo); + // TODO this #if CONFIG_IM_BUILD_FOR_UNIT_TEST is temporary. There is a change incoming soon + // that will allow triggering NOC update directly. +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + void SendUpdateFabricNotificationForTest(FabricIndex fabricIndex) { NotifyNOCUpdatedOnFabric(fabricIndex); } +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST + FabricInfo * FindFabric(const Crypto::P256PublicKey & rootPubKey, FabricId fabricId); FabricInfo * FindFabricWithIndex(FabricIndex fabricIndex); const FabricInfo * FindFabricWithIndex(FabricIndex fabricIndex) const; @@ -594,6 +605,8 @@ class DLL_EXPORT FabricTable CHIP_ERROR AddNewFabricInner(FabricInfo & fabric, FabricIndex * assignedIndex); + CHIP_ERROR NotifyNOCUpdatedOnFabric(FabricIndex fabricIndex); + FabricInfo mStates[CHIP_CONFIG_MAX_FABRICS]; PersistentStorageDelegate * mStorage = nullptr; Crypto::OperationalKeystore * mOperationalKeystore = nullptr; diff --git a/src/protocols/secure_channel/CASESession.cpp b/src/protocols/secure_channel/CASESession.cpp index 2c26c89039a6d5..2a84ec920838e6 100644 --- a/src/protocols/secure_channel/CASESession.cpp +++ b/src/protocols/secure_channel/CASESession.cpp @@ -144,12 +144,30 @@ void CASESession::Clear() mState = State::kInitialized; Crypto::ClearSecretData(mIPK); + if (mFabricsTable) + { + mFabricsTable->RemoveFabricDelegate(this); + } + mLocalNodeId = kUndefinedNodeId; mPeerNodeId = kUndefinedNodeId; mFabricsTable = nullptr; mFabricIndex = kUndefinedFabricIndex; } +void CASESession::InvalidateIfPendingEstablishmentOnFabric(FabricIndex fabricIndex) +{ + if (mFabricIndex != fabricIndex) + { + return; + } + if (!IsSessionEstablishmentInProgress()) + { + return; + } + AbortPendingEstablish(CHIP_ERROR_CANCELLED); +} + CHIP_ERROR CASESession::Init(SessionManager & sessionManager, Credentials::CertificateValidityPolicy * policy, SessionEstablishmentDelegate * delegate, const ScopedNodeId & sessionEvictionHint) { @@ -172,16 +190,21 @@ CHIP_ERROR CASESession::Init(SessionManager & sessionManager, Credentials::Certi } CHIP_ERROR -CASESession::PrepareForSessionEstablishment(SessionManager & sessionManager, FabricTable * fabrics, +CASESession::PrepareForSessionEstablishment(SessionManager & sessionManager, FabricTable * fabricTable, SessionResumptionStorage * sessionResumptionStorage, Credentials::CertificateValidityPolicy * policy, SessionEstablishmentDelegate * delegate, ScopedNodeId previouslyEstablishedPeer, Optional mrpConfig) { - VerifyOrReturnError(fabrics != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + // Below VerifyOrReturnError is not SuccessOrExit since we only want to goto `exit:` after + // Init has been successfully called. + VerifyOrReturnError(fabricTable != nullptr, CHIP_ERROR_INVALID_ARGUMENT); ReturnErrorOnFailure(Init(sessionManager, policy, delegate, previouslyEstablishedPeer)); - mFabricsTable = fabrics; + CHIP_ERROR err = CHIP_NO_ERROR; + SuccessOrExit(err = fabricTable->AddFabricDelegate(this)); + + mFabricsTable = fabricTable; mRole = CryptoContext::SessionRole::kResponder; mSessionResumptionStorage = sessionResumptionStorage; mLocalMRPConfig = mrpConfig; @@ -189,7 +212,12 @@ CASESession::PrepareForSessionEstablishment(SessionManager & sessionManager, Fab ChipLogDetail(SecureChannel, "Allocated SecureSession (%p) - waiting for Sigma1 msg", mSecureSessionHolder.Get().Value()->AsSecureSession()); - return CHIP_NO_ERROR; +exit: + if (err != CHIP_NO_ERROR) + { + Clear(); + } + return err; } CHIP_ERROR CASESession::EstablishSession(SessionManager & sessionManager, FabricTable * fabricTable, ScopedNodeId peerScopedNodeId, @@ -222,6 +250,8 @@ CHIP_ERROR CASESession::EstablishSession(SessionManager & sessionManager, Fabric // been initialized SuccessOrExit(err); + SuccessOrExit(err = fabricTable->AddFabricDelegate(this)); + mFabricsTable = fabricTable; mFabricIndex = fabricInfo->GetFabricIndex(); mSessionResumptionStorage = sessionResumptionStorage; @@ -251,12 +281,17 @@ void CASESession::OnResponseTimeout(ExchangeContext * ec) VerifyOrReturn(mExchangeCtxt == ec, ChipLogError(SecureChannel, "CASESession::OnResponseTimeout exchange doesn't match")); ChipLogError(SecureChannel, "CASESession timed out while waiting for a response from the peer. Current state was %u", to_underlying(mState)); - // Discard the exchange so that Clear() doesn't try closing it. The + // Discard the exchange so that Clear() doesn't try aborting it. The // exchange will handle that. DiscardExchange(); + AbortPendingEstablish(CHIP_ERROR_TIMEOUT); +} + +void CASESession::AbortPendingEstablish(CHIP_ERROR err) +{ Clear(); // Do this last in case the delegate frees us. - mDelegate->OnSessionEstablishmentError(CHIP_ERROR_TIMEOUT); + mDelegate->OnSessionEstablishmentError(err); } CHIP_ERROR CASESession::DeriveSecureSession(CryptoContext & session) const @@ -1692,6 +1727,22 @@ CHIP_ERROR CASESession::OnMessageReceived(ExchangeContext * ec, const PayloadHea Protocols::SecureChannel::MsgType msgType = static_cast(payloadHeader.GetMessageType()); SuccessOrExit(err); +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + if (mStopHandshakeAtState.HasValue() && mState == mStopHandshakeAtState.Value()) + { + mStopHandshakeAtState = Optional::Missing(); + // For testing purposes we are trying to stop a successful CASESession from happening by dropping part of the + // handshake in the middle. We are trying to keep both sides of the CASESession establishment in an active + // pending state. In order to keep this side open we have to tell the exchange context that we will send an + // async message. + // + // Should you need to resume the CASESession, you could theoretically pass along the msg to a callback that gets + // registered when setting mStopHandshakeAtState. + mExchangeCtxt->WillSendMessage(); + return CHIP_NO_ERROR; + } +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST + #if CHIP_CONFIG_SLOW_CRYPTO if (msgType == Protocols::SecureChannel::MsgType::CASE_Sigma1 || msgType == Protocols::SecureChannel::MsgType::CASE_Sigma2 || msgType == Protocols::SecureChannel::MsgType::CASE_Sigma2Resume || @@ -1788,12 +1839,10 @@ CHIP_ERROR CASESession::OnMessageReceived(ExchangeContext * ec, const PayloadHea // Call delegate to indicate session establishment failure. if (err != CHIP_NO_ERROR) { - // Discard the exchange so that Clear() doesn't try closing it. The + // Discard the exchange so that Clear() doesn't try aborting it. The // exchange will handle that. DiscardExchange(); - Clear(); - // Do this last in case the delegate frees us. - mDelegate->OnSessionEstablishmentError(err); + AbortPendingEstablish(err); } return err; } diff --git a/src/protocols/secure_channel/CASESession.h b/src/protocols/secure_channel/CASESession.h index 2e8bf3f61f8ee7..01601ba8d2676c 100644 --- a/src/protocols/secure_channel/CASESession.h +++ b/src/protocols/secure_channel/CASESession.h @@ -58,6 +58,7 @@ namespace chip { // when implementing concurrent CASE session. class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, public Messaging::ExchangeDelegate, + public FabricTable::Delegate, public PairingSession { public: @@ -73,7 +74,7 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, * Initialize using configured fabrics and wait for session establishment requests. * * @param sessionManager session manager from which to allocate a secure session object - * @param fabrics Table of fabrics that are currently configured on the device + * @param fabricTable Table of fabrics that are currently configured on the device * @param policy Optional application-provided certificate validity policy * @param delegate Callback object * @param previouslyEstablishedPeer If a session had previously been established successfully to a peer, this should @@ -84,7 +85,7 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, * @return CHIP_ERROR The result of initialization */ CHIP_ERROR PrepareForSessionEstablishment( - SessionManager & sessionManager, FabricTable * fabrics, SessionResumptionStorage * sessionResumptionStorage, + SessionManager & sessionManager, FabricTable * fabricTable, SessionResumptionStorage * sessionResumptionStorage, Credentials::CertificateValidityPolicy * policy, SessionEstablishmentDelegate * delegate, ScopedNodeId previouslyEstablishedPeer, Optional mrpConfig = Optional::Missing()); @@ -166,6 +167,28 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, //// SessionDelegate //// void OnSessionReleased() override; + //// FabricTable::Delegate Implementation //// + void OnFabricDeletedFromStorage(FabricTable & fabricTable, FabricIndex fabricIndex) override + { + (void) fabricTable; + InvalidateIfPendingEstablishmentOnFabric(fabricIndex); + } + void OnFabricRetrievedFromStorage(FabricTable & fabricTable, FabricIndex fabricIndex) override + { + (void) fabricTable; + (void) fabricIndex; + } + void OnFabricPersistedToStorage(FabricTable & fabricTable, FabricIndex fabricIndex) override + { + (void) fabricTable; + (void) fabricIndex; + } + void OnFabricNOCUpdated(chip::FabricTable & fabricTable, chip::FabricIndex fabricIndex) override + { + (void) fabricTable; + InvalidateIfPendingEstablishmentOnFabric(fabricIndex); + } + FabricIndex GetFabricIndex() const { return mFabricIndex; } // TODO: remove Clear, we should create a new instance instead reset the old instance. @@ -174,6 +197,7 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, void Clear(); private: + friend class CASESessionForTest; enum class State : uint8_t { kInitialized = 0, @@ -237,6 +261,8 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, void OnSuccessStatusReport() override; CHIP_ERROR OnFailureStatusReport(Protocols::SecureChannel::GeneralStatusCode generalCode, uint16_t protocolCode) override; + void AbortPendingEstablish(CHIP_ERROR err); + CHIP_ERROR GetHardcodedTime(); CHIP_ERROR SetEffectiveTime(); @@ -244,6 +270,12 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, CHIP_ERROR ValidateReceivedMessage(Messaging::ExchangeContext * ec, const PayloadHeader & payloadHeader, const System::PacketBufferHandle & msg); + void InvalidateIfPendingEstablishmentOnFabric(FabricIndex fabricIndex); + +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + void SetStopSigmaHandshakeAt(Optional state) { mStopHandshakeAtState = state; } +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST + Crypto::Hash_SHA256_stream mCommissioningHash; Crypto::P256PublicKey mRemotePubKey; #ifdef ENABLE_HSM_CASE_EPHEMERAL_KEY @@ -272,6 +304,10 @@ class DLL_EXPORT CASESession : public Messaging::UnsolicitedMessageHandler, uint8_t mInitiatorRandom[kSigmaParamRandomNumberSize]; State mState; + +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + Optional mStopHandshakeAtState = Optional::Missing(); +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST }; } // namespace chip diff --git a/src/protocols/secure_channel/PairingSession.cpp b/src/protocols/secure_channel/PairingSession.cpp index b3721ca8533a45..8b12fa69974b80 100644 --- a/src/protocols/secure_channel/PairingSession.cpp +++ b/src/protocols/secure_channel/PairingSession.cpp @@ -138,6 +138,17 @@ CHIP_ERROR PairingSession::DecodeMRPParametersIfPresent(TLV::Tag expectedTag, TL return tlvReader.ExitContainer(containerType); } +bool PairingSession::IsSessionEstablishmentInProgress() +{ + if (!mSecureSessionHolder) + { + return false; + } + + Transport::SecureSession * secureSession = mSecureSessionHolder->AsSecureSession(); + return secureSession->IsPairing(); +} + void PairingSession::Clear() { // Clear acts like the destructor if PairingSession, if it is call during diff --git a/src/protocols/secure_channel/PairingSession.h b/src/protocols/secure_channel/PairingSession.h index f8ac1d3afcd4ba..dccb8ec7d8ead7 100644 --- a/src/protocols/secure_channel/PairingSession.h +++ b/src/protocols/secure_channel/PairingSession.h @@ -187,6 +187,8 @@ class DLL_EXPORT PairingSession : public SessionDelegate */ CHIP_ERROR DecodeMRPParametersIfPresent(TLV::Tag expectedTag, TLV::ContiguousBufferTLVReader & tlvReader); + bool IsSessionEstablishmentInProgress(); + // TODO: remove Clear, we should create a new instance instead reset the old instance. void Clear(); diff --git a/src/protocols/secure_channel/tests/TestCASESession.cpp b/src/protocols/secure_channel/tests/TestCASESession.cpp index 86626ae08a2fb7..339fc7123fc811 100644 --- a/src/protocols/secure_channel/tests/TestCASESession.cpp +++ b/src/protocols/secure_channel/tests/TestCASESession.cpp @@ -236,21 +236,25 @@ void CASE_SecurePairingWaitTest(nlTestSuite * inSuite, void * inContext) // Test all combinations of invalid parameters TestCASESecurePairingDelegate delegate; - CASESession pairing; FabricTable fabrics; + // In normal operation scope of FabricTable outlives CASESession. Without this scoping we hit + // ASAN test issue since FabricTable is not normally on the stack. + { + CASESession caseSession; - NL_TEST_ASSERT(inSuite, pairing.GetSecureSessionType() == SecureSession::Type::kCASE); + NL_TEST_ASSERT(inSuite, caseSession.GetSecureSessionType() == SecureSession::Type::kCASE); - pairing.SetGroupDataProvider(&gDeviceGroupDataProvider); - NL_TEST_ASSERT(inSuite, - pairing.PrepareForSessionEstablishment(sessionManager, nullptr, nullptr, nullptr, nullptr, ScopedNodeId()) == - CHIP_ERROR_INVALID_ARGUMENT); - NL_TEST_ASSERT(inSuite, - pairing.PrepareForSessionEstablishment(sessionManager, nullptr, nullptr, nullptr, &delegate, ScopedNodeId()) == - CHIP_ERROR_INVALID_ARGUMENT); - NL_TEST_ASSERT(inSuite, - pairing.PrepareForSessionEstablishment(sessionManager, &fabrics, nullptr, nullptr, &delegate, ScopedNodeId()) == - CHIP_NO_ERROR); + caseSession.SetGroupDataProvider(&gDeviceGroupDataProvider); + NL_TEST_ASSERT(inSuite, + caseSession.PrepareForSessionEstablishment(sessionManager, nullptr, nullptr, nullptr, nullptr, + ScopedNodeId()) == CHIP_ERROR_INVALID_ARGUMENT); + NL_TEST_ASSERT(inSuite, + caseSession.PrepareForSessionEstablishment(sessionManager, nullptr, nullptr, nullptr, &delegate, + ScopedNodeId()) == CHIP_ERROR_INVALID_ARGUMENT); + NL_TEST_ASSERT(inSuite, + caseSession.PrepareForSessionEstablishment(sessionManager, &fabrics, nullptr, nullptr, &delegate, + ScopedNodeId()) == CHIP_NO_ERROR); + } } void CASE_SecurePairingStartTest(nlTestSuite * inSuite, void * inContext) @@ -344,11 +348,26 @@ void CASE_SecurePairingHandshakeTestCommon(nlTestSuite * inSuite, void * inConte NL_TEST_ASSERT(inSuite, loopback.mSentMessageCount == sTestCaseMessageCount); NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingComplete == 1); NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingComplete == 1); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingErrors == 0); NL_TEST_ASSERT(inSuite, pairingAccessory.GetRemoteMRPConfig().mIdleRetransTimeout == System::Clock::Milliseconds32(5000)); NL_TEST_ASSERT(inSuite, pairingAccessory.GetRemoteMRPConfig().mActiveRetransTimeout == System::Clock::Milliseconds32(300)); NL_TEST_ASSERT(inSuite, pairingCommissioner.GetRemoteMRPConfig().mIdleRetransTimeout == System::Clock::Milliseconds32(360000)); NL_TEST_ASSERT(inSuite, pairingCommissioner.GetRemoteMRPConfig().mActiveRetransTimeout == System::Clock::Milliseconds32(100000)); +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + // Confirming that FabricTable sending a notification that fabric was updated doesn't affect + // already established connections. + // + // This is compiled for host tests which is enough test coverage + gCommissionerFabrics.SendUpdateFabricNotificationForTest(gCommissionerFabricIndex); + gDeviceFabrics.SendUpdateFabricNotificationForTest(gDeviceFabricIndex); + NL_TEST_ASSERT(inSuite, loopback.mSentMessageCount == sTestCaseMessageCount); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingComplete == 1); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingComplete == 1); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingErrors == 0); +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST } void CASE_SecurePairingHandshakeTest(nlTestSuite * inSuite, void * inContext) @@ -824,6 +843,85 @@ static void CASE_SessionResumptionStorage(nlTestSuite * inSuite, void * inContex } } +// TODO, move all tests above into this class +#if CONFIG_IM_BUILD_FOR_UNIT_TEST +namespace chip { +// TODO rename CASESessionForTest to TestCASESession. Not doing that immediately since that requires +// removing a lot of the `using namesapce` above which is a larger cleanup. +class CASESessionForTest +{ +public: + static void CASE_SimulateUpdateNOCInvalidatePendingEstablishment(nlTestSuite * inSuite, void * inContext); +}; + +void CASESessionForTest::CASE_SimulateUpdateNOCInvalidatePendingEstablishment(nlTestSuite * inSuite, void * inContext) +{ + SessionManager sessionManager; + TestCASESecurePairingDelegate delegateCommissioner; + CASESession pairingCommissioner; + pairingCommissioner.SetGroupDataProvider(&gCommissionerGroupDataProvider); + + TestContext & ctx = *reinterpret_cast(inContext); + + TestCASESecurePairingDelegate delegateAccessory; + CASESession pairingAccessory; + + auto & loopback = ctx.GetLoopback(); + loopback.mSentMessageCount = 0; + + NL_TEST_ASSERT(inSuite, + ctx.GetExchangeManager().RegisterUnsolicitedMessageHandlerForType(Protocols::SecureChannel::MsgType::CASE_Sigma1, + &pairingAccessory) == CHIP_NO_ERROR); + + // In order for all the test iterations below, we need to stop the CASE sigma handshake in the middle such + // that the CASE session is in the process of being established. + pairingCommissioner.SetStopSigmaHandshakeAt(MakeOptional(CASESession::State::kSentSigma1)); + + ExchangeContext * contextCommissioner = ctx.NewUnauthenticatedExchangeToBob(&pairingCommissioner); + + pairingAccessory.SetGroupDataProvider(&gDeviceGroupDataProvider); + NL_TEST_ASSERT(inSuite, + pairingAccessory.PrepareForSessionEstablishment(sessionManager, &gDeviceFabrics, nullptr, nullptr, + &delegateAccessory, ScopedNodeId()) == CHIP_NO_ERROR); + + gDeviceFabrics.SendUpdateFabricNotificationForTest(gDeviceFabricIndex); + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 0); + + NL_TEST_ASSERT(inSuite, + pairingCommissioner.EstablishSession(sessionManager, &gCommissionerFabrics, + ScopedNodeId{ Node01_01, gCommissionerFabricIndex }, contextCommissioner, + nullptr, nullptr, &delegateCommissioner) == CHIP_NO_ERROR); + ctx.DrainAndServiceIO(); + + // At this point the CASESession is in the process of establishing. Confirm that there are no errors and there are session + // has not been established. + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingComplete == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingComplete == 0); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingErrors == 0); + + // Simulating an update to the Fabric NOC for gCommissionerFabrics fabric table. + // Confirm that CASESession on commisioner side has reported an error. + gCommissionerFabrics.SendUpdateFabricNotificationForTest(gCommissionerFabricIndex); + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingErrors == 1); + + // Simulating an update to the Fabric NOC for gDeviceFabrics fabric table. + // Confirm that CASESession on accessory side has reported an error. + gDeviceFabrics.SendUpdateFabricNotificationForTest(gDeviceFabricIndex); + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingErrors == 1); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingErrors == 1); + + // Sanity check that pairing did not complete. + NL_TEST_ASSERT(inSuite, delegateAccessory.mNumPairingComplete == 0); + NL_TEST_ASSERT(inSuite, delegateCommissioner.mNumPairingComplete == 0); +} +} // namespace chip +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST + // Test Suite /** @@ -839,6 +937,11 @@ static const nlTest sTests[] = NL_TEST_DEF("Sigma1Parsing", CASE_Sigma1ParsingTest), NL_TEST_DEF("DestinationId", CASE_DestinationIdTest), NL_TEST_DEF("SessionResumptionStorage", CASE_SessionResumptionStorage), +#if CONFIG_IM_BUILD_FOR_UNIT_TEST + // This is compiled for host tests which is enough test coverage to ensure updating NOC invalidates + // CASESession that are in the process of establishing. + NL_TEST_DEF("InvalidatePendingSessionEstablishment", chip::CASESessionForTest::CASE_SimulateUpdateNOCInvalidatePendingEstablishment), +#endif // CONFIG_IM_BUILD_FOR_UNIT_TEST NL_TEST_SENTINEL() };