[tcat] implement rate limitation for TCAT TLVs 0x10, 0x11 and 0x12 and remove TLV 0x14 (#12211)

This commit implements rate limitation for the TCAT commands Present
PSKd Hash TLV (0x10), Present PSKc Hash TLV (0x11) and Present
Install-code Hash TLV (0x12) to prevent password guessing attacks.

It also removes the TCAT command Request PSKd Hash TLV (0x14), to
prevent offline password guessing attacks with a single Hash value
retrieved from the device.

Note: The commit does not remove the Request PSKd Hash TLV
implementation in the Python commissioner such that the non-existence
of the command TLV can still be tested.
This commit is contained in:
arnulfrupp
2026-05-04 16:10:19 +02:00
committed by GitHub
parent d27c618ccb
commit 928c78a01b
6 changed files with 58 additions and 90 deletions
+14 -31
View File
@@ -60,8 +60,10 @@ TcatAgent::TcatAgent(Instance &aInstance)
, mTimerSetsToActive(false) , mTimerSetsToActive(false)
, mActiveOrStandbyTimer(aInstance) , mActiveOrStandbyTimer(aInstance)
, mTcatActiveDurationMs(0) , mTcatActiveDurationMs(0)
, mHashVerificationAttempts(1)
{ {
ClearCommissionerState(); ClearCommissionerState();
mLastHashVerificationTimestamp = Get<UptimeTracker>().GetUptimeInSeconds();
} }
void TcatAgent::ClearCommissionerState(void) void TcatAgent::ClearCommissionerState(void)
@@ -496,10 +498,6 @@ Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutg
error = HandleRequestRandomNumberChallenge(aOutgoingMessage, response); error = HandleRequestRandomNumberChallenge(aOutgoingMessage, response);
break; break;
case kTlvRequestPskdHash:
error = HandleRequestPskdHash(aIncomingMessage, aOutgoingMessage, offset, length, response);
break;
case kTlvGetCommissionerCertificate: case kTlvGetCommissionerCertificate:
error = HandleGetCommissionerCertificate(aOutgoingMessage, response); error = HandleGetCommissionerCertificate(aOutgoingMessage, response);
break; break;
@@ -883,33 +881,6 @@ exit:
return error; return error;
} }
Error TcatAgent::HandleRequestPskdHash(const Message &aIncomingMessage,
Message &aOutgoingMessage,
uint16_t aOffset,
uint16_t aLength,
bool &aResponse)
{
Error error = kErrorNone;
uint64_t providedChallenge = 0;
Crypto::HmacSha256::Hash hash;
VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
VerifyOrExit(StringLength(mVendorInfo->mPskdString, kMaxPskdLength) != 0, error = kErrorFailed);
VerifyOrExit(aLength == sizeof(providedChallenge), error = kErrorParse);
SuccessOrExit(error = aIncomingMessage.Read(aOffset, &providedChallenge, aLength));
SuccessOrExit(error = CalculateHash(providedChallenge, mVendorInfo->mPskdString,
StringLength(mVendorInfo->mPskdString, kMaxPskdLength), hash));
SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, hash.GetBytes(),
Crypto::HmacSha256::Hash::kSize));
aResponse = true;
exit:
return error;
}
Error TcatAgent::VerifyHash(const Message &aIncomingMessage, Error TcatAgent::VerifyHash(const Message &aIncomingMessage,
uint16_t aOffset, uint16_t aOffset,
uint16_t aLength, uint16_t aLength,
@@ -918,14 +889,26 @@ Error TcatAgent::VerifyHash(const Message &aIncomingMessage,
{ {
Error error = kErrorNone; Error error = kErrorNone;
Crypto::HmacSha256::Hash hash; Crypto::HmacSha256::Hash hash;
UptimeSec currentTime = Get<UptimeTracker>().GetUptimeInSeconds();
uint32_t newAttempts = (currentTime - mLastHashVerificationTimestamp) / kHashVerificationAttemptTime;
uint32_t totalAttempts = newAttempts + mHashVerificationAttempts;
VerifyOrExit(aLength == Crypto::HmacSha256::Hash::kSize, error = kErrorSecurity); VerifyOrExit(aLength == Crypto::HmacSha256::Hash::kSize, error = kErrorSecurity);
VerifyOrExit(mRandomChallenge != 0, error = kErrorSecurity); VerifyOrExit(mRandomChallenge != 0, error = kErrorSecurity);
// In case uptime has overflowed (will never happen in practical functional operational life), up to
// kHashVerificationMaxAttempts additional attempts can be tolerated.
mHashVerificationAttempts = static_cast<uint8_t>(Min<uint32_t>(totalAttempts, kHashVerificationMaxAttempts));
VerifyOrExit(mHashVerificationAttempts > 0, error = kErrorBusy);
mLastHashVerificationTimestamp = currentTime;
mHashVerificationAttempts--;
SuccessOrExit(error = CalculateHash(mRandomChallenge, reinterpret_cast<const char *>(aBuf), aBufLen, hash)); SuccessOrExit(error = CalculateHash(mRandomChallenge, reinterpret_cast<const char *>(aBuf), aBufLen, hash));
DumpDebg("Hash", &hash, sizeof(hash)); DumpDebg("Hash", &hash, sizeof(hash));
VerifyOrExit(aIncomingMessage.Compare(aOffset, hash), error = kErrorSecurity); VerifyOrExit(aIncomingMessage.Compare(aOffset, hash), error = kErrorSecurity);
mHashVerificationAttempts++;
exit: exit:
return error; return error;
+19 -16
View File
@@ -38,6 +38,10 @@
#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
#if !OPENTHREAD_CONFIG_UPTIME_ENABLE
#error "OPENTHREAD_CONFIG_UPTIME_ENABLE is required for TCAT agent"
#endif
#include <openthread/netdiag.h> #include <openthread/netdiag.h>
#include <openthread/tcat.h> #include <openthread/tcat.h>
#include <openthread/platform/ble.h> #include <openthread/platform/ble.h>
@@ -48,6 +52,7 @@
#include "common/log.hpp" #include "common/log.hpp"
#include "common/message.hpp" #include "common/message.hpp"
#include "common/non_copyable.hpp" #include "common/non_copyable.hpp"
#include "common/uptime.hpp"
#include "mac/mac_types.hpp" #include "mac/mac_types.hpp"
#include "meshcop/dataset.hpp" #include "meshcop/dataset.hpp"
#include "meshcop/meshcop.hpp" #include "meshcop/meshcop.hpp"
@@ -172,7 +177,6 @@ public:
kTlvPresentPskcHash = 0x11, ///< TCAT commissioner rights elevation request TLV using PSKc hash kTlvPresentPskcHash = 0x11, ///< TCAT commissioner rights elevation request TLV using PSKc hash
kTlvPresentInstallCodeHash = 0x12, ///< TCAT commissioner rights elevation request TLV using install code kTlvPresentInstallCodeHash = 0x12, ///< TCAT commissioner rights elevation request TLV using install code
kTlvRequestRandomNumChallenge = 0x13, ///< TCAT random number challenge query TLV kTlvRequestRandomNumChallenge = 0x13, ///< TCAT random number challenge query TLV
kTlvRequestPskdHash = 0x14, ///< TCAT PSKd hash request TLV
// Command Class Commissioning // Command Class Commissioning
kTlvSetActiveOperationalDataset = 0x20, ///< TCAT active operational dataset TLV kTlvSetActiveOperationalDataset = 0x20, ///< TCAT active operational dataset TLV
@@ -463,11 +467,6 @@ private:
Error HandlePresentPskcHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength); Error HandlePresentPskcHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength);
Error HandlePresentInstallCodeHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength); Error HandlePresentInstallCodeHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength);
Error HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &aResponse); Error HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &aResponse);
Error HandleRequestPskdHash(const Message &aIncomingMessage,
Message &aOutgoingMessage,
uint16_t aOffset,
uint16_t aLength,
bool &aResponse);
Error HandleStartThreadInterface(void); Error HandleStartThreadInterface(void);
Error HandleStopThreadInterface(void); Error HandleStopThreadInterface(void);
Error HandleGetCommissionerCertificate(Message &aOutgoingMessage, bool &aResponse); Error HandleGetCommissionerCertificate(Message &aOutgoingMessage, bool &aResponse);
@@ -490,16 +489,18 @@ private:
const Dataset *aCommSuppliedDataset) const; const Dataset *aCommSuppliedDataset) const;
uint8_t CheckAuthorizationRequirements(CommandClassFlags aFlagsChecked, Dataset::Info *aActiveDatasetInfo) const; uint8_t CheckAuthorizationRequirements(CommandClassFlags aFlagsChecked, Dataset::Info *aActiveDatasetInfo) const;
static constexpr uint16_t kPingPayloadMaxLength = 512; static constexpr uint16_t kPingPayloadMaxLength = 512;
static constexpr uint16_t kProvisioningUrlMaxLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_APP_URL_TLV_LENGTH; static constexpr uint16_t kProvisioningUrlMaxLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_APP_URL_TLV_LENGTH;
static constexpr uint16_t kMaxPskdLength = OT_JOINER_MAX_PSKD_LENGTH; static constexpr uint16_t kMaxPskdLength = OT_JOINER_MAX_PSKD_LENGTH;
static constexpr uint16_t kTcatMaxDeviceIdSize = OT_TCAT_MAX_DEVICEID_SIZE; static constexpr uint16_t kTcatMaxDeviceIdSize = OT_TCAT_MAX_DEVICEID_SIZE;
static constexpr uint16_t kInstallCodeMaxSize = 255; static constexpr uint16_t kInstallCodeMaxSize = 255;
static constexpr uint16_t kCommissionerCertMaxLength = 1024; static constexpr uint16_t kCommissionerCertMaxLength = 1024;
static constexpr uint16_t kBufferReserve = 2048 / (Buffer::kSize - sizeof(otMessageBuffer)) + 1; static constexpr uint16_t kBufferReserve = 2048 / (Buffer::kSize - sizeof(otMessageBuffer)) + 1;
static constexpr uint8_t kServiceNameMaxLength = OT_TCAT_SERVICE_NAME_MAX_LENGTH; static constexpr uint8_t kServiceNameMaxLength = OT_TCAT_SERVICE_NAME_MAX_LENGTH;
static constexpr uint8_t kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT; static constexpr uint8_t kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT;
static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX; static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX;
static constexpr uint32_t kHashVerificationAttemptTime = 5;
static constexpr uint8_t kHashVerificationMaxAttempts = 10;
const VendorInfo *mVendorInfo; const VendorInfo *mVendorInfo;
Callback<JoinCallback> mJoinCallback; Callback<JoinCallback> mJoinCallback;
@@ -524,6 +525,8 @@ private:
using ExpireTimer = TimerMilliIn<TcatAgent, &TcatAgent::HandleTimer>; using ExpireTimer = TimerMilliIn<TcatAgent, &TcatAgent::HandleTimer>;
ExpireTimer mActiveOrStandbyTimer; ExpireTimer mActiveOrStandbyTimer;
uint32_t mTcatActiveDurationMs; uint32_t mTcatActiveDurationMs;
UptimeSec mLastHashVerificationTimestamp;
uint8_t mHashVerificationAttempts;
}; };
DeclareTmfHandler(TcatAgent, kUriTcatEnable); DeclareTmfHandler(TcatAgent, kUriTcatEnable);
+24 -5
View File
@@ -31,6 +31,9 @@ source "tests/scripts/expect/_common.exp"
spawn_node 1 "cli" spawn_node 1 "cli"
# Sleep > 50 seconds to ensure the maximum number of unsuccessful hash verification attempts (10) are permitted
sleep 51
spawn_tcat_client_for_node 1 tools/tcat_ble_client/auth-cert/CommCert2 spawn_tcat_client_for_node 1 tools/tcat_ble_client/auth-cert/CommCert2
send "commission\n" send "commission\n"
@@ -41,11 +44,6 @@ send "random_challenge\n"
expect_line "\tTYPE:\tRESPONSE_W_PAYLOAD" expect_line "\tTYPE:\tRESPONSE_W_PAYLOAD"
expect_line "\tLEN:\t8" expect_line "\tLEN:\t8"
send "peer_pskd_hash JJJJJJ\n"
expect_line "Requested hash is valid."
expect_line "\tTYPE:\tRESPONSE_W_PAYLOAD"
expect_line "\tLEN:\t32"
send "present_hash pskd AAAA\n" send "present_hash pskd AAAA\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS" expect_line "\tTYPE:\tRESPONSE_W_STATUS"
expect_line "\tVALUE:\t0x07" expect_line "\tVALUE:\t0x07"
@@ -74,6 +72,27 @@ send "present_hash pskc aaaa\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS" expect_line "\tTYPE:\tRESPONSE_W_STATUS"
expect_line "\tVALUE:\t0x07" expect_line "\tVALUE:\t0x07"
# Sleep >> 5 seconds to ensure a few more hash verification attempts are allowed
sleep 20
# Try hash verification >10 times to enforce rate limitation
for {set i 0} {$i < 11} {incr i} {
send "present_hash pskd AAAA\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS"
}
send "present_hash pskd AAAA\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS"
expect_line "\tVALUE:\t0x05"
# Sleep >5 seconds to ensure one more hash verification attempt is allowed
sleep 5.5
send "present_hash pskd AAAA\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS"
expect_line "\tVALUE:\t0x07"
dispose_tcat_client 1 dispose_tcat_client 1
switch_node 1 switch_node 1
@@ -324,41 +324,6 @@ class GetNetworkNameCommand(BleCommand):
return TLV(TcatTLVType.GET_NETWORK_NAME.value, bytes()).to_bytes() return TLV(TcatTLVType.GET_NETWORK_NAME.value, bytes()).to_bytes()
class GetPskdHash(BleCommand):
def __init__(self):
super().__init__()
self.digest = None
def get_log_string(self) -> str:
return 'Retrieving peer PSKd hash.'
def get_help_string(self) -> str:
return 'Get calculated PSKd hash.'
def prepare_data(self, args, context) -> bytes:
bless: BleStreamSecure = context['ble_sstream']
if bless.peer_public_key is None:
raise DataNotPrepared("Peer certificate not present.")
challenge = token_bytes(CHALLENGE_SIZE)
pskd = bytes(args[0], 'utf-8')
data = TLV(TcatTLVType.GET_PSKD_HASH.value, challenge).to_bytes()
hash = hmac.new(pskd, digestmod=sha256)
hash.update(challenge)
hash.update(bless.peer_public_key)
self.digest = hash.digest()
return data
def process_response(self, tlv_response, context) -> None:
if tlv_response.value == self.digest:
print('Requested hash is valid.')
else:
print('Requested hash is NOT valid.')
class GetRandomNumberChallenge(BleCommand): class GetRandomNumberChallenge(BleCommand):
def get_log_string(self) -> str: def get_log_string(self) -> str:
+1 -2
View File
@@ -33,7 +33,7 @@ import shlex
from typing import Optional from typing import Optional
from cli.base_commands import (DisconnectCommand, HelpCommand, HelloCommand, CommissionCommand, DecommissionCommand, from cli.base_commands import (DisconnectCommand, HelpCommand, HelloCommand, CommissionCommand, DecommissionCommand,
ExtractDatasetCommand, GetCommissionerCertificate, GetDeviceIdCommand, GetPskdHash, ExtractDatasetCommand, GetCommissionerCertificate, GetDeviceIdCommand,
GetExtPanIDCommand, GetNetworkNameCommand, GetProvisioningUrlCommand, PingCommand, GetExtPanIDCommand, GetNetworkNameCommand, GetProvisioningUrlCommand, PingCommand,
GetRandomNumberChallenge, ThreadStateCommand, ScanCommand, PresentHash, GetRandomNumberChallenge, ThreadStateCommand, ScanCommand, PresentHash,
DiagnosticTlvsCommand, GetApplicationLayersCommand, SendVendorData, DiagnosticTlvsCommand, GetApplicationLayersCommand, SendVendorData,
@@ -74,7 +74,6 @@ class CLI:
'simulation': SimulationCommand(), 'simulation': SimulationCommand(),
'random_challenge': GetRandomNumberChallenge(), 'random_challenge': GetRandomNumberChallenge(),
'present_hash': PresentHash(), 'present_hash': PresentHash(),
'peer_pskd_hash': GetPskdHash(),
'tlv': TlvCommand(), 'tlv': TlvCommand(),
'get_comm_cert': GetCommissionerCertificate(), 'get_comm_cert': GetCommissionerCertificate(),
'diagnostic_tlvs': DiagnosticTlvsCommand() 'diagnostic_tlvs': DiagnosticTlvsCommand()
-1
View File
@@ -42,7 +42,6 @@ class TcatTLVType(Enum):
PRESENT_PSKC_HASH = 0x11 PRESENT_PSKC_HASH = 0x11
PRESENT_INSTALL_CODE_HASH = 0x12 PRESENT_INSTALL_CODE_HASH = 0x12
GET_RANDOM_NUMBER_CHALLENGE = 0x13 GET_RANDOM_NUMBER_CHALLENGE = 0x13
GET_PSKD_HASH = 0x14
ACTIVE_DATASET = 0x20 ACTIVE_DATASET = 0x20
GET_COMMISSIONER_CERTIFICATE = 0x25 GET_COMMISSIONER_CERTIFICATE = 0x25
GET_ACTIVE_DATASET = 0x40 GET_ACTIVE_DATASET = 0x40