[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)
, mActiveOrStandbyTimer(aInstance)
, mTcatActiveDurationMs(0)
, mHashVerificationAttempts(1)
{
ClearCommissionerState();
mLastHashVerificationTimestamp = Get<UptimeTracker>().GetUptimeInSeconds();
}
void TcatAgent::ClearCommissionerState(void)
@@ -496,10 +498,6 @@ Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutg
error = HandleRequestRandomNumberChallenge(aOutgoingMessage, response);
break;
case kTlvRequestPskdHash:
error = HandleRequestPskdHash(aIncomingMessage, aOutgoingMessage, offset, length, response);
break;
case kTlvGetCommissionerCertificate:
error = HandleGetCommissionerCertificate(aOutgoingMessage, response);
break;
@@ -883,33 +881,6 @@ exit:
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,
uint16_t aOffset,
uint16_t aLength,
@@ -918,14 +889,26 @@ Error TcatAgent::VerifyHash(const Message &aIncomingMessage,
{
Error error = kErrorNone;
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(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));
DumpDebg("Hash", &hash, sizeof(hash));
VerifyOrExit(aIncomingMessage.Compare(aOffset, hash), error = kErrorSecurity);
mHashVerificationAttempts++;
exit:
return error;
+19 -16
View File
@@ -38,6 +38,10 @@
#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/tcat.h>
#include <openthread/platform/ble.h>
@@ -48,6 +52,7 @@
#include "common/log.hpp"
#include "common/message.hpp"
#include "common/non_copyable.hpp"
#include "common/uptime.hpp"
#include "mac/mac_types.hpp"
#include "meshcop/dataset.hpp"
#include "meshcop/meshcop.hpp"
@@ -172,7 +177,6 @@ public:
kTlvPresentPskcHash = 0x11, ///< TCAT commissioner rights elevation request TLV using PSKc hash
kTlvPresentInstallCodeHash = 0x12, ///< TCAT commissioner rights elevation request TLV using install code
kTlvRequestRandomNumChallenge = 0x13, ///< TCAT random number challenge query TLV
kTlvRequestPskdHash = 0x14, ///< TCAT PSKd hash request TLV
// Command Class Commissioning
kTlvSetActiveOperationalDataset = 0x20, ///< TCAT active operational dataset TLV
@@ -463,11 +467,6 @@ private:
Error HandlePresentPskcHash(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 HandleRequestPskdHash(const Message &aIncomingMessage,
Message &aOutgoingMessage,
uint16_t aOffset,
uint16_t aLength,
bool &aResponse);
Error HandleStartThreadInterface(void);
Error HandleStopThreadInterface(void);
Error HandleGetCommissionerCertificate(Message &aOutgoingMessage, bool &aResponse);
@@ -490,16 +489,18 @@ private:
const Dataset *aCommSuppliedDataset) const;
uint8_t CheckAuthorizationRequirements(CommandClassFlags aFlagsChecked, Dataset::Info *aActiveDatasetInfo) const;
static constexpr uint16_t kPingPayloadMaxLength = 512;
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 kTcatMaxDeviceIdSize = OT_TCAT_MAX_DEVICEID_SIZE;
static constexpr uint16_t kInstallCodeMaxSize = 255;
static constexpr uint16_t kCommissionerCertMaxLength = 1024;
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 kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT;
static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX;
static constexpr uint16_t kPingPayloadMaxLength = 512;
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 kTcatMaxDeviceIdSize = OT_TCAT_MAX_DEVICEID_SIZE;
static constexpr uint16_t kInstallCodeMaxSize = 255;
static constexpr uint16_t kCommissionerCertMaxLength = 1024;
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 kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT;
static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX;
static constexpr uint32_t kHashVerificationAttemptTime = 5;
static constexpr uint8_t kHashVerificationMaxAttempts = 10;
const VendorInfo *mVendorInfo;
Callback<JoinCallback> mJoinCallback;
@@ -524,6 +525,8 @@ private:
using ExpireTimer = TimerMilliIn<TcatAgent, &TcatAgent::HandleTimer>;
ExpireTimer mActiveOrStandbyTimer;
uint32_t mTcatActiveDurationMs;
UptimeSec mLastHashVerificationTimestamp;
uint8_t mHashVerificationAttempts;
};
DeclareTmfHandler(TcatAgent, kUriTcatEnable);
+24 -5
View File
@@ -31,6 +31,9 @@ source "tests/scripts/expect/_common.exp"
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
send "commission\n"
@@ -41,11 +44,6 @@ send "random_challenge\n"
expect_line "\tTYPE:\tRESPONSE_W_PAYLOAD"
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"
expect_line "\tTYPE:\tRESPONSE_W_STATUS"
expect_line "\tVALUE:\t0x07"
@@ -74,6 +72,27 @@ send "present_hash pskc aaaa\n"
expect_line "\tTYPE:\tRESPONSE_W_STATUS"
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
switch_node 1
@@ -324,41 +324,6 @@ class GetNetworkNameCommand(BleCommand):
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):
def get_log_string(self) -> str:
+1 -2
View File
@@ -33,7 +33,7 @@ import shlex
from typing import Optional
from cli.base_commands import (DisconnectCommand, HelpCommand, HelloCommand, CommissionCommand, DecommissionCommand,
ExtractDatasetCommand, GetCommissionerCertificate, GetDeviceIdCommand, GetPskdHash,
ExtractDatasetCommand, GetCommissionerCertificate, GetDeviceIdCommand,
GetExtPanIDCommand, GetNetworkNameCommand, GetProvisioningUrlCommand, PingCommand,
GetRandomNumberChallenge, ThreadStateCommand, ScanCommand, PresentHash,
DiagnosticTlvsCommand, GetApplicationLayersCommand, SendVendorData,
@@ -74,7 +74,6 @@ class CLI:
'simulation': SimulationCommand(),
'random_challenge': GetRandomNumberChallenge(),
'present_hash': PresentHash(),
'peer_pskd_hash': GetPskdHash(),
'tlv': TlvCommand(),
'get_comm_cert': GetCommissionerCertificate(),
'diagnostic_tlvs': DiagnosticTlvsCommand()
-1
View File
@@ -42,7 +42,6 @@ class TcatTLVType(Enum):
PRESENT_PSKC_HASH = 0x11
PRESENT_INSTALL_CODE_HASH = 0x12
GET_RANDOM_NUMBER_CHALLENGE = 0x13
GET_PSKD_HASH = 0x14
ACTIVE_DATASET = 0x20
GET_COMMISSIONER_CERTIFICATE = 0x25
GET_ACTIVE_DATASET = 0x40