diff --git a/src/cli/cli_tcat.cpp b/src/cli/cli_tcat.cpp index 4dda80d0cbba..ed0407f503f6 100644 --- a/src/cli/cli_tcat.cpp +++ b/src/cli/cli_tcat.cpp @@ -92,8 +92,9 @@ namespace Cli { otTcatAdvertisedDeviceId sAdvertisedDeviceIds[OT_TCAT_DEVICE_ID_MAX]; otTcatGeneralDeviceId sGeneralDeviceId; -const char kPskdVendor[] = "JJJJJJ"; -const char kUrl[] = "dummy_url"; +const char kPskdVendor[] = "JJJJJJ"; +const char kInstallVendor[] = "InstallCode"; +const char kUrl[] = "dummy_url"; static bool IsDeviceIdSet(void) { @@ -250,6 +251,7 @@ template <> otError Tcat::Process(Arg aArgs[]) ClearAllBytes(mVendorInfo); mVendorInfo.mPskdString = kPskdVendor; mVendorInfo.mProvisioningUrl = kUrl; + mVendorInfo.mInstallCode = kInstallVendor; if (IsDeviceIdSet()) { diff --git a/src/core/meshcop/secure_transport.cpp b/src/core/meshcop/secure_transport.cpp index 59f859c96b57..a4abd66c565c 100644 --- a/src/core/meshcop/secure_transport.cpp +++ b/src/core/meshcop/secure_transport.cpp @@ -425,7 +425,6 @@ Error SecureTransport::Setup(bool aClient) int SecureTransport::SetApplicationSecureKeys(void) { int rval = 0; - switch (mCipherSuites[0]) { case MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8: diff --git a/src/core/meshcop/secure_transport.hpp b/src/core/meshcop/secure_transport.hpp index 1d0acd8e86a8..58ab18d75dcb 100644 --- a/src/core/meshcop/secure_transport.hpp +++ b/src/core/meshcop/secure_transport.hpp @@ -489,6 +489,15 @@ class SecureTransport : public InstanceLocator */ void HandleReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + /** + * Extracts public key from it's own certificate. + * + * @return public key from own certificate in form of entire ASN.1 field. + * + */ + + mbedtls_asn1_buf *GetOwnPublicKey(void) { return &mOwnCert.pk_raw; } + private: enum State : uint8_t { diff --git a/src/core/meshcop/tcat_agent.cpp b/src/core/meshcop/tcat_agent.cpp index 847a005c6a02..9cca057f604d 100644 --- a/src/core/meshcop/tcat_agent.cpp +++ b/src/core/meshcop/tcat_agent.cpp @@ -32,8 +32,12 @@ */ #include "tcat_agent.hpp" +#include #include +#include +#include "common/error.hpp" #include "meshcop/network_name.hpp" +#include "thread/key_manager.hpp" #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE @@ -42,6 +46,7 @@ #include "common/debug.hpp" #include "common/encoding.hpp" #include "common/locator_getters.hpp" +#include "common/random.hpp" #include "common/string.hpp" #include "instance/instance.hpp" #include "radio/radio.hpp" @@ -71,6 +76,7 @@ TcatAgent::TcatAgent(Instance &aInstance) , mCommissionerHasNetworkName(false) , mCommissionerHasDomainName(false) , mCommissionerHasExtendedPanId(false) + , mRandomChallenge(0) { mJoinerPskd.Clear(); mCurrentServiceName[0] = 0; @@ -84,6 +90,7 @@ Error TcatAgent::Start(AppDataReceiveCallback aAppDataReceiveCallback, JoinCallb VerifyOrExit(mVendorInfo != nullptr, error = kErrorFailed); mAppDataReceiveCallback.Set(aAppDataReceiveCallback, aContext); mJoinCallback.Set(aHandler, aContext); + mRandomChallenge = 0; mCurrentApplicationProtocol = kApplicationProtocolNone; mState = kStateEnabled; @@ -99,6 +106,7 @@ void TcatAgent::Stop(void) mState = kStateDisabled; mAppDataReceiveCallback.Clear(); mJoinCallback.Clear(); + mRandomChallenge = 0; LogInfo("TCAT agent stopped"); } @@ -180,6 +188,7 @@ void TcatAgent::Disconnected(void) { mState = kStateEnabled; } + mRandomChallenge = 0; LogInfo("TCAT agent disconnected"); } @@ -436,7 +445,21 @@ Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutg case kTlvGetProvisioningURL: error = HandleGetProvisioningUrl(aOutgoingMessage, response); break; - + case kTlvPresentPskdHash: + error = HandlePresentPskdHash(aIncomingMessage, offset, length); + break; + case kTlvPresentPskcHash: + error = HandlePresentPskcHash(aIncomingMessage, offset, length); + break; + case kTlvPresentInstallCodeHash: + error = HandlePresentInstallCodeHash(aIncomingMessage, offset, length); + break; + case kTlvRequestRandomNumChallenge: + error = HandleRequestRandomNumberChallenge(aOutgoingMessage, response); + break; + case kTlvRequestPskdHash: + error = HandleRequestPskdHash(aIncomingMessage, aOutgoingMessage, offset, length, response); + break; default: error = kErrorInvalidCommand; } @@ -471,6 +494,10 @@ Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutg statusCode = kStatusUnsupported; break; + case kErrorSecurity: + statusCode = kStatusHashError; + break; + default: statusCode = kStatusGeneralError; break; @@ -629,13 +656,134 @@ Error TcatAgent::HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &respo length = StringLength(mVendorInfo->mProvisioningUrl, kProvisioningUrlMaxLength); VerifyOrExit(length > 0 && length <= Tlv::kBaseTlvMaxLength, error = kErrorInvalidState); - error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, mVendorInfo->mProvisioningUrl, length); + SuccessOrExit(error = + Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, mVendorInfo->mProvisioningUrl, length)); response = true; exit: return error; } +Error TcatAgent::HandlePresentPskdHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength) +{ + Error error = kErrorNone; + + VerifyOrExit(mRandomChallenge != 0, error = kErrorFailed); + + error = VerifyHash(aIncomingMessage, aOffset, aLength, mVendorInfo->mPskdString, + StringLength(mVendorInfo->mPskdString, kMaxPSKdLength)); + +exit: + return error; +} + +Error TcatAgent::HandlePresentPskcHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength) +{ + Error error = kErrorNone; + Dataset::Info datasetInfo; + Pskc pskc; + + VerifyOrExit(Get().Read(datasetInfo) == kErrorNone, error = kErrorSecurity); + VerifyOrExit(datasetInfo.IsPresent(), error = kErrorSecurity); + pskc = datasetInfo.Get(); + + error = VerifyHash(aIncomingMessage, aOffset, aLength, reinterpret_cast(pskc.m8), Pskc::kSize); + +exit: + return error; +} + +Error TcatAgent::HandlePresentInstallCodeHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength) +{ + Error error = kErrorNone; + + VerifyOrExit(mVendorInfo->mInstallCode != nullptr, error = kErrorSecurity); + + error = VerifyHash(aIncomingMessage, aOffset, aLength, mVendorInfo->mInstallCode, + StringLength(mVendorInfo->mInstallCode, 255)); + +exit: + return error; +} + +Error TcatAgent::HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &response) +{ + Error error = kErrorNone; + + SuccessOrExit(error = Random::Crypto::Fill(mRandomChallenge)); + + SuccessOrExit( + error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, &mRandomChallenge, sizeof(mRandomChallenge))); + response = true; +exit: + return error; +} + +Error TcatAgent::HandleRequestPskdHash(const Message &aIncomingMessage, + Message &aOutgoingMessage, + uint16_t aOffset, + uint16_t aLength, + bool &response) +{ + Error error = kErrorNone; + uint64_t providedChallenge = 0; + Crypto::HmacSha256::Hash hash; + + VerifyOrExit(StringLength(mVendorInfo->mPskdString, kMaxPSKdLength) != 0, error = kErrorFailed); + VerifyOrExit(aLength == sizeof(providedChallenge), error = kErrorParse); + + SuccessOrExit(error = aIncomingMessage.Read(aOffset, &providedChallenge, aLength)); + + hash = CalculateHash(providedChallenge, mVendorInfo->mPskdString, + StringLength(mVendorInfo->mPskdString, kMaxPSKdLength)); + + SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, hash.GetBytes(), + Crypto::HmacSha256::Hash::kSize)); + response = true; + +exit: + return error; +} + +Error TcatAgent::VerifyHash(const Message &aIncomingMessage, + uint16_t aOffset, + uint16_t aLength, + const char *aBuf, + size_t aBufLen) +{ + Error error = kErrorNone; + Crypto::HmacSha256::Hash hash; + Crypto::HmacSha256::Hash recivedHash; + + VerifyOrExit(aLength == Crypto::HmacSha256::Hash::kSize, error = kErrorSecurity); + VerifyOrExit(mRandomChallenge != 0, error = kErrorSecurity); + + SuccessOrExit(error = aIncomingMessage.Read(aOffset, recivedHash.m8, aLength)); + + hash = CalculateHash(mRandomChallenge, aBuf, aBufLen); + + VerifyOrExit(hash == recivedHash, error = kErrorSecurity); +exit: + return error; +} + +Crypto::HmacSha256::Hash TcatAgent::CalculateHash(uint64_t aChallenge, const char *aBuf, size_t aBufLen) +{ + mbedtls_asn1_buf *rawKey = Get().GetOwnPublicKey(); + Crypto::Key cryptoKey; + Crypto::HmacSha256 hmac; + Crypto::HmacSha256::Hash hash; + + cryptoKey.Set(rawKey->p, rawKey->len); + + hmac.Start(cryptoKey); + hmac.Update(aChallenge); + hmac.Update(aBuf, aBufLen); + hmac.Finish(hash); + + return hash; +} + Error TcatAgent::HandleStartThreadInterface(void) { Error error; diff --git a/src/core/meshcop/tcat_agent.hpp b/src/core/meshcop/tcat_agent.hpp index 15578ffbc824..5941671d79d2 100644 --- a/src/core/meshcop/tcat_agent.hpp +++ b/src/core/meshcop/tcat_agent.hpp @@ -363,8 +363,24 @@ class TcatAgent : public InstanceLocator, private NonCopyable Error HandleGetDeviceId(Message &aOutgoingMessage, bool &response); Error HandleGetExtPanId(Message &aOutgoingMessage, bool &response); Error HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &response); + Error HandlePresentPskdHash(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 HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &response); + Error HandleRequestPskdHash(const Message &aIncomingMessage, + Message &aOutgoingMessage, + uint16_t aOffset, + uint16_t aLength, + bool &response); Error HandleStartThreadInterface(void); + Error VerifyHash(const Message &aIncomingMessage, + uint16_t aOffset, + uint16_t aLength, + const char *aBuf, + size_t aBufLen); + Crypto::HmacSha256::Hash CalculateHash(uint64_t aChallenge, const char *aBuf, size_t aBufLen); + bool CheckCommandClassAuthorizationFlags(CommandClassFlags aCommissionerCommandClassFlags, CommandClassFlags aDeviceCommandClassFlags, Dataset *aDataset) const; @@ -374,6 +390,7 @@ class TcatAgent : public InstanceLocator, private NonCopyable static constexpr uint16_t kJoinerUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT; static constexpr uint16_t kPingPayloadMaxLength = 512; static constexpr uint16_t kProvisioningUrlMaxLength = 64; + static constexpr uint16_t kMaxPSKdLength = OT_JOINER_MAX_PSKD_LENGTH; static constexpr uint16_t kTcatMaxDeviceIdSize = OT_TCAT_MAX_DEVICEID_SIZE; JoinerPskd mJoinerPskd; @@ -391,6 +408,7 @@ class TcatAgent : public InstanceLocator, private NonCopyable bool mCommissionerHasNetworkName : 1; bool mCommissionerHasDomainName : 1; bool mCommissionerHasExtendedPanId : 1; + uint64_t mRandomChallenge; friend class Ble::BleSecure; }; diff --git a/src/core/radio/ble_secure.hpp b/src/core/radio/ble_secure.hpp index 7be1201d1814..2ad41b46765d 100644 --- a/src/core/radio/ble_secure.hpp +++ b/src/core/radio/ble_secure.hpp @@ -357,6 +357,15 @@ class BleSecure : public InstanceLocator, private NonCopyable return mTls.GetThreadAttributeFromOwnCertificate(aThreadOidDescriptor, aAttributeBuffer, aAttributeLength); } + /** + * Extracts public key from it's own certificate. + * + * @return public key from own certificate in form of entire ASN.1 field. + * + */ + + mbedtls_asn1_buf *GetOwnPublicKey(void) { return mTls.GetOwnPublicKey(); } + /** * Sets the authentication mode for the BLE secure connection. It disables or enables the verification * of peer certificate. diff --git a/tests/scripts/expect/_common.exp b/tests/scripts/expect/_common.exp index b94a48583e7b..8b9968d343d9 100644 --- a/tests/scripts/expect/_common.exp +++ b/tests/scripts/expect/_common.exp @@ -266,4 +266,35 @@ proc fail {message} { error $message } +proc spawn_tcat_client_for_node {id} { + global tcat_ids + global spawn_id + + switch_node $id + + send "tcat start\n" + expect_line "Done" + + spawn python "tools/tcat_ble_client/bbtc.py" --simulation $id --cert_path "tools/tcat_ble_client/auth" + expect_line "Done" + + set tcat_ids($id) $spawn_id + + return $spawn_id +} + +proc switch_tcat_client_for_node {id} { + global tcat_ids + global spawn_id + + send_user "\n# ${id}\n" + set spawn_id $tcat_ids($id) +} + +proc dispose_tcat_client {id} { + switch_tcat_client_for_node $id + send "exit\n" + expect eof +} + set timeout 10 diff --git a/tests/scripts/expect/cli-tcat-advertisement.exp b/tests/scripts/expect/cli-tcat-advertisement.exp index cf5e05134a29..3ec91d55fc04 100755 --- a/tests/scripts/expect/cli-tcat-advertisement.exp +++ b/tests/scripts/expect/cli-tcat-advertisement.exp @@ -31,7 +31,6 @@ source "tests/scripts/expect/_common.exp" spawn_node 1 "cli" -switch_node 1 send "tcat advid ianapen f378\n" expect_line "Done" @@ -89,3 +88,5 @@ expect_line "Done" send "tcat devid\n" expect_line "Done" + +dispose_all diff --git a/tests/scripts/expect/cli-tcat-decommission.exp b/tests/scripts/expect/cli-tcat-decommission.exp index c0488c031678..302e8db08b68 100755 --- a/tests/scripts/expect/cli-tcat-decommission.exp +++ b/tests/scripts/expect/cli-tcat-decommission.exp @@ -31,13 +31,8 @@ source "tests/scripts/expect/_common.exp" spawn_node 1 "cli" -switch_node 1 -send "tcat start\n" -expect_line "Done" +spawn_tcat_client_for_node 1 -spawn python "tools/tcat_ble_client/bbtc.py" --simulation 1 --cert_path "tools/tcat_ble_client/auth" -set py_client "$spawn_id" -expect_line "Done" send "commission\n" expect_line "\tTYPE:\tRESPONSE_W_STATUS" expect_line "\tVALUE:\t0x00" @@ -49,8 +44,7 @@ expect_line "\tVALUE:\t0x00" send "decommission\n" expect_line "\tTYPE:\tRESPONSE_W_STATUS" -send "exit\n" -expect eof +dispose_tcat_client 1 switch_node 1 send "tcat stop\n" @@ -62,3 +56,5 @@ expect_line "Error 23: NotFound" send "networkkey\n" expect_line "00000000000000000000000000000000" expect_line "Done" + +dispose_all diff --git a/tests/scripts/expect/cli-tcat-hashes.exp b/tests/scripts/expect/cli-tcat-hashes.exp new file mode 100755 index 000000000000..90540e44ba4c --- /dev/null +++ b/tests/scripts/expect/cli-tcat-hashes.exp @@ -0,0 +1,83 @@ +#!/usr/bin/expect -f +# +# Copyright (c) 2024, 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. +# + +source "tests/scripts/expect/_common.exp" + +spawn_node 1 "cli" + +spawn_tcat_client_for_node 1 + +send "commission\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +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 JJJJJJ\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +send "present_hash pskd AAAA\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x07" + +send "present_hash install InstallCode\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +send "present_hash install Code\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x07" + +send "present_hash pskc 5e9b9b360f80b88be2603fb0135c8d65\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +send "present_hash pskc aaaa\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x07" + +dispose_tcat_client 1 + +switch_node 1 +send "tcat stop\n" +expect_line "Done" + +send "networkkey\n" +expect_line "fda7c771a27202e232ecd04cf934f476" +expect_line "Done" + +dispose_all diff --git a/tests/scripts/expect/cli-tcat.exp b/tests/scripts/expect/cli-tcat.exp index 53d344b89847..e30926a06016 100755 --- a/tests/scripts/expect/cli-tcat.exp +++ b/tests/scripts/expect/cli-tcat.exp @@ -31,13 +31,7 @@ source "tests/scripts/expect/_common.exp" spawn_node 1 "cli" -switch_node 1 -send "tcat start\n" -expect_line "Done" - -spawn python "tools/tcat_ble_client/bbtc.py" --simulation 1 --cert_path "tools/tcat_ble_client/auth" -set py_client "$spawn_id" -expect_line "Done" +spawn_tcat_client_for_node 1 send "network_name\n" expect_line "\tTYPE:\tRESPONSE_W_STATUS" @@ -83,8 +77,7 @@ expect_line "\tTYPE:\tRESPONSE_W_PAYLOAD" expect_line "\tLEN:\t9" expect_line "\tVALUE:\t0x64756d6d795f75726c" -send "exit\n" -expect eof +dispose_tcat_client 1 switch_node 1 send "tcat stop\n" @@ -96,3 +89,5 @@ expect_line "Done" wait_for "state" "leader" expect_line "Done" + +dispose_all diff --git a/tools/tcat_ble_client/bbtc.py b/tools/tcat_ble_client/bbtc.py index 5ad735943de6..23773ec4abad 100755 --- a/tools/tcat_ble_client/bbtc.py +++ b/tools/tcat_ble_client/bbtc.py @@ -87,12 +87,10 @@ async def main(): print('Setting up secure TLS channel..', end='') try: await ble_sstream.do_handshake() - print('\nDone') - ble_sstream.log_cert_identities() + print('Done') except Exception as e: - print('\nFailed') + print('Failed') logger.error(e) - ble_sstream.log_cert_identities() quit_with_reason('TLS handshake failure') ds = ThreadDataset() diff --git a/tools/tcat_ble_client/ble/ble_stream_secure.py b/tools/tcat_ble_client/ble/ble_stream_secure.py index 97ac0d653d9c..c54d14a6ef22 100644 --- a/tools/tcat_ble_client/ble/ble_stream_secure.py +++ b/tools/tcat_ble_client/ble/ble_stream_secure.py @@ -31,6 +31,9 @@ import ssl import sys import logging +import OpenSSL +from cryptography.x509 import load_der_x509_certificate +import cryptography from tlv.tlv import TLV from tlv.tcat_tlv import TcatTLVType @@ -49,6 +52,8 @@ def __init__(self, stream): self.outgoing = ssl.MemoryBIO() self.ssl_object = None self.cert = '' + self.peer_challenge = None + self._peer_public_key = None def load_cert(self, certfile='', keyfile='', cafile=''): if certfile and keyfile: @@ -102,6 +107,13 @@ async def do_handshake(self, timeout=30.0): else: print('TLS Connection timed out.') return False + print('') + cert = self.ssl_object.getpeercert(True) + cert_obj = load_der_x509_certificate(cert) + self._peer_public_key = cert_obj.public_key().public_bytes( + cryptography.hazmat.primitives.serialization.Encoding.DER, + cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo) + self.log_cert_identities() return True async def send(self, bytes): @@ -142,8 +154,22 @@ async def close(self): if self.ssl_object.session is not None: logger.debug('sending Disconnect command TLV') data = TLV(TcatTLVType.DISCONNECT.value, bytes()).to_bytes() + self.peer_challenge = None + self._peer_public_key = None await self.send(data) + @property + def peer_public_key(self): + return self._peer_public_key + + @property + def peer_challenge(self): + return self._peer_challenge + + @peer_challenge.setter + def peer_challenge(self, value): + self._peer_challenge = value + def log_cert_identities(self): # using the internal object of the ssl library is necessary to see the cert data in # case of handshake failure - see https://sethmlarson.dev/experimental-python-3.10-apis-and-trust-stores diff --git a/tools/tcat_ble_client/cli/base_commands.py b/tools/tcat_ble_client/cli/base_commands.py index 669f78ccf1fb..fbce032b9c7e 100644 --- a/tools/tcat_ble_client/cli/base_commands.py +++ b/tools/tcat_ble_client/cli/base_commands.py @@ -40,6 +40,9 @@ from os import path from time import time from secrets import token_bytes +from hashlib import sha256 +import hmac +import binascii class HelpCommand(Command): @@ -55,6 +58,10 @@ async def execute_default(self, args, context): return CommandResultNone() +class DataNotPrepared(Exception): + pass + + class BleCommand(Command): @abstractmethod @@ -62,19 +69,22 @@ def get_log_string(self) -> str: pass @abstractmethod - def prepare_data(self, context): + def prepare_data(self, args, context): pass async def execute_default(self, args, context): bless: BleStreamSecure = context['ble_sstream'] print(self.get_log_string()) - data = self.prepare_data(context) - response = await bless.send_with_resp(data) - if not response: - return - tlv_response = TLV.from_bytes(response) - return CommandResultTLV(tlv_response) + try: + data = self.prepare_data(args, context) + response = await bless.send_with_resp(data) + if not response: + return + tlv_response = TLV.from_bytes(response) + return CommandResultTLV(tlv_response) + except DataNotPrepared as err: + print('Command failed', err) class HelloCommand(BleCommand): @@ -85,7 +95,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Send round trip "Hello world!" message.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.APPLICATION.value, bytes('Hello world!', 'ascii')).to_bytes() @@ -97,7 +107,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Update the connected device with current dataset.' - def prepare_data(self, context): + def prepare_data(self, args, context): dataset: ThreadDataset = context['dataset'] dataset_bytes = dataset.to_bytes() return TLV(TcatTLVType.ACTIVE_DATASET.value, dataset_bytes).to_bytes() @@ -111,7 +121,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Stop Thread interface and decommission device from current network.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.DECOMMISSION.value, bytes()).to_bytes() @@ -123,7 +133,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Get unique identifier for the TCAT device.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.GET_DEVICE_ID.value, bytes()).to_bytes() @@ -135,7 +145,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Get extended PAN ID that is commissioned in the active dataset.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.GET_EXT_PAN_ID.value, bytes()).to_bytes() @@ -147,7 +157,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Get a URL for an application suited to commission the TCAT device.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.GET_PROVISIONING_URL.value, bytes()).to_bytes() @@ -159,10 +169,111 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Get the Thread network name that is commissioned in the active dataset.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.GET_NETWORK_NAME.value, bytes()).to_bytes() +class PresentHash(BleCommand): + + def get_log_string(self) -> str: + return 'Presenting hash.' + + def get_help_string(self) -> str: + return 'Present calculated hash.' + + def prepare_data(self, args, context): + type = args[0] + code = None + tlv_type = None + if type == "pskd": + code = bytes(args[1], 'utf-8') + tlv_type = TcatTLVType.PRESENT_PSKD_HASH.value + elif type == "pskc": + code = bytes.fromhex(args[1]) + tlv_type = TcatTLVType.PRESENT_PSKC_HASH.value + elif type == "install": + code = bytes(args[1], 'utf-8') + tlv_type = TcatTLVType.PRESENT_INSTALL_CODE_HASH.value + else: + raise DataNotPrepared("Hash code name incorrect.") + bless: BleStreamSecure = context['ble_sstream'] + if bless.peer_public_key is None: + raise DataNotPrepared("Peer certificate not present.") + + if bless.peer_challenge is None: + raise DataNotPrepared("Peer challenge not present.") + + hash = hmac.new(bless.peer_public_key, digestmod=sha256) + hash.update(bless.peer_challenge) + hash.update(code) + + data = TLV(tlv_type, hash.digest()).to_bytes() + return data + + +class GetPskdHash(Command): + + def get_log_string(self) -> str: + return 'Retrieving peer PSKd hash.' + + def get_help_string(self) -> str: + return 'Get calculated PSKd hash.' + + async def execute_default(self, args, context): + bless: BleStreamSecure = context['ble_sstream'] + + print(self.get_log_string()) + try: + if bless.peer_public_key is None: + print("Peer certificate not present.") + return + challenge_size = 8 + challenge = token_bytes(challenge_size) + pskd = bytes(args[0], 'utf-8') + data = TLV(TcatTLVType.GET_PSKD_HASH.value, challenge).to_bytes() + response = await bless.send_with_resp(data) + if not response: + return + tlv_response = TLV.from_bytes(response) + if tlv_response.value != None: + hash = hmac.new(bless.peer_public_key, digestmod=sha256) + hash.update(challenge) + hash.update(pskd) + digest = hash.digest() + if digest == tlv_response.value: + print('Requested hash is valid.') + else: + print('Requested hash is NOT valid.') + return CommandResultTLV(tlv_response) + except DataNotPrepared as err: + print('Command failed', err) + + +class GetRandomNumberChallenge(Command): + + def get_log_string(self) -> str: + return 'Retrieving random challenge.' + + def get_help_string(self) -> str: + return 'Get the device random number challenge.' + + async def execute_default(self, args, context): + bless: BleStreamSecure = context['ble_sstream'] + + print(self.get_log_string()) + try: + data = TLV(TcatTLVType.GET_RANDOM_NUMBER_CHALLENGE.value, bytes()).to_bytes() + response = await bless.send_with_resp(data) + if not response: + return + tlv_response = TLV.from_bytes(response) + if tlv_response.value != None: + bless.peer_challenge = tlv_response.value + return CommandResultTLV(tlv_response) + except DataNotPrepared as err: + print('Command failed', err) + + class PingCommand(Command): def get_help_string(self) -> str: @@ -202,7 +313,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Enable thread interface.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.THREAD_START.value, bytes()).to_bytes() @@ -214,7 +325,7 @@ def get_log_string(self) -> str: def get_help_string(self) -> str: return 'Disable thread interface.' - def prepare_data(self, context): + def prepare_data(self, args, context): return TLV(TcatTLVType.THREAD_STOP.value, bytes()).to_bytes() diff --git a/tools/tcat_ble_client/cli/cli.py b/tools/tcat_ble_client/cli/cli.py index 9835c950a79d..35ea1a2b6748 100644 --- a/tools/tcat_ble_client/cli/cli.py +++ b/tools/tcat_ble_client/cli/cli.py @@ -30,8 +30,8 @@ from argparse import ArgumentParser from ble.ble_stream_secure import BleStreamSecure from cli.base_commands import (HelpCommand, HelloCommand, CommissionCommand, DecommissionCommand, GetDeviceIdCommand, - GetExtPanIDCommand, GetNetworkNameCommand, GetProvisioningUrlCommand, PingCommand, - ThreadStateCommand, ScanCommand) + GetPskdHash, GetExtPanIDCommand, GetNetworkNameCommand, GetProvisioningUrlCommand, + PingCommand, GetRandomNumberChallenge, ThreadStateCommand, ScanCommand, PresentHash) from cli.dataset_commands import (DatasetCommand) from dataset.dataset import ThreadDataset from typing import Optional @@ -56,6 +56,9 @@ def __init__(self, 'dataset': DatasetCommand(), 'thread': ThreadStateCommand(), 'scan': ScanCommand(), + 'random_challenge': GetRandomNumberChallenge(), + 'present_hash': PresentHash(), + 'peer_pskd_hash': GetPskdHash(), } self._context = { 'ble_sstream': ble_sstream, diff --git a/tools/tcat_ble_client/tlv/tcat_tlv.py b/tools/tcat_ble_client/tlv/tcat_tlv.py index 7d26864ae787..564db91bb09f 100644 --- a/tools/tcat_ble_client/tlv/tcat_tlv.py +++ b/tools/tcat_ble_client/tlv/tcat_tlv.py @@ -37,6 +37,11 @@ class TcatTLVType(Enum): GET_DEVICE_ID = 0x0B GET_EXT_PAN_ID = 0x0C GET_PROVISIONING_URL = 0x0D + PRESENT_PSKD_HASH = 0x10 + PRESENT_PSKC_HASH = 0x11 + PRESENT_INSTALL_CODE_HASH = 0x12 + GET_RANDOM_NUMBER_CHALLENGE = 0x13 + GET_PSKD_HASH = 0x14 ACTIVE_DATASET = 0x20 DECOMMISSION = 0x60 APPLICATION = 0x82