From 075f4f7d0e19e417fa9833b06d087b675350436a Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 9 Dec 2025 12:20:04 -0800 Subject: [PATCH] [border-agent] add Thread Admin One-Time Passcodes (TAP) APIs (#12188) This commit introduces new APIs to handle Thread Administration One-Time Passcodes (TAP). The new `otBorderAgentEphemeralKeyGenerateTap()` API generates a cryptographically secure 9-character TAP string. This consists of eight random numeric digits and a final check digit calculated using the Verhoeff algorithm for error detection. The corresponding `otBorderAgentEphemeralKeyValidateTap()` API validates a given TAP string by checking its length, ensuring it contains only digits, and verifying the Verhoeff checksum. A new test is added to ensure the correctness of both the generation and validation logic, covering success and failure scenarios. --- include/openthread/border_agent.h | 44 ++++++++++++ include/openthread/instance.h | 2 +- src/core/api/border_agent_api.cpp | 14 ++++ .../meshcop/border_agent_ephemeral_key.cpp | 62 ++++++++++++++++ .../meshcop/border_agent_ephemeral_key.hpp | 40 +++++++++++ tests/nexus/openthread-core-nexus-config.h | 1 + tests/nexus/test_border_agent.cpp | 70 +++++++++++++++++++ 7 files changed, 232 insertions(+), 1 deletion(-) diff --git a/include/openthread/border_agent.h b/include/openthread/border_agent.h index 415615d52..952a405d2 100644 --- a/include/openthread/border_agent.h +++ b/include/openthread/border_agent.h @@ -423,6 +423,11 @@ otError otBorderAgentEvictActiveCommissioner(otInstance *aInstance); */ #define OT_BORDER_AGENT_MAX_EPHEMERAL_KEY_TIMEOUT (10 * 60 * 1000u) +/** + * The string length of Thread Administration One-Time Passcode (TAP). + */ +#define OT_BORDER_AGENT_EPHEMERAL_KEY_TAP_STRING_LENGTH 9 + /** * Represents Border Agent's Ephemeral Key Manager state. */ @@ -435,6 +440,14 @@ typedef enum otBorderAgentEphemeralKeyState OT_BORDER_AGENT_STATE_ACCEPTED = 4, ///< Session is established and candidate is accepted as full commissioner. } otBorderAgentEphemeralKeyState; +/** + * Represents a Thread Administration One-Time Passcode (TAP). + */ +typedef struct otBorderAgentEphemeralKeyTap +{ + char mTap[OT_BORDER_AGENT_EPHEMERAL_KEY_TAP_STRING_LENGTH + 1]; ///< TAP string buffer (including `\0` character). +} otBorderAgentEphemeralKeyTap; + /** * Gets the state of Border Agent's Ephemeral Key Manager. * @@ -563,6 +576,37 @@ void otBorderAgentEphemeralKeySetCallback(otInstance *aIns */ const char *otBorderAgentEphemeralKeyStateToString(otBorderAgentEphemeralKeyState aState); +/** + * Generates a cryptographically secure random Thread Administration One-Time Passcode (TAP) string. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE` and `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`. + * + * The TAP is a string of 9 characters, generated as a sequence of eight cryptographically secure random + * numeric digits [`0`-`9`] followed by a single check digit determined using the Verhoeff algorithm. + * + * @param[out] aTap A pointer to an `otBorderAgentEphemeralKeyTap` to output the generated TAP. + * + * @retval OT_ERROR_NONE Successfully generated a random TAP. @p aTap is updated. + * @retval OT_ERROR_FAILED Failed to generate a random TAP. + */ +otError otBorderAgentEphemeralKeyGenerateTap(otBorderAgentEphemeralKeyTap *aTap); + +/** + * Validates a given Thread Administration One-Time Passcode (TAP) string. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE` and `OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE`. + * + * Validates that the TAP string has the proper length, contains digit characters [`0`-`9`], and validates the + * Verhoeff checksum. + * + * @param[in] aTap The `otBorderAgentEphemeralKeyTap` to validate. + * + * @retval OT_ERROR_NONE Successfully validated the @p aTap. + * @retval OT_ERROR_INVALID_ARGS The @p aTap string has an invalid length or contains non-digit characters. + * @retval OT_ERROR_FAILED Checksum validation failed. + */ +otError otBorderAgentEphemeralKeyValidateTap(const otBorderAgentEphemeralKeyTap *aTap); + /** * @} */ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index c724f8f4f..c58f5f8b4 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (558) +#define OPENTHREAD_API_VERSION (559) /** * @addtogroup api-instance diff --git a/src/core/api/border_agent_api.cpp b/src/core/api/border_agent_api.cpp index 2a492efa7..2dde67c70 100644 --- a/src/core/api/border_agent_api.cpp +++ b/src/core/api/border_agent_api.cpp @@ -172,6 +172,20 @@ const char *otBorderAgentEphemeralKeyStateToString(otBorderAgentEphemeralKeyStat return MeshCoP::BorderAgent::EphemeralKeyManager::StateToString(MapEnum(aState)); } +#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE + +otError otBorderAgentEphemeralKeyGenerateTap(otBorderAgentEphemeralKeyTap *aTap) +{ + return AsCoreType(aTap).GenerateRandom(); +} + +otError otBorderAgentEphemeralKeyValidateTap(const otBorderAgentEphemeralKeyTap *aTap) +{ + return AsCoreType(aTap).Validate(); +} + +#endif + #endif // OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE #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 index ec1962ed0..a9d426fa3 100644 --- a/src/core/meshcop/border_agent_ephemeral_key.cpp +++ b/src/core/meshcop/border_agent_ephemeral_key.cpp @@ -36,6 +36,7 @@ #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE #include "instance/instance.hpp" +#include "utils/verhoeff_checksum.hpp" namespace ot { namespace MeshCoP { @@ -428,6 +429,67 @@ const char *EphemeralKeyManager::DeactivationReasonToString(DeactivationReason a #endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) +//--------------------------------------------------------------------------------------------------------------------- +// EphemeralKeyManager::Tap + +#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE + +Error EphemeralKeyManager::Tap::GenerateRandom(void) +{ + Error error; + char checksum; + + ClearAllBytes(mTap); + + for (uint8_t index = 0; index < kLength - 1; index++) + { + SuccessOrExit(error = GenerateRandomDigit(mTap[index])); + } + + IgnoreError(Utils::VerhoeffChecksum::Calculate(mTap, checksum)); + mTap[kLength - 1] = checksum; + +exit: + return error; +} + +Error EphemeralKeyManager::Tap::GenerateRandomDigit(char &aChar) +{ + static constexpr uint8_t kMaxValue = 250; + + Error error; + uint8_t byte; + + // To ensure uniform random distribution and avoid bias toward + // certain digit values, we ignore random `uint8` values of 250 or + // larger (i.e., values in the range [250-255]). This ensures the + // random `byte` is uniformly distributed in `[0-249]`, which, + // when `% 10`, gives us a uniform probability of `[0-9]` values. + + do + { + SuccessOrExit(error = Random::Crypto::Fill(byte)); + } while (byte >= kMaxValue); + + aChar = '0' + (byte % 10); + +exit: + return error; +} + +Error EphemeralKeyManager::Tap::Validate(void) const +{ + Error error; + + VerifyOrExit(StringLength(mTap, kLength + 1) == kLength, error = kErrorInvalidArgs); + error = Utils::VerhoeffChecksum::Validate(mTap); + +exit: + return error; +} + +#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE + } // namespace BorderAgent } // namespace MeshCoP } // namespace ot diff --git a/src/core/meshcop/border_agent_ephemeral_key.hpp b/src/core/meshcop/border_agent_ephemeral_key.hpp index 6739873fa..16cdcf1bb 100644 --- a/src/core/meshcop/border_agent_ephemeral_key.hpp +++ b/src/core/meshcop/border_agent_ephemeral_key.hpp @@ -78,6 +78,43 @@ public: kStateAccepted = OT_BORDER_AGENT_STATE_ACCEPTED, ///< Session connected and accepted as full commissioner. }; +#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE + /** + * Represents a Thread Administration One-Time Passcode (TAP). + */ + class Tap : public otBorderAgentEphemeralKeyTap + { + public: + static constexpr uint8_t kLength = OT_BORDER_AGENT_EPHEMERAL_KEY_TAP_STRING_LENGTH; ///< TAP string length. + + /** + * Generates a cryptographically secure random Thread Administration One-Time Passcode (TAP) string. + * + * The TAP is a string of 9 characters, generated as a sequence of eight cryptographically secure random + * numeric digits [`0`-`9`] followed by a single check digit determined using the Verhoeff algorithm. + * + * @retval kErrorNone Successfully generated a random TAP. + * @retval kErrorFailed Failed to generate a random TAP. + */ + Error GenerateRandom(void); + + /** + * Validates a given Thread Administration One-Time Passcode (TAP) string. + * + * Validates that the TAP string has the proper length, contains digit characters [`0`-`9`], and validates the + * Verhoeff checksum. + * + * @retval kErrorNone Successfully validated the TAP. + * @retval kErrorInvalidArgs The TAP string has an invalid length or contains non-digit characters. + * @retval kErrorFailed Checksum validation failed. + */ + Error Validate(void) const; + + private: + Error GenerateRandomDigit(char &aChar); + }; +#endif // OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE + /** * Initializes the `EphemeralKeyManager`. * @@ -226,6 +263,9 @@ private: } // namespace MeshCoP DefineMapEnum(otBorderAgentEphemeralKeyState, MeshCoP::BorderAgent::EphemeralKeyManager::State); +#if OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE +DefineCoreType(otBorderAgentEphemeralKeyTap, MeshCoP::BorderAgent::EphemeralKeyManager::Tap); +#endif } // namespace ot diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 3194e98ce..7817c019d 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -135,6 +135,7 @@ #define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 1 #define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_UPTIME_ENABLE 1 +#define OPENTHREAD_CONFIG_VERHOEFF_CHECKSUM_ENABLE 1 // CLI configs #define OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH 800 diff --git a/tests/nexus/test_border_agent.cpp b/tests/nexus/test_border_agent.cpp index b5458d1d2..151618dd5 100644 --- a/tests/nexus/test_border_agent.cpp +++ b/tests/nexus/test_border_agent.cpp @@ -757,6 +757,75 @@ void TestBorderAgentEphemeralKey(void) VerifyOrQuit(node0.Get().GetCounters().mEpskcInvalidArgsErrors == 2); } +void TestBorderAgentEphemeralKeyTapGeneration(void) +{ + static constexpr uint16_t kMaxRounds = 20; + + using Tap = EphemeralKeyManager::Tap; + + Core nexus; + Node &node = nexus.CreateNode(); + Tap tap; + uint8_t index; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAgentEphemeralKeyTapGeneration"); + + nexus.AdvanceTime(0); + + node.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + VerifyOrQuit(node.Get().IsLeader()); + + for (uint16_t round = 0; round < kMaxRounds; round++) + { + SuccessOrQuit(tap.GenerateRandom()); + SuccessOrQuit(tap.Validate()); + Log("Generated random TAP: %s", tap.mTap); + + // Tamper with one of the digits and ensure checksum fails. + + index = round % Tap::kLength; + + tap.mTap[index]++; + + if (tap.mTap[index] > '9') + { + tap.mTap[index] = '0'; + } + + VerifyOrQuit(tap.Validate() == kErrorFailed); + } + + // CHeck valid TAP strings (Thread spec (1.4.1d3) - section 8.4.9.7) + + SuccessOrQuit(StringCopy(tap.mTap, "903723159")); + SuccessOrQuit(tap.Validate()); + + SuccessOrQuit(StringCopy(tap.mTap, "746351983")); + SuccessOrQuit(tap.Validate()); + + // Check invalid TAP strings. + + SuccessOrQuit(StringCopy(tap.mTap, "")); + VerifyOrQuit(tap.Validate() == kErrorInvalidArgs); + + SuccessOrQuit(StringCopy(tap.mTap, "1234")); + VerifyOrQuit(tap.Validate() == kErrorInvalidArgs); + + SuccessOrQuit(StringCopy(tap.mTap, "12345678")); + VerifyOrQuit(tap.Validate() == kErrorInvalidArgs); + + SuccessOrQuit(StringCopy(tap.mTap, "123456789")); + VerifyOrQuit(tap.Validate() == kErrorFailed); + + SuccessOrQuit(StringCopy(tap.mTap, "a23456789")); + VerifyOrQuit(tap.Validate() == kErrorInvalidArgs); + + SuccessOrQuit(StringCopy(tap.mTap, "12345678A")); + VerifyOrQuit(tap.Validate() == kErrorInvalidArgs); +} + EpskcEvent GetNewestEpskcEvent(Node &aNode) { const EpskcEvent *epskcEvent = nullptr; @@ -1979,6 +2048,7 @@ int main(void) { ot::Nexus::TestBorderAgent(); ot::Nexus::TestBorderAgentEphemeralKey(); + ot::Nexus::TestBorderAgentEphemeralKeyTapGeneration(); ot::Nexus::TestHistoryTrackerBorderAgentEpskcEvent(); ot::Nexus::TestBorderAgentTxtDataCallback(); ot::Nexus::TestBorderAgentServiceRegistration();