From 213665cce005b88097c3a9ccace9a52767f551fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Bida?= Date: Wed, 2 Oct 2024 20:10:16 +0200 Subject: [PATCH] [tcat] implementation of TCAT general class commands (#10700) Commit introduces implementation of missing general class commands: - PresentPskdHash - PresentPskcHash - PresentInstallCodeHash - RequestRandomNumChallenge - RequestPskdHash Also include minor fixes in Tcat python client and refactoring of expect tests for tcat. --- .github/workflows/posix.yml | 2 +- .github/workflows/simulation-1.1.yml | 2 +- .github/workflows/simulation-1.4.yml | 2 +- include/openthread/ble_secure.h | 10 + include/openthread/instance.h | 2 +- src/cli/cli_tcat.cpp | 6 +- src/core/api/ble_secure_api.cpp | 5 + src/core/meshcop/secure_transport.hpp | 8 + src/core/meshcop/tcat_agent.cpp | 184 ++++++++++++++++-- src/core/meshcop/tcat_agent.hpp | 47 ++++- src/core/radio/ble_secure.hpp | 15 ++ tests/scripts/expect/_common.exp | 31 +++ .../scripts/expect/cli-tcat-advertisement.exp | 3 +- .../scripts/expect/cli-tcat-decommission.exp | 12 +- tests/scripts/expect/cli-tcat-hashes.exp | 83 ++++++++ tests/scripts/expect/cli-tcat.exp | 13 +- tools/tcat_ble_client/bbtc.py | 6 +- .../tcat_ble_client/ble/ble_stream_secure.py | 23 +++ tools/tcat_ble_client/cli/base_commands.py | 147 ++++++++++++-- tools/tcat_ble_client/cli/cli.py | 7 +- tools/tcat_ble_client/tlv/tcat_tlv.py | 5 + 21 files changed, 543 insertions(+), 70 deletions(-) create mode 100755 tests/scripts/expect/cli-tcat-hashes.exp diff --git a/.github/workflows/posix.yml b/.github/workflows/posix.yml index 00226c5f66c..5434809ef00 100644 --- a/.github/workflows/posix.yml +++ b/.github/workflows/posix.yml @@ -60,7 +60,7 @@ jobs: - name: Bootstrap run: | sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat - pip install bleak + pip install bleak 'cryptography==43.0.0' - name: Run RCP Mode run: | ulimit -c unlimited diff --git a/.github/workflows/simulation-1.1.yml b/.github/workflows/simulation-1.1.yml index e4a179d8c53..c7b9db256b2 100644 --- a/.github/workflows/simulation-1.1.yml +++ b/.github/workflows/simulation-1.1.yml @@ -247,7 +247,7 @@ jobs: - name: Bootstrap run: | sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat - pip install bleak + pip install bleak 'cryptography==43.0.0' - name: Run run: | ulimit -c unlimited diff --git a/.github/workflows/simulation-1.4.yml b/.github/workflows/simulation-1.4.yml index e02a85c2def..af2cb1fa46b 100644 --- a/.github/workflows/simulation-1.4.yml +++ b/.github/workflows/simulation-1.4.yml @@ -298,7 +298,7 @@ jobs: - name: Bootstrap run: | sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat - pip install bleak + pip install bleak 'cryptography==43.0.0' - name: Run RCP Mode run: | ulimit -c unlimited diff --git a/include/openthread/ble_secure.h b/include/openthread/ble_secure.h index 8aa6cd296ed..5b5c315af43 100644 --- a/include/openthread/ble_secure.h +++ b/include/openthread/ble_secure.h @@ -407,6 +407,16 @@ otError otBleSecureSendApplicationTlv(otInstance *aInstance, uint8_t *aBuf, uint */ otError otBleSecureFlush(otInstance *aInstance); +/** + * Gets the Install Code Verify Status during the current session. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @retval TRUE The install code was correctly verified. + * @retval FALSE The install code was not verified. + */ +bool otBleSecureGetInstallCodeVerifyStatus(otInstance *aInstance); + /** * @} */ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 3e3c0105913..56d63bb35d6 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 (451) +#define OPENTHREAD_API_VERSION (452) /** * @addtogroup api-instance diff --git a/src/cli/cli_tcat.cpp b/src/cli/cli_tcat.cpp index 063f8b48d3e..5a0bfe49606 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) { @@ -293,6 +294,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/api/ble_secure_api.cpp b/src/core/api/ble_secure_api.cpp index 049df388765..ec03f9a151a 100644 --- a/src/core/api/ble_secure_api.cpp +++ b/src/core/api/ble_secure_api.cpp @@ -186,4 +186,9 @@ otError otBleSecureSendApplicationTlv(otInstance *aInstance, uint8_t *aBuf, uint otError otBleSecureFlush(otInstance *aInstance) { return AsCoreType(aInstance).Get().Flush(); } +bool otBleSecureGetInstallCodeVerifyStatus(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().GetInstallCodeVerifyStatus(); +} + #endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE diff --git a/src/core/meshcop/secure_transport.hpp b/src/core/meshcop/secure_transport.hpp index 162d104c64d..82e4c842c61 100644 --- a/src/core/meshcop/secure_transport.hpp +++ b/src/core/meshcop/secure_transport.hpp @@ -301,6 +301,14 @@ class SecureTransport : public InstanceLocator * @param[in] aX509CaCertChainLength The length of chain. */ void SetCaCertificateChain(const uint8_t *aX509CaCertificateChain, uint32_t aX509CaCertChainLength); + + /** + * Extracts public key from it's own certificate. + * + * @returns Public key from own certificate in form of entire ASN.1 field. + */ + const mbedtls_asn1_buf &GetOwnPublicKey(void) const { return mOwnCert.pk_raw; } + #endif // MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED #if defined(MBEDTLS_BASE64_C) && defined(MBEDTLS_SSL_KEEP_PEER_CERTIFICATE) diff --git a/src/core/meshcop/tcat_agent.cpp b/src/core/meshcop/tcat_agent.cpp index 79231001bdd..e9dede3d676 100644 --- a/src/core/meshcop/tcat_agent.cpp +++ b/src/core/meshcop/tcat_agent.cpp @@ -32,6 +32,7 @@ */ #include "tcat_agent.hpp" +#include "common/code_utils.hpp" #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE @@ -59,6 +60,10 @@ TcatAgent::TcatAgent(Instance &aInstance) , mCommissionerHasNetworkName(false) , mCommissionerHasDomainName(false) , mCommissionerHasExtendedPanId(false) + , mRandomChallenge(0) + , mPskdVerified(false) + , mPskcVerified(false) + , mInstallCodeVerified(false) { mJoinerPskd.Clear(); mCurrentServiceName[0] = 0; @@ -72,6 +77,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; @@ -87,6 +93,10 @@ void TcatAgent::Stop(void) mState = kStateDisabled; mAppDataReceiveCallback.Clear(); mJoinCallback.Clear(); + mRandomChallenge = 0; + mPskdVerified = false; + mPskcVerified = false; + mInstallCodeVerified = false; LogInfo("TCAT agent stopped"); } @@ -169,6 +179,11 @@ void TcatAgent::Disconnected(void) mState = kStateEnabled; } + mRandomChallenge = 0; + mPskdVerified = false; + mPskcVerified = false; + mInstallCodeVerified = false; + LogInfo("TCAT agent disconnected"); } @@ -190,14 +205,14 @@ bool TcatAgent::CheckCommandClassAuthorizationFlags(CommandClassFlags aCommissio additionalDeviceRequirementMet = true; } - if (aDeviceCommandClassFlags & kPskdFlag) + if (!additionalDeviceRequirementMet && (aDeviceCommandClassFlags & kPskdFlag)) { - additionalDeviceRequirementMet = true; + additionalDeviceRequirementMet = mPskdVerified; } - if (aDeviceCommandClassFlags & kPskcFlag) + if (!additionalDeviceRequirementMet && (aDeviceCommandClassFlags & kPskcFlag)) { - additionalDeviceRequirementMet = true; + additionalDeviceRequirementMet = mPskcVerified; } if (mCommissionerHasNetworkName || mCommissionerHasExtendedPanId) @@ -424,7 +439,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; } @@ -459,6 +488,10 @@ Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutg statusCode = kStatusUnsupported; break; + case kErrorSecurity: + statusCode = kStatusHashError; + break; + default: statusCode = kStatusGeneralError; break; @@ -519,7 +552,7 @@ Error TcatAgent::HandlePing(const Message &aIncomingMessage, Message &aOutgoingMessage, uint16_t aOffset, uint16_t aLength, - bool &response) + bool &aResponse) { Error error = kErrorNone; ot::ExtendedTlv extTlv; @@ -540,13 +573,13 @@ Error TcatAgent::HandlePing(const Message &aIncomingMessage, } SuccessOrExit(error = aOutgoingMessage.AppendBytesFromMessage(aIncomingMessage, aOffset, aLength)); - response = true; + aResponse = true; exit: return error; } -Error TcatAgent::HandleGetNetworkName(Message &aOutgoingMessage, bool &response) +Error TcatAgent::HandleGetNetworkName(Message &aOutgoingMessage, bool &aResponse) { Error error = kErrorNone; MeshCoP::NameData nameData = Get().GetNetworkName().GetAsData(); @@ -558,13 +591,13 @@ Error TcatAgent::HandleGetNetworkName(Message &aOutgoingMessage, bool &response) SuccessOrExit( error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, nameData.GetBuffer(), nameData.GetLength())); - response = true; + aResponse = true; exit: return error; } -Error TcatAgent::HandleGetDeviceId(Message &aOutgoingMessage, bool &response) +Error TcatAgent::HandleGetDeviceId(Message &aOutgoingMessage, bool &aResponse) { const uint8_t *deviceId; uint16_t length = 0; @@ -587,13 +620,13 @@ Error TcatAgent::HandleGetDeviceId(Message &aOutgoingMessage, bool &response) SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, deviceId, length)); - response = true; + aResponse = true; exit: return error; } -Error TcatAgent::HandleGetExtPanId(Message &aOutgoingMessage, bool &response) +Error TcatAgent::HandleGetExtPanId(Message &aOutgoingMessage, bool &aResponse) { Error error; @@ -601,13 +634,13 @@ Error TcatAgent::HandleGetExtPanId(Message &aOutgoingMessage, bool &response) SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, &Get().GetExtPanId(), sizeof(ExtendedPanId))); - response = true; + aResponse = true; exit: return error; } -Error TcatAgent::HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &response) +Error TcatAgent::HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &aResponse) { Error error = kErrorNone; uint16_t length; @@ -617,13 +650,132 @@ 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); - response = true; + SuccessOrExit(error = + Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, mVendorInfo->mProvisioningUrl, length)); + aResponse = true; exit: return error; } +Error TcatAgent::HandlePresentPskdHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength) +{ + Error error = kErrorNone; + + VerifyOrExit(mVendorInfo->mPskdString != nullptr, error = kErrorSecurity); + + SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffset, aLength, mVendorInfo->mPskdString, + StringLength(mVendorInfo->mPskdString, kMaxPskdLength))); + mPskdVerified = true; + +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(); + + SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffset, aLength, pskc.m8, Pskc::kSize)); + mPskcVerified = true; + +exit: + return error; +} + +Error TcatAgent::HandlePresentInstallCodeHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength) +{ + Error error = kErrorNone; + + VerifyOrExit(mVendorInfo->mInstallCode != nullptr, error = kErrorSecurity); + + SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffset, aLength, mVendorInfo->mInstallCode, + StringLength(mVendorInfo->mInstallCode, kInstallCodeMaxSize))); + mInstallCodeVerified = true; + +exit: + return error; +} + +Error TcatAgent::HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &aResponse) +{ + Error error = kErrorNone; + + SuccessOrExit(error = Random::Crypto::Fill(mRandomChallenge)); + + SuccessOrExit( + error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, &mRandomChallenge, sizeof(mRandomChallenge))); + aResponse = true; +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(StringLength(mVendorInfo->mPskdString, kMaxPskdLength) != 0, error = kErrorFailed); + VerifyOrExit(aLength == sizeof(providedChallenge), error = kErrorParse); + + SuccessOrExit(error = aIncomingMessage.Read(aOffset, &providedChallenge, aLength)); + + 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, + const void *aBuf, + size_t aBufLen) +{ + Error error = kErrorNone; + Crypto::HmacSha256::Hash hash; + + VerifyOrExit(aLength == Crypto::HmacSha256::Hash::kSize, error = kErrorSecurity); + VerifyOrExit(mRandomChallenge != 0, error = kErrorSecurity); + + CalculateHash(mRandomChallenge, reinterpret_cast(aBuf), aBufLen, hash); + + VerifyOrExit(aIncomingMessage.Compare(aOffset, hash), error = kErrorSecurity); + +exit: + return error; +} + +void TcatAgent::CalculateHash(uint64_t aChallenge, const char *aBuf, size_t aBufLen, Crypto::HmacSha256::Hash &aHash) +{ + const mbedtls_asn1_buf &rawKey = Get().GetOwnPublicKey(); + Crypto::Key cryptoKey; + Crypto::HmacSha256 hmac; + + cryptoKey.Set(reinterpret_cast(aBuf), static_cast(aBufLen)); + + hmac.Start(cryptoKey); + hmac.Update(aChallenge); + hmac.Update(rawKey.p, static_cast(rawKey.len)); + hmac.Finish(aHash); +} + Error TcatAgent::HandleStartThreadInterface(void) { Error error; diff --git a/src/core/meshcop/tcat_agent.hpp b/src/core/meshcop/tcat_agent.hpp index 96309463697..e318351db08 100644 --- a/src/core/meshcop/tcat_agent.hpp +++ b/src/core/meshcop/tcat_agent.hpp @@ -325,6 +325,14 @@ class TcatAgent : public InstanceLocator, private NonCopyable */ Error GetAdvertisementData(uint16_t &aLen, uint8_t *aAdvertisementData); + /** + * @brief Gets the Install Code Verify Status during the current session. + * + * @retval TRUE The install code was correctly verified. + * @retval FALSE The install code was not verified. + */ + bool GetInstallCodeVerifyStatus(void) const { return mInstallCodeVerified; } + private: Error Connected(MeshCoP::SecureTransport &aTlsContext); void Disconnected(void); @@ -336,23 +344,42 @@ class TcatAgent : public InstanceLocator, private NonCopyable Message &aOutgoingMessage, uint16_t aOffset, uint16_t aLength, - bool &response); - Error HandleGetNetworkName(Message &aOutgoingMessage, bool &response); - Error HandleGetDeviceId(Message &aOutgoingMessage, bool &response); - Error HandleGetExtPanId(Message &aOutgoingMessage, bool &response); - Error HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &response); + bool &aResponse); + Error HandleGetNetworkName(Message &aOutgoingMessage, bool &aResponse); + Error HandleGetDeviceId(Message &aOutgoingMessage, bool &aResponse); + Error HandleGetExtPanId(Message &aOutgoingMessage, bool &aResponse); + Error HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &aResponse); + 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 &aResponse); + Error HandleRequestPskdHash(const Message &aIncomingMessage, + Message &aOutgoingMessage, + uint16_t aOffset, + uint16_t aLength, + bool &aResponse); Error HandleStartThreadInterface(void); - bool CheckCommandClassAuthorizationFlags(CommandClassFlags aCommissionerCommandClassFlags, - CommandClassFlags aDeviceCommandClassFlags, - Dataset *aDataset) const; + Error VerifyHash(const Message &aIncomingMessage, + uint16_t aOffset, + uint16_t aLength, + const void *aBuf, + size_t aBufLen); + void CalculateHash(uint64_t aChallenge, const char *aBuf, size_t aBufLen, Crypto::HmacSha256::Hash &aHash); + + bool CheckCommandClassAuthorizationFlags(CommandClassFlags aCommissionerCommandClassFlags, + CommandClassFlags aDeviceCommandClassFlags, + Dataset *aDataset) const; + bool CanProcessTlv(uint8_t aTlvType) const; CommandClass GetCommandClass(uint8_t aTlvType) const; 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; + static constexpr uint16_t kInstallCodeMaxSize = 255; JoinerPskd mJoinerPskd; const VendorInfo *mVendorInfo; @@ -369,6 +396,10 @@ class TcatAgent : public InstanceLocator, private NonCopyable bool mCommissionerHasNetworkName : 1; bool mCommissionerHasDomainName : 1; bool mCommissionerHasExtendedPanId : 1; + uint64_t mRandomChallenge; + bool mPskdVerified : 1; + bool mPskcVerified : 1; + bool mInstallCodeVerified : 1; friend class Ble::BleSecure; }; diff --git a/src/core/radio/ble_secure.hpp b/src/core/radio/ble_secure.hpp index 2264d018941..48eda196dd6 100644 --- a/src/core/radio/ble_secure.hpp +++ b/src/core/radio/ble_secure.hpp @@ -334,6 +334,13 @@ class BleSecure : public InstanceLocator, private NonCopyable return mTls.GetThreadAttributeFromOwnCertificate(aThreadOidDescriptor, aAttributeBuffer, aAttributeLength); } + /** + * Extracts public key from it's own certificate. + * + * @returns Public key from own certificate in form of entire ASN.1 field. + */ + const mbedtls_asn1_buf &GetOwnPublicKey(void) const { return mTls.GetOwnPublicKey(); } + /** * Sets the authentication mode for the BLE secure connection. It disables or enables the verification * of peer certificate. @@ -419,6 +426,14 @@ class BleSecure : public InstanceLocator, private NonCopyable */ Error HandleBleMtuUpdate(uint16_t aMtu); + /** + * @brief Gets the Install Code Verify Status during the current session. + * + * @return TRUE The install code was correctly verfied. + * @return FALSE The install code was not verified. + */ + bool GetInstallCodeVerifyStatus(void) const { return mTcatAgent.GetInstallCodeVerifyStatus(); } + private: enum BleState : uint8_t { diff --git a/tests/scripts/expect/_common.exp b/tests/scripts/expect/_common.exp index b94a48583e7..8b9968d343d 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 cf5e05134a2..3ec91d55fc0 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 c0488c03167..302e8db08b6 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 00000000000..90540e44ba4 --- /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 53d344b8984..e30926a0601 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 5ad735943de..23773ec4aba 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 97ac0d653d9..78428b01156 100644 --- a/tools/tcat_ble_client/ble/ble_stream_secure.py +++ b/tools/tcat_ble_client/ble/ble_stream_secure.py @@ -32,6 +32,8 @@ import sys import logging +from cryptography.x509 import load_der_x509_certificate +from cryptography.hazmat.primitives.serialization import (Encoding, PublicFormat) from tlv.tlv import TLV from tlv.tcat_tlv import TcatTLVType from time import time @@ -49,6 +51,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 +106,11 @@ 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(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + self.log_cert_identities() return True async def send(self, bytes): @@ -142,8 +151,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 669f78ccf1f..8702eaa66e2 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,115 @@ 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(code, digestmod=sha256) + hash.update(bless.peer_challenge) + hash.update(bless.peer_public_key) + + 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(pskd, digestmod=sha256) + hash.update(challenge) + hash.update(bless.peer_public_key) + 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: + if len(tlv_response.value) == 8: + bless.peer_challenge = tlv_response.value + else: + print('Challenge format invalid.') + return CommandResultNone() + return CommandResultTLV(tlv_response) + except DataNotPrepared as err: + print('Command failed', err) + + class PingCommand(Command): def get_help_string(self) -> str: @@ -202,7 +317,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 +329,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 9835c950a79..35ea1a2b674 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 7d26864ae78..564db91bb09 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