[border-agent] move EphemeralKeyManager to its own files (#12138)

Moves `EphemeralKeyManager` class and its implementation from
`border_agent.hpp` and `border_agent.cpp` to their own separate files
`border_agent_ephemeral_key.hpp/cpp`.

This is a structural change to improve code organization and does not
introduce any functional changes.
This commit is contained in:
Abtin Keshavarzian
2025-11-17 12:52:25 -08:00
committed by GitHub
parent bb21e0af00
commit b03053b485
7 changed files with 673 additions and 570 deletions
+2
View File
@@ -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",
+1
View File
@@ -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
+1
View File
@@ -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"
-392
View File
@@ -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<TxtData>().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<const uint8_t *>(aKeyString), static_cast<uint8_t>(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<Manager>().mCounters.mEpskcActivations++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcActivated);
#endif
break;
case kErrorInvalidState:
Get<Manager>().mCounters.mEpskcInvalidBaStateErrors++;
break;
case kErrorInvalidArgs:
Get<Manager>().mCounters.mEpskcInvalidArgsErrors++;
break;
default:
Get<Manager>().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<Manager>().mCounters.*(entry.mCounterPtr))++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
event = static_cast<HistoryTracker::EpskcEvent>(entry.mEvent);
#endif
break;
}
}
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().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<EphemeralKeyManager *>(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<EphemeralKeyManager *>(aContext)->HandleRemoveSession(aSession);
}
void EphemeralKeyManager::HandleRemoveSession(SecureSession &aSession)
{
CoapDtlsSession &coapSession = static_cast<CoapDtlsSession &>(aSession);
coapSession.Cleanup();
coapSession.Free();
mCoapDtlsSession = nullptr;
}
void EphemeralKeyManager::HandleSessionConnected(void)
{
SetState(kStateConnected);
Get<Manager>().mCounters.mEpskcSecureSessionSuccesses++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().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<Manager>().mCounters.mEpskcCommissionerPetitions++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcPetitioned);
#endif
}
void EphemeralKeyManager::HandleTimer(void) { Stop(kReasonEpskcTimeout); }
void EphemeralKeyManager::HandleTask(void) { mCallback.InvokeIfSet(); }
void EphemeralKeyManager::HandleTransportClosed(void *aContext)
{
reinterpret_cast<EphemeralKeyManager *>(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<Dnssd>().IsReady());
service.Clear();
service.mServiceInstance = Get<Manager>().GetServiceName();
service.mServiceType = kServiceType;
service.mPort = GetUdpPort();
if (ShouldRegisterService())
{
Get<Dnssd>().RegisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr);
}
else
{
Get<Dnssd>().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
-178
View File
@@ -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<EphemeralKeyManager, &EphemeralKeyManager::HandleTimer>;
using CallbackTask = TaskletIn<EphemeralKeyManager, &EphemeralKeyManager::HandleTask>;
State mState;
Dtls::Transport mDtlsTransport;
CoapDtlsSession *mCoapDtlsSession;
TimeoutTimer mTimer;
CallbackTask mCallbackTask;
Callback<CallbackHandler> 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
@@ -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<TxtData>().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<const uint8_t *>(aKeyString), static_cast<uint8_t>(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<Manager>().mCounters.mEpskcActivations++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcActivated);
#endif
break;
case kErrorInvalidState:
Get<Manager>().mCounters.mEpskcInvalidBaStateErrors++;
break;
case kErrorInvalidArgs:
Get<Manager>().mCounters.mEpskcInvalidArgsErrors++;
break;
default:
Get<Manager>().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<Manager>().mCounters.*(entry.mCounterPtr))++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
event = static_cast<HistoryTracker::EpskcEvent>(entry.mEvent);
#endif
break;
}
}
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().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<EphemeralKeyManager *>(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<EphemeralKeyManager *>(aContext)->HandleRemoveSession(aSession);
}
void EphemeralKeyManager::HandleRemoveSession(SecureSession &aSession)
{
CoapDtlsSession &coapSession = static_cast<CoapDtlsSession &>(aSession);
coapSession.Cleanup();
coapSession.Free();
mCoapDtlsSession = nullptr;
}
void EphemeralKeyManager::HandleSessionConnected(void)
{
SetState(kStateConnected);
Get<Manager>().mCounters.mEpskcSecureSessionSuccesses++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().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<Manager>().mCounters.mEpskcCommissionerPetitions++;
#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcPetitioned);
#endif
}
void EphemeralKeyManager::HandleTimer(void) { Stop(kReasonEpskcTimeout); }
void EphemeralKeyManager::HandleTask(void) { mCallback.InvokeIfSet(); }
void EphemeralKeyManager::HandleTransportClosed(void *aContext)
{
reinterpret_cast<EphemeralKeyManager *>(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<Dnssd>().IsReady());
service.Clear();
service.mServiceInstance = Get<Manager>().GetServiceName();
service.mServiceType = kServiceType;
service.mPort = GetUdpPort();
if (ShouldRegisterService())
{
Get<Dnssd>().RegisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr);
}
else
{
Get<Dnssd>().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
@@ -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 <openthread/border_agent.h>
#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<EphemeralKeyManager, &EphemeralKeyManager::HandleTimer>;
using CallbackTask = TaskletIn<EphemeralKeyManager, &EphemeralKeyManager::HandleTask>;
State mState;
Dtls::Transport mDtlsTransport;
CoapDtlsSession *mCoapDtlsSession;
TimeoutTimer mTimer;
CallbackTask mCallbackTask;
Callback<CallbackHandler> 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_