diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 253a84a16..422f47550 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -386,6 +386,8 @@ openthread_core_files = [ "meshcop/announce_begin_client.hpp", "meshcop/border_agent.cpp", "meshcop/border_agent.hpp", + "meshcop/border_agent_ephemeral_key.cpp", + "meshcop/border_agent_ephemeral_key.hpp", "meshcop/border_agent_tracker.cpp", "meshcop/border_agent_tracker.hpp", "meshcop/border_agent_txt_data.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f17eda820..049b1f245 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -161,6 +161,7 @@ set(COMMON_SOURCES mac/wakeup_tx_scheduler.cpp meshcop/announce_begin_client.cpp meshcop/border_agent.cpp + meshcop/border_agent_ephemeral_key.cpp meshcop/border_agent_tracker.cpp meshcop/border_agent_txt_data.cpp meshcop/commissioner.cpp diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index ec4438816..425f982aa 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -88,6 +88,7 @@ #include "mac/mac.hpp" #include "mac/wakeup_tx_scheduler.hpp" #include "meshcop/border_agent.hpp" +#include "meshcop/border_agent_ephemeral_key.hpp" #include "meshcop/border_agent_tracker.hpp" #include "meshcop/border_agent_txt_data.hpp" #include "meshcop/commissioner.hpp" diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp index b51f5c4f8..022ceb5f2 100644 --- a/src/core/meshcop/border_agent.cpp +++ b/src/core/meshcop/border_agent.cpp @@ -478,398 +478,6 @@ exit: return error; } -//---------------------------------------------------------------------------------------------------------------------- -// EphemeralKeyManager - -#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE - -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE -const char EphemeralKeyManager::kServiceType[] = "_meshcop-e._udp"; -#endif - -EphemeralKeyManager::EphemeralKeyManager(Instance &aInstance) - : InstanceLocator(aInstance) -#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_FEATURE_ENABLED_BY_DEFAULT - , mState(kStateStopped) -#else - , mState(kStateDisabled) -#endif - , mDtlsTransport(aInstance, kNoLinkSecurity) - , mCoapDtlsSession(nullptr) - , mTimer(aInstance) - , mCallbackTask(aInstance) -{ -} - -void EphemeralKeyManager::SetEnabled(bool aEnabled) -{ - if (aEnabled) - { - VerifyOrExit(mState == kStateDisabled); - SetState(kStateStopped); - } - else - { - VerifyOrExit(mState != kStateDisabled); - Stop(); - SetState(kStateDisabled); - } - - Get().Refresh(); - -exit: - return; -} - -Error EphemeralKeyManager::Start(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort) -{ - Error error = kErrorNone; - uint16_t length; - - VerifyOrExit(mState == kStateStopped, error = kErrorInvalidState); - - length = StringLength(aKeyString, kMaxKeyLength + 1); - VerifyOrExit((length >= kMinKeyLength) && (length <= kMaxKeyLength), error = kErrorInvalidArgs); - - IgnoreError(mDtlsTransport.SetMaxConnectionAttempts(kMaxConnectionAttempts, HandleTransportClosed, this)); - - mDtlsTransport.SetAcceptCallback(EphemeralKeyManager::HandleAcceptSession, this); - mDtlsTransport.SetRemoveSessionCallback(EphemeralKeyManager::HandleRemoveSession, this); - - SuccessOrExit(error = mDtlsTransport.Open()); - SuccessOrExit(error = mDtlsTransport.Bind(aUdpPort)); - - SuccessOrExit( - error = mDtlsTransport.SetPsk(reinterpret_cast(aKeyString), static_cast(length))); - - aTimeout = Min((aTimeout == 0) ? kDefaultTimeout : aTimeout, kMaxTimeout); - mTimer.Start(aTimeout); - - LogInfo("Allow ephemeral key for %lu msec on port %u", ToUlong(aTimeout), GetUdpPort()); - - SetState(kStateStarted); - -exit: - switch (error) - { - case kErrorNone: - Get().mCounters.mEpskcActivations++; -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcActivated); -#endif - break; - case kErrorInvalidState: - Get().mCounters.mEpskcInvalidBaStateErrors++; - break; - case kErrorInvalidArgs: - Get().mCounters.mEpskcInvalidArgsErrors++; - break; - default: - Get().mCounters.mEpskcStartSecureSessionErrors++; - break; - } - - return error; -} - -void EphemeralKeyManager::Stop(void) { Stop(kReasonLocalDisconnect); } - -void EphemeralKeyManager::Stop(DeactivationReason aReason) -{ - switch (mState) - { - case kStateStarted: - case kStateConnected: - case kStateAccepted: - break; - case kStateDisabled: - case kStateStopped: - ExitNow(); - } - - LogInfo("Stopping ephemeral key use - reason: %s", DeactivationReasonToString(aReason)); - SetState(kStateStopped); - - mTimer.Stop(); - mDtlsTransport.Close(); - - UpdateCountersAndRecordEvent(aReason); - -exit: - return; -} - -void EphemeralKeyManager::UpdateCountersAndRecordEvent(DeactivationReason aReason) -{ - struct ReasonToCounterEventEntry - { - DeactivationReason mReason; - uint8_t mEvent; // Raw values of `HistoryTracker::Local::Epskc` enum. - uint32_t Counters::*mCounterPtr; - }; - -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE -#define ReasonEntry(kReason, kCounter, kEvent) \ - { \ - kReason, \ - HistoryTracker::Local::kEvent, \ - &Counters::kCounter, \ - } -#else -#define ReasonEntry(kReason, kCounter, kEvent) {kReason, 0, &Counters::kCounter} -#endif - - static const ReasonToCounterEventEntry kReasonToCounterEventEntries[] = { - ReasonEntry(kReasonLocalDisconnect, mEpskcDeactivationClears, kEpskcDeactivatedLocalClose), - ReasonEntry(kReasonSessionTimeout, mEpskcDeactivationClears, kEpskcDeactivatedSessionTimeout), - ReasonEntry(kReasonPeerDisconnect, mEpskcDeactivationDisconnects, kEpskcDeactivatedRemoteClose), - ReasonEntry(kReasonSessionError, mEpskcStartSecureSessionErrors, kEpskcDeactivatedSessionError), - ReasonEntry(kReasonMaxFailedAttempts, mEpskcDeactivationMaxAttempts, kEpskcDeactivatedMaxAttempts), - ReasonEntry(kReasonEpskcTimeout, mEpskcDeactivationTimeouts, kEpskcDeactivatedEpskcTimeout), - }; - -#undef ReasonEntry - -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - HistoryTracker::EpskcEvent event = HistoryTracker::Local::kEpskcDeactivatedUnknown; -#endif - - for (const ReasonToCounterEventEntry &entry : kReasonToCounterEventEntries) - { - if (aReason == entry.mReason) - { - (Get().mCounters.*(entry.mCounterPtr))++; -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - event = static_cast(entry.mEvent); -#endif - break; - } - } - -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - Get().RecordEpskcEvent(event); -#endif -} - -void EphemeralKeyManager::SetState(State aState) -{ -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - bool isServiceRegistered = ShouldRegisterService(); -#endif - - VerifyOrExit(mState != aState); - LogInfo("Ephemeral key - state: %s -> %s", StateToString(mState), StateToString(aState)); - mState = aState; - mCallbackTask.Post(); - -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - VerifyOrExit(isServiceRegistered != ShouldRegisterService()); - RegisterOrUnregisterService(); -#endif - -exit: - return; -} - -SecureSession *EphemeralKeyManager::HandleAcceptSession(void *aContext, const Ip6::MessageInfo &aMessageInfo) -{ - OT_UNUSED_VARIABLE(aMessageInfo); - - return static_cast(aContext)->HandleAcceptSession(); -} - -Manager::CoapDtlsSession *EphemeralKeyManager::HandleAcceptSession(void) -{ - CoapDtlsSession *session = nullptr; - - VerifyOrExit(mCoapDtlsSession == nullptr); - - session = CoapDtlsSession::Allocate(GetInstance(), mDtlsTransport); - VerifyOrExit(session != nullptr); - - mCoapDtlsSession = session; - -exit: - return session; -} - -void EphemeralKeyManager::HandleRemoveSession(void *aContext, SecureSession &aSession) -{ - static_cast(aContext)->HandleRemoveSession(aSession); -} - -void EphemeralKeyManager::HandleRemoveSession(SecureSession &aSession) -{ - CoapDtlsSession &coapSession = static_cast(aSession); - - coapSession.Cleanup(); - coapSession.Free(); - mCoapDtlsSession = nullptr; -} - -void EphemeralKeyManager::HandleSessionConnected(void) -{ - SetState(kStateConnected); - Get().mCounters.mEpskcSecureSessionSuccesses++; -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcConnected); -#endif -} - -void EphemeralKeyManager::HandleSessionDisconnected(SecureSession::ConnectEvent aEvent) -{ - DeactivationReason reason = kReasonUnknown; - - // The ephemeral key can be used once - VerifyOrExit((mState == kStateConnected) || (mState == kStateAccepted)); - - switch (aEvent) - { - case SecureSession::kDisconnectedError: - reason = kReasonSessionError; - break; - case SecureSession::kDisconnectedPeerClosed: - reason = kReasonPeerDisconnect; - break; - case SecureSession::kDisconnectedMaxAttempts: - reason = kReasonMaxFailedAttempts; - break; - case SecureSession::kDisconnectedTimeout: - reason = kReasonSessionTimeout; - break; - default: - break; - } - - Stop(reason); - -exit: - return; -} - -void EphemeralKeyManager::HandleCommissionerPetitionAccepted(void) -{ - SetState(kStateAccepted); - Get().mCounters.mEpskcCommissionerPetitions++; -#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE - Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcPetitioned); -#endif -} - -void EphemeralKeyManager::HandleTimer(void) { Stop(kReasonEpskcTimeout); } - -void EphemeralKeyManager::HandleTask(void) { mCallback.InvokeIfSet(); } - -void EphemeralKeyManager::HandleTransportClosed(void *aContext) -{ - reinterpret_cast(aContext)->HandleTransportClosed(); -} - -void EphemeralKeyManager::HandleTransportClosed(void) { Stop(kReasonMaxFailedAttempts); } - -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - -bool EphemeralKeyManager::ShouldRegisterService(void) const -{ - bool shouldRegister = false; - - switch (mState) - { - case kStateDisabled: - case kStateStopped: - break; - case kStateStarted: - case kStateConnected: - case kStateAccepted: - shouldRegister = true; - break; - } - - return shouldRegister; -} - -void EphemeralKeyManager::RegisterOrUnregisterService(void) -{ - Dnssd::Service service; - - VerifyOrExit(Get().IsReady()); - - service.Clear(); - service.mServiceInstance = Get().GetServiceName(); - service.mServiceType = kServiceType; - service.mPort = GetUdpPort(); - - if (ShouldRegisterService()) - { - Get().RegisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); - } - else - { - Get().UnregisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); - } - -exit: - return; -} - -#endif // OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - -const char *EphemeralKeyManager::StateToString(State aState) -{ - static const char *const kStateStrings[] = { - "Disabled", // (0) kStateDisabled - "Stopped", // (1) kStateStopped - "Started", // (2) kStateStarted - "Connected", // (3) kStateConnected - "Accepted", // (4) kStateAccepted - }; - - struct EnumCheck - { - InitEnumValidatorCounter(); - ValidateNextEnum(kStateDisabled); - ValidateNextEnum(kStateStopped); - ValidateNextEnum(kStateStarted); - ValidateNextEnum(kStateConnected); - ValidateNextEnum(kStateAccepted); - }; - - return kStateStrings[aState]; -} - -#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) - -const char *EphemeralKeyManager::DeactivationReasonToString(DeactivationReason aReason) -{ - static const char *const kReasonStrings[] = { - "LocalDisconnect", // (0) kReasonLocalDisconnect - "PeerDisconnect", // (1) kReasonPeerDisconnect - "SessionError", // (2) kReasonSessionError - "SessionTimeout", // (3) kReasonSessionTimeout - "MaxFailedAttempts", // (4) kReasonMaxFailedAttempts - "EpskcTimeout", // (5) kReasonTimeout - "Unknown", // (6) kReasonUnknown - }; - - struct EnumCheck - { - InitEnumValidatorCounter(); - ValidateNextEnum(kReasonLocalDisconnect); - ValidateNextEnum(kReasonPeerDisconnect); - ValidateNextEnum(kReasonSessionError); - ValidateNextEnum(kReasonSessionTimeout); - ValidateNextEnum(kReasonMaxFailedAttempts); - ValidateNextEnum(kReasonEpskcTimeout); - ValidateNextEnum(kReasonUnknown); - }; - - return kReasonStrings[aReason]; -} - -#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) - -#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE - //---------------------------------------------------------------------------------------------------------------------- // `Manager::CoapDtlsSession diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp index 3593be66b..3e29111bf 100644 --- a/src/core/meshcop/border_agent.hpp +++ b/src/core/meshcop/border_agent.hpp @@ -364,180 +364,6 @@ private: DeclareTmfHandler(Manager, kUriRelayRx); -#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE -/** - * Manages the ephemeral key use by Border Agent. - */ -class EphemeralKeyManager : public InstanceLocator, private NonCopyable -{ - friend class Manager; - -public: - static constexpr uint16_t kMinKeyLength = OT_BORDER_AGENT_MIN_EPHEMERAL_KEY_LENGTH; ///< Min key len. - static constexpr uint16_t kMaxKeyLength = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_LENGTH; ///< Max key len. - static constexpr uint32_t kDefaultTimeout = OT_BORDER_AGENT_DEFAULT_EPHEMERAL_KEY_TIMEOUT; //< Default timeout. - static constexpr uint32_t kMaxTimeout = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT; ///< Max timeout. - - typedef otBorderAgentEphemeralKeyCallback CallbackHandler; ///< Callback function pointer. - - /** - * Represents the state of the `EphemeralKeyManager`. - */ - enum State : uint8_t - { - kStateDisabled = OT_BORDER_AGENT_STATE_DISABLED, ///< Ephemeral key feature is disabled. - kStateStopped = OT_BORDER_AGENT_STATE_STOPPED, ///< Enabled, but the key is not set and started. - kStateStarted = OT_BORDER_AGENT_STATE_STARTED, ///< Key is set and listening to accept connection. - kStateConnected = OT_BORDER_AGENT_STATE_CONNECTED, ///< Session connected, not full commissioner. - kStateAccepted = OT_BORDER_AGENT_STATE_ACCEPTED, ///< Session connected and accepted as full commissioner. - }; - - /** - * Initializes the `EphemeralKeyManager`. - * - * @param[in] aInstance The OpenThread instance. - */ - explicit EphemeralKeyManager(Instance &aInstance); - - /** - * Enables/disables Ephemeral Key Manager. - * - * If this method is called to disable, while an an ephemeral key is in use, the ephemeral key use will - * be stopped (as if `Stop()` is called). - * - * @param[in] aEnabled Whether to enable or disable. - */ - void SetEnabled(bool aEnabled); - - /** - * Starts using an ephemeral key for a given timeout duration. - * - * An ephemeral key can only be set when `GetState()` is `kStateStopped`. Otherwise, `kErrorInvalidState` is - * returned. This means that setting the ephemeral key again while a previously set key is still in use will - * fail. Callers can stop the previous key by calling `Stop()` before starting with a new key. - * - * The given @p aKeyString is used directly as the ephemeral PSK (excluding the trailing null `\0` character). - * Its length must be between `kMinKeyLength` and `kMaxKeyLength`, inclusive. - * - * The ephemeral key can be used only once by an external commissioner candidate to establish a secure session. - * After the commissioner candidate disconnects, the use of the ephemeral key is stopped. If the timeout - * expires, the use of the ephemeral key is also stopped, and any established session using the key is - * immediately disconnected. - * - * @param[in] aKeyString The ephemeral key. - * @param[in] aTimeout The timeout duration, in milliseconds, to use the ephemeral key. - * If zero, the default `kDefaultTimeout` value is used. If the timeout value is - * larger than `kMaxTimeout`, the maximum value is used instead. - * @param[in] aUdpPort The UDP port to use with the ephemeral key. If the UDP port is zero, an ephemeral - * port is used. `GetUdpPort()` returns the current UDP port being used. - * - * @retval kErrorNone Successfully started using the ephemeral key. - * @retval kErrorInvalidState A previously set ephemeral key is still in use or feature is disabled. - * @retval kErrorInvalidArgs The given @p aKeyString is not valid. - * @retval kErrorFailed Failed to start (e.g., it could not bind to the given UDP port). - */ - Error Start(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort); - - /** - * Stops the ephemeral key use and disconnects any established secure session using it. - * - * If there is no ephemeral key in use, calling this method has no effect. - */ - void Stop(void); - - /** - * Gets the state of ephemeral key use and its session. - * - * @returns The `EmpheralKeyManager` state. - */ - State GetState(void) const { return mState; } - - /** - * Gets the UDP port used by ephemeral key DTLS secure transport. - * - * @returns UDP port number. - */ - uint16_t GetUdpPort(void) const { return mDtlsTransport.GetUdpPort(); } - - /** - * Sets the callback. - * - * @param[in] aCallback The callback function pointer. - * @param[in] aContext The context associated and used with callback handler. - */ - void SetCallback(CallbackHandler aCallback, void *aContext) { mCallback.Set(aCallback, aContext); } - - /** - * Converts a given `State` to human-readable string. - * - * @param[in] aState The state to convert. - * - * @returns The string corresponding to @p aState. - */ - static const char *StateToString(State aState); - -private: - static constexpr uint16_t kMaxConnectionAttempts = 10; - - static_assert(kMaxKeyLength <= Dtls::Transport::kPskMaxLength, "Max e-key len is larger than max PSK len"); - - using CoapDtlsSession = Manager::CoapDtlsSession; - using Counters = Manager::Counters; - - enum DeactivationReason : uint8_t - { - kReasonLocalDisconnect, - kReasonPeerDisconnect, - kReasonSessionError, - kReasonSessionTimeout, - kReasonMaxFailedAttempts, - kReasonEpskcTimeout, - kReasonUnknown, - }; - - void SetState(State aState); - void Stop(DeactivationReason aReason); - void HandleTimer(void); - void HandleTask(void); - bool OwnsSession(CoapDtlsSession &aSession) const { return mCoapDtlsSession == &aSession; } - void HandleSessionConnected(void); - void HandleSessionDisconnected(SecureSession::ConnectEvent aEvent); - void HandleCommissionerPetitionAccepted(void); - void UpdateCountersAndRecordEvent(DeactivationReason aReason); -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - bool ShouldRegisterService(void) const; - void RegisterOrUnregisterService(void); -#endif - - // Session or Transport callbacks - static SecureSession *HandleAcceptSession(void *aContext, const Ip6::MessageInfo &aMessageInfo); - CoapDtlsSession *HandleAcceptSession(void); - static void HandleRemoveSession(void *aContext, SecureSession &aSession); - void HandleRemoveSession(SecureSession &aSession); - static void HandleTransportClosed(void *aContext); - void HandleTransportClosed(void); - -#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) - static const char *DeactivationReasonToString(DeactivationReason aReason); -#endif - -#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE - static const char kServiceType[]; -#endif - - using TimeoutTimer = TimerMilliIn; - using CallbackTask = TaskletIn; - - State mState; - Dtls::Transport mDtlsTransport; - CoapDtlsSession *mCoapDtlsSession; - TimeoutTimer mTimer; - CallbackTask mCallbackTask; - Callback mCallback; -}; - -#endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE - } // namespace BorderAgent } // namespace MeshCoP @@ -547,10 +373,6 @@ DefineCoreType(otBorderAgentId, MeshCoP::BorderAgent::Id); DefineCoreType(otBorderAgentSessionIterator, MeshCoP::BorderAgent::Manager::SessionIterator); -#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE -DefineMapEnum(otBorderAgentEphemeralKeyState, MeshCoP::BorderAgent::EphemeralKeyManager::State); -#endif - } // namespace ot #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE diff --git a/src/core/meshcop/border_agent_ephemeral_key.cpp b/src/core/meshcop/border_agent_ephemeral_key.cpp new file mode 100644 index 000000000..ec1962ed0 --- /dev/null +++ b/src/core/meshcop/border_agent_ephemeral_key.cpp @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the Border Agent Ephemeral Key Manager. + */ + +#include "border_agent_ephemeral_key.hpp" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace MeshCoP { +namespace BorderAgent { + +RegisterLogModule("BorderAgent"); + +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE +const char EphemeralKeyManager::kServiceType[] = "_meshcop-e._udp"; +#endif + +EphemeralKeyManager::EphemeralKeyManager(Instance &aInstance) + : InstanceLocator(aInstance) +#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_FEATURE_ENABLED_BY_DEFAULT + , mState(kStateStopped) +#else + , mState(kStateDisabled) +#endif + , mDtlsTransport(aInstance, kNoLinkSecurity) + , mCoapDtlsSession(nullptr) + , mTimer(aInstance) + , mCallbackTask(aInstance) +{ +} + +void EphemeralKeyManager::SetEnabled(bool aEnabled) +{ + if (aEnabled) + { + VerifyOrExit(mState == kStateDisabled); + SetState(kStateStopped); + } + else + { + VerifyOrExit(mState != kStateDisabled); + Stop(); + SetState(kStateDisabled); + } + + Get().Refresh(); + +exit: + return; +} + +Error EphemeralKeyManager::Start(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort) +{ + Error error = kErrorNone; + uint16_t length; + + VerifyOrExit(mState == kStateStopped, error = kErrorInvalidState); + + length = StringLength(aKeyString, kMaxKeyLength + 1); + VerifyOrExit((length >= kMinKeyLength) && (length <= kMaxKeyLength), error = kErrorInvalidArgs); + + IgnoreError(mDtlsTransport.SetMaxConnectionAttempts(kMaxConnectionAttempts, HandleTransportClosed, this)); + + mDtlsTransport.SetAcceptCallback(EphemeralKeyManager::HandleAcceptSession, this); + mDtlsTransport.SetRemoveSessionCallback(EphemeralKeyManager::HandleRemoveSession, this); + + SuccessOrExit(error = mDtlsTransport.Open()); + SuccessOrExit(error = mDtlsTransport.Bind(aUdpPort)); + + SuccessOrExit( + error = mDtlsTransport.SetPsk(reinterpret_cast(aKeyString), static_cast(length))); + + aTimeout = Min((aTimeout == 0) ? kDefaultTimeout : aTimeout, kMaxTimeout); + mTimer.Start(aTimeout); + + LogInfo("Allow ephemeral key for %lu msec on port %u", ToUlong(aTimeout), GetUdpPort()); + + SetState(kStateStarted); + +exit: + switch (error) + { + case kErrorNone: + Get().mCounters.mEpskcActivations++; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcActivated); +#endif + break; + case kErrorInvalidState: + Get().mCounters.mEpskcInvalidBaStateErrors++; + break; + case kErrorInvalidArgs: + Get().mCounters.mEpskcInvalidArgsErrors++; + break; + default: + Get().mCounters.mEpskcStartSecureSessionErrors++; + break; + } + + return error; +} + +void EphemeralKeyManager::Stop(void) { Stop(kReasonLocalDisconnect); } + +void EphemeralKeyManager::Stop(DeactivationReason aReason) +{ + switch (mState) + { + case kStateStarted: + case kStateConnected: + case kStateAccepted: + break; + case kStateDisabled: + case kStateStopped: + ExitNow(); + } + + LogInfo("Stopping ephemeral key use - reason: %s", DeactivationReasonToString(aReason)); + SetState(kStateStopped); + + mTimer.Stop(); + mDtlsTransport.Close(); + + UpdateCountersAndRecordEvent(aReason); + +exit: + return; +} + +void EphemeralKeyManager::UpdateCountersAndRecordEvent(DeactivationReason aReason) +{ + struct ReasonToCounterEventEntry + { + DeactivationReason mReason; + uint8_t mEvent; // Raw values of `HistoryTracker::Local::Epskc` enum. + uint32_t Counters::*mCounterPtr; + }; + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE +#define ReasonEntry(kReason, kCounter, kEvent) \ + { \ + kReason, \ + HistoryTracker::Local::kEvent, \ + &Counters::kCounter, \ + } +#else +#define ReasonEntry(kReason, kCounter, kEvent) {kReason, 0, &Counters::kCounter} +#endif + + static const ReasonToCounterEventEntry kReasonToCounterEventEntries[] = { + ReasonEntry(kReasonLocalDisconnect, mEpskcDeactivationClears, kEpskcDeactivatedLocalClose), + ReasonEntry(kReasonSessionTimeout, mEpskcDeactivationClears, kEpskcDeactivatedSessionTimeout), + ReasonEntry(kReasonPeerDisconnect, mEpskcDeactivationDisconnects, kEpskcDeactivatedRemoteClose), + ReasonEntry(kReasonSessionError, mEpskcStartSecureSessionErrors, kEpskcDeactivatedSessionError), + ReasonEntry(kReasonMaxFailedAttempts, mEpskcDeactivationMaxAttempts, kEpskcDeactivatedMaxAttempts), + ReasonEntry(kReasonEpskcTimeout, mEpskcDeactivationTimeouts, kEpskcDeactivatedEpskcTimeout), + }; + +#undef ReasonEntry + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + HistoryTracker::EpskcEvent event = HistoryTracker::Local::kEpskcDeactivatedUnknown; +#endif + + for (const ReasonToCounterEventEntry &entry : kReasonToCounterEventEntries) + { + if (aReason == entry.mReason) + { + (Get().mCounters.*(entry.mCounterPtr))++; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + event = static_cast(entry.mEvent); +#endif + break; + } + } + +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordEpskcEvent(event); +#endif +} + +void EphemeralKeyManager::SetState(State aState) +{ +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + bool isServiceRegistered = ShouldRegisterService(); +#endif + + VerifyOrExit(mState != aState); + LogInfo("Ephemeral key - state: %s -> %s", StateToString(mState), StateToString(aState)); + mState = aState; + mCallbackTask.Post(); + +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + VerifyOrExit(isServiceRegistered != ShouldRegisterService()); + RegisterOrUnregisterService(); +#endif + +exit: + return; +} + +SecureSession *EphemeralKeyManager::HandleAcceptSession(void *aContext, const Ip6::MessageInfo &aMessageInfo) +{ + OT_UNUSED_VARIABLE(aMessageInfo); + + return static_cast(aContext)->HandleAcceptSession(); +} + +Manager::CoapDtlsSession *EphemeralKeyManager::HandleAcceptSession(void) +{ + CoapDtlsSession *session = nullptr; + + VerifyOrExit(mCoapDtlsSession == nullptr); + + session = CoapDtlsSession::Allocate(GetInstance(), mDtlsTransport); + VerifyOrExit(session != nullptr); + + mCoapDtlsSession = session; + +exit: + return session; +} + +void EphemeralKeyManager::HandleRemoveSession(void *aContext, SecureSession &aSession) +{ + static_cast(aContext)->HandleRemoveSession(aSession); +} + +void EphemeralKeyManager::HandleRemoveSession(SecureSession &aSession) +{ + CoapDtlsSession &coapSession = static_cast(aSession); + + coapSession.Cleanup(); + coapSession.Free(); + mCoapDtlsSession = nullptr; +} + +void EphemeralKeyManager::HandleSessionConnected(void) +{ + SetState(kStateConnected); + Get().mCounters.mEpskcSecureSessionSuccesses++; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcConnected); +#endif +} + +void EphemeralKeyManager::HandleSessionDisconnected(SecureSession::ConnectEvent aEvent) +{ + DeactivationReason reason = kReasonUnknown; + + // The ephemeral key can be used once + VerifyOrExit((mState == kStateConnected) || (mState == kStateAccepted)); + + switch (aEvent) + { + case SecureSession::kDisconnectedError: + reason = kReasonSessionError; + break; + case SecureSession::kDisconnectedPeerClosed: + reason = kReasonPeerDisconnect; + break; + case SecureSession::kDisconnectedMaxAttempts: + reason = kReasonMaxFailedAttempts; + break; + case SecureSession::kDisconnectedTimeout: + reason = kReasonSessionTimeout; + break; + default: + break; + } + + Stop(reason); + +exit: + return; +} + +void EphemeralKeyManager::HandleCommissionerPetitionAccepted(void) +{ + SetState(kStateAccepted); + Get().mCounters.mEpskcCommissionerPetitions++; +#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE + Get().RecordEpskcEvent(HistoryTracker::Local::kEpskcPetitioned); +#endif +} + +void EphemeralKeyManager::HandleTimer(void) { Stop(kReasonEpskcTimeout); } + +void EphemeralKeyManager::HandleTask(void) { mCallback.InvokeIfSet(); } + +void EphemeralKeyManager::HandleTransportClosed(void *aContext) +{ + reinterpret_cast(aContext)->HandleTransportClosed(); +} + +void EphemeralKeyManager::HandleTransportClosed(void) { Stop(kReasonMaxFailedAttempts); } + +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + +bool EphemeralKeyManager::ShouldRegisterService(void) const +{ + bool shouldRegister = false; + + switch (mState) + { + case kStateDisabled: + case kStateStopped: + break; + case kStateStarted: + case kStateConnected: + case kStateAccepted: + shouldRegister = true; + break; + } + + return shouldRegister; +} + +void EphemeralKeyManager::RegisterOrUnregisterService(void) +{ + Dnssd::Service service; + + VerifyOrExit(Get().IsReady()); + + service.Clear(); + service.mServiceInstance = Get().GetServiceName(); + service.mServiceType = kServiceType; + service.mPort = GetUdpPort(); + + if (ShouldRegisterService()) + { + Get().RegisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); + } + else + { + Get().UnregisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); + } + +exit: + return; +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + +const char *EphemeralKeyManager::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "Disabled", // (0) kStateDisabled + "Stopped", // (1) kStateStopped + "Started", // (2) kStateStarted + "Connected", // (3) kStateConnected + "Accepted", // (4) kStateAccepted + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + ValidateNextEnum(kStateDisabled); + ValidateNextEnum(kStateStopped); + ValidateNextEnum(kStateStarted); + ValidateNextEnum(kStateConnected); + ValidateNextEnum(kStateAccepted); + }; + + return kStateStrings[aState]; +} + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + +const char *EphemeralKeyManager::DeactivationReasonToString(DeactivationReason aReason) +{ + static const char *const kReasonStrings[] = { + "LocalDisconnect", // (0) kReasonLocalDisconnect + "PeerDisconnect", // (1) kReasonPeerDisconnect + "SessionError", // (2) kReasonSessionError + "SessionTimeout", // (3) kReasonSessionTimeout + "MaxFailedAttempts", // (4) kReasonMaxFailedAttempts + "EpskcTimeout", // (5) kReasonEpskcTimeout + "Unknown", // (6) kReasonUnknown + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + ValidateNextEnum(kReasonLocalDisconnect); + ValidateNextEnum(kReasonPeerDisconnect); + ValidateNextEnum(kReasonSessionError); + ValidateNextEnum(kReasonSessionTimeout); + ValidateNextEnum(kReasonMaxFailedAttempts); + ValidateNextEnum(kReasonEpskcTimeout); + ValidateNextEnum(kReasonUnknown); + }; + + return kReasonStrings[aReason]; +} + +#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + +} // namespace BorderAgent +} // namespace MeshCoP +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE diff --git a/src/core/meshcop/border_agent_ephemeral_key.hpp b/src/core/meshcop/border_agent_ephemeral_key.hpp new file mode 100644 index 000000000..6739873fa --- /dev/null +++ b/src/core/meshcop/border_agent_ephemeral_key.hpp @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions for the Border Agent Ephemeral Key Manager. + */ + +#ifndef BORDER_AGENT_EPHEMERAL_KEY_HPP_ +#define BORDER_AGENT_EPHEMERAL_KEY_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE + +#include + +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/tasklet.hpp" +#include "common/timer.hpp" +#include "meshcop/border_agent.hpp" +#include "meshcop/secure_transport.hpp" + +namespace ot { +namespace MeshCoP { +namespace BorderAgent { + +/** + * Manages the ephemeral key use by Border Agent. + */ +class EphemeralKeyManager : public InstanceLocator, private NonCopyable +{ + friend class Manager; + +public: + static constexpr uint16_t kMinKeyLength = OT_BORDER_AGENT_MIN_EPHEMERAL_KEY_LENGTH; ///< Min key len. + static constexpr uint16_t kMaxKeyLength = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_LENGTH; ///< Max key len. + static constexpr uint32_t kDefaultTimeout = OT_BORDER_AGENT_DEFAULT_EPHEMERAL_KEY_TIMEOUT; ///< Default timeout. + static constexpr uint32_t kMaxTimeout = OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT; ///< Max timeout. + + typedef otBorderAgentEphemeralKeyCallback CallbackHandler; ///< Callback function pointer. + + /** + * Represents the state of the `EphemeralKeyManager`. + */ + enum State : uint8_t + { + kStateDisabled = OT_BORDER_AGENT_STATE_DISABLED, ///< Ephemeral key feature is disabled. + kStateStopped = OT_BORDER_AGENT_STATE_STOPPED, ///< Enabled, but the key is not set and started. + kStateStarted = OT_BORDER_AGENT_STATE_STARTED, ///< Key is set and listening to accept connection. + kStateConnected = OT_BORDER_AGENT_STATE_CONNECTED, ///< Session connected, not full commissioner. + kStateAccepted = OT_BORDER_AGENT_STATE_ACCEPTED, ///< Session connected and accepted as full commissioner. + }; + + /** + * Initializes the `EphemeralKeyManager`. + * + * @param[in] aInstance The OpenThread instance. + */ + explicit EphemeralKeyManager(Instance &aInstance); + + /** + * Enables/disables Ephemeral Key Manager. + * + * If this method is called to disable, while an an ephemeral key is in use, the ephemeral key use will + * be stopped (as if `Stop()` is called). + * + * @param[in] aEnabled Whether to enable or disable. + */ + void SetEnabled(bool aEnabled); + + /** + * Starts using an ephemeral key for a given timeout duration. + * + * An ephemeral key can only be set when `GetState()` is `kStateStopped`. Otherwise, `kErrorInvalidState` is + * returned. This means that setting the ephemeral key again while a previously set key is still in use will + * fail. Callers can stop the previous key by calling `Stop()` before starting with a new key. + * + * The given @p aKeyString is used directly as the ephemeral PSK (excluding the trailing null `\0` character). + * Its length must be between `kMinKeyLength` and `kMaxKeyLength`, inclusive. + * + * The ephemeral key can be used only once by an external commissioner candidate to establish a secure session. + * After the commissioner candidate disconnects, the use of the ephemeral key is stopped. If the timeout + * expires, the use of the ephemeral key is also stopped, and any established session using the key is + * immediately disconnected. + * + * @param[in] aKeyString The ephemeral key. + * @param[in] aTimeout The timeout duration, in milliseconds, to use the ephemeral key. + * If zero, the default `kDefaultTimeout` value is used. If the timeout value is + * larger than `kMaxTimeout`, the maximum value is used instead. + * @param[in] aUdpPort The UDP port to use with the ephemeral key. If the UDP port is zero, an ephemeral + * port is used. `GetUdpPort()` returns the current UDP port being used. + * + * @retval kErrorNone Successfully started using the ephemeral key. + * @retval kErrorInvalidState A previously set ephemeral key is still in use or feature is disabled. + * @retval kErrorInvalidArgs The given @p aKeyString is not valid. + * @retval kErrorFailed Failed to start (e.g., it could not bind to the given UDP port). + */ + Error Start(const char *aKeyString, uint32_t aTimeout, uint16_t aUdpPort); + + /** + * Stops the ephemeral key use and disconnects any established secure session using it. + * + * If there is no ephemeral key in use, calling this method has no effect. + */ + void Stop(void); + + /** + * Gets the state of ephemeral key use and its session. + * + * @returns The `EphemeralKeyManager` state. + */ + State GetState(void) const { return mState; } + + /** + * Gets the UDP port used by ephemeral key DTLS secure transport. + * + * @returns UDP port number. + */ + uint16_t GetUdpPort(void) const { return mDtlsTransport.GetUdpPort(); } + + /** + * Sets the callback. + * + * @param[in] aCallback The callback function pointer. + * @param[in] aContext The context associated and used with callback handler. + */ + void SetCallback(CallbackHandler aCallback, void *aContext) { mCallback.Set(aCallback, aContext); } + + /** + * Converts a given `State` to human-readable string. + * + * @param[in] aState The state to convert. + * + * @returns The string corresponding to @p aState. + */ + static const char *StateToString(State aState); + +private: + static constexpr uint16_t kMaxConnectionAttempts = 10; + + static_assert(kMaxKeyLength <= Dtls::Transport::kPskMaxLength, "Max e-key len is larger than max PSK len"); + + using CoapDtlsSession = Manager::CoapDtlsSession; + using Counters = Manager::Counters; + + enum DeactivationReason : uint8_t + { + kReasonLocalDisconnect, + kReasonPeerDisconnect, + kReasonSessionError, + kReasonSessionTimeout, + kReasonMaxFailedAttempts, + kReasonEpskcTimeout, + kReasonUnknown, + }; + + void SetState(State aState); + void Stop(DeactivationReason aReason); + void HandleTimer(void); + void HandleTask(void); + bool OwnsSession(CoapDtlsSession &aSession) const { return mCoapDtlsSession == &aSession; } + void HandleSessionConnected(void); + void HandleSessionDisconnected(SecureSession::ConnectEvent aEvent); + void HandleCommissionerPetitionAccepted(void); + void UpdateCountersAndRecordEvent(DeactivationReason aReason); +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + bool ShouldRegisterService(void) const; + void RegisterOrUnregisterService(void); +#endif + + // Session or Transport callbacks + static SecureSession *HandleAcceptSession(void *aContext, const Ip6::MessageInfo &aMessageInfo); + CoapDtlsSession *HandleAcceptSession(void); + static void HandleRemoveSession(void *aContext, SecureSession &aSession); + void HandleRemoveSession(SecureSession &aSession); + static void HandleTransportClosed(void *aContext); + void HandleTransportClosed(void); + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + static const char *DeactivationReasonToString(DeactivationReason aReason); +#endif + +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + static const char kServiceType[]; +#endif + + using TimeoutTimer = TimerMilliIn; + using CallbackTask = TaskletIn; + + State mState; + Dtls::Transport mDtlsTransport; + CoapDtlsSession *mCoapDtlsSession; + TimeoutTimer mTimer; + CallbackTask mCallbackTask; + Callback mCallback; +}; + +} // namespace BorderAgent +} // namespace MeshCoP + +DefineMapEnum(otBorderAgentEphemeralKeyState, MeshCoP::BorderAgent::EphemeralKeyManager::State); + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE + +#endif // BORDER_AGENT_EPHEMERAL_KEY_HPP_