diff --git a/configs/fne-config.example.yml b/configs/fne-config.example.yml index 73ad134c..a51e4419 100644 --- a/configs/fne-config.example.yml +++ b/configs/fne-config.example.yml @@ -203,6 +203,8 @@ master: # (This field *must* be 32 hex bytes in length or 64 characters # 0 - 9, A - F.) kmfPresharedKey: "000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F" + # Flag indicating whether or not P25 OTAR KMF services will allow a warm start rekey request. + kmfWarmStart: true # Flag indicating whether or not verbose debug logging for P25 OTAR KMF services is enabled. kmfDebug: false diff --git a/src/common/network/BaseNetwork.cpp b/src/common/network/BaseNetwork.cpp index 1e3b403c..c1f5aed5 100644 --- a/src/common/network/BaseNetwork.cpp +++ b/src/common/network/BaseNetwork.cpp @@ -183,7 +183,39 @@ bool BaseNetwork::writeLLAKeyReq(const uint32_t srcId) //Utils::dump("BaseNetwork::writeLLAKeyReq(), KMM Buffer", buffer, modifyKeyCmd.fullLength() + 11U); - return writeMaster({ NET_FUNC::KEY_LLA_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U); + return writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U); +} + +/* Writes a UKEK enc. key request to the network. */ + +bool BaseNetwork::writeUKEKReq(const uint32_t rsi) +{ + using namespace p25::defines; + using namespace p25::kmm; + + if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) + return false; + + uint8_t buffer[DATA_PACKET_LENGTH]; + ::memset(buffer, 0x00U, DATA_PACKET_LENGTH); + + KMMModifyKey modifyKeyCmd; + modifyKeyCmd.setDstLLId(rsi); + modifyKeyCmd.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE); + modifyKeyCmd.setAlgId(ALGO_AES_256); + modifyKeyCmd.setKId(0U); + + KeysetItem ks; + ks.keysetId(0U); + ks.algId(ALGO_AES_256); + ks.keyLength(P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + modifyKeyCmd.setKeysetItem(ks); + + modifyKeyCmd.encode(buffer + 11U); + + //Utils::dump("BaseNetwork::writeUKEKReq(), KMM Buffer", buffer, modifyKeyCmd.fullLength() + 11U); + + return writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U); } /* Writes the local activity log to the network. */ diff --git a/src/common/network/BaseNetwork.h b/src/common/network/BaseNetwork.h index 7e0b1f3e..42105e7b 100644 --- a/src/common/network/BaseNetwork.h +++ b/src/common/network/BaseNetwork.h @@ -477,6 +477,13 @@ namespace network */ bool writeLLAKeyReq(const uint32_t srcId); + /** + * @brief Writes a UKEK enc. key request to the network. + * @param rsi Source Radio ID. + * @returns bool True, if request was sent, otherwise false. + */ + bool writeUKEKReq(const uint32_t rsi); + /** * @brief Writes the local activity log to the network. * \code{.unparsed} diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index 323eec0c..94e685c0 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -74,7 +74,7 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, m_nxdnInCallCallback(nullptr), m_analogInCallCallback(nullptr), m_keyRespCallback(nullptr), - m_llaKeyRespCallback(nullptr) + m_kekKeyRespCallback(nullptr) { assert(!address.empty()); assert(port > 0U); @@ -350,7 +350,7 @@ void Network::clock(uint32_t ms) { NET_FUNC::ACK, &Network::PacketHandler::ack }, { NET_FUNC::KEY_RSP, &Network::PacketHandler::keyResponse }, - { NET_FUNC::KEY_LLA_RSP, &Network::PacketHandler::llaKeyResponse }, + { NET_FUNC::KEY_KEK_RSP, &Network::PacketHandler::kekKeyResponse }, { NET_FUNC::MST_DISC, &Network::PacketHandler::masterDisconnect }, diff --git a/src/common/network/Network.h b/src/common/network/Network.h index dfdb86c5..45f7a0f2 100644 --- a/src/common/network/Network.h +++ b/src/common/network/Network.h @@ -313,10 +313,10 @@ namespace network */ void setKeyResponseCallback(std::function&& callback) { m_keyRespCallback = callback; } /** - * @brief Helper to set the LLA encryption key response callback. + * @brief Helper to set the UKEK/LLA encryption key response callback. * @param callback */ - void setLLAKeyResponseCallback(std::function&& callback) { m_llaKeyRespCallback = callback; } + void setKEKKeyResponseCallback(std::function&& callback) { m_kekKeyRespCallback = callback; } public: /** @@ -441,10 +441,10 @@ namespace network */ std::function m_keyRespCallback; /** - * @brief LLA Encryption Key Response Function Callback. - * (This is called once the master responds to a key LLA request.) + * @brief LLA/UKEK Encryption Key Response Function Callback. + * (This is called once the master responds to a key KEK request.) */ - std::function m_llaKeyRespCallback; + std::function m_kekKeyRespCallback; using PacketHandlerFunc = bool (*)(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length); @@ -543,7 +543,7 @@ namespace network static bool keyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length); /** - * @brief Handles NET_FUNC::KEY_LLA_RSP packets. + * @brief Handles NET_FUNC::KEY_KEK_RSP packets. * @param network Instance of the TrafficNetwork class. * @param peerId Peer ID. * @param streamId Stream ID. @@ -554,7 +554,7 @@ namespace network * @param length Length of buffer. * @returns bool True, if the packet was handled, otherwise false. */ - static bool llaKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, + static bool kekKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length); /** diff --git a/src/common/network/RTPFNEHeader.h b/src/common/network/RTPFNEHeader.h index 6d6255dc..926a7b50 100644 --- a/src/common/network/RTPFNEHeader.h +++ b/src/common/network/RTPFNEHeader.h @@ -68,8 +68,8 @@ namespace network ACK = 0x7EU, //!< Packet Acknowledge NAK = 0x7FU, //!< Packet Negative Acknowledge - KEY_LLA_REQ = 0x80U, //!< Encryption Key LLA Request - KEY_LLA_RSP = 0x81U, //!< Encryption Key LLA Response + KEY_KEK_REQ = 0x80U, //!< Encryption Key LLA/UKEK Request + KEY_KEK_RSP = 0x81U, //!< Encryption Key LLA/UKEK Response RADIO_ALIAS_SYNC = 0x85U, //!< Radio Alias List Sync diff --git a/src/common/network/packet/LLAKeyResponse.cpp b/src/common/network/packet/KEKKeyResponse.cpp similarity index 89% rename from src/common/network/packet/LLAKeyResponse.cpp rename to src/common/network/packet/KEKKeyResponse.cpp index 32b3022c..525f67ca 100644 --- a/src/common/network/packet/LLAKeyResponse.cpp +++ b/src/common/network/packet/KEKKeyResponse.cpp @@ -18,9 +18,9 @@ using namespace network; // Public Class Members // --------------------------------------------------------------------------- -/* Handles NET_FUNC::KEY_LLA_RSP packets. */ +/* Handles NET_FUNC::KEY_KEK_RSP packets. */ -bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, +bool Network::PacketHandler::kekKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now, const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length) { (void)peerId; @@ -69,6 +69,8 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u break; case P25DEF::ALGO_AES_256: + keyLength = P25DEF::MAX_ENC_KEY_LENGTH_BYTES; + break; default: LogWarning(LOG_NET, "PEER %u, unknown algorithm ID $%02X, unable to determine key length", network->m_peerId, ks.algId()); break; @@ -85,7 +87,7 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u } else { if (modifyKey->getDecryptInfoFmt() == KMM_DECRYPT_PEER_ENC) { - LogInfoEx(LOG_NET, "PEER %u, received encrypted LLA enc. key, but no preshared key available, algId = $%02X, kID = $%04X", network->m_peerId, + LogInfoEx(LOG_NET, "PEER %u, received encrypted UKEK/LLA enc. key, but no preshared key available, algId = $%02X, kID = $%04X", network->m_peerId, ks.algId(), ki.kId()); break; } @@ -93,8 +95,8 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u } // fire off key response callback if we have one - if (network->m_llaKeyRespCallback != nullptr) { - network->m_llaKeyRespCallback(modifyKey->getDstLLId(), ki, ks.keyLength()); + if (network->m_kekKeyRespCallback != nullptr) { + network->m_kekKeyRespCallback(modifyKey->getDstLLId(), ki, ks.algId(), ks.keyLength()); } } } diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index 5d32ec45..13aee023 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -517,6 +517,7 @@ namespace p25 REKEY_ACK = 0x1DU, //!< Rekey Ack REKEY_CMD = 0x1EU, //!< Rekey Command + WARM_START_CMD = 0x20U, //!< Warm-Start Command ZEROIZE_CMD = 0x21U, //!< Zeroize Command ZEROIZE_RSP = 0x22U, //!< Zeroize Response diff --git a/src/common/p25/kmm/KMMFactory.cpp b/src/common/p25/kmm/KMMFactory.cpp index 82e1dacf..61f92d79 100644 --- a/src/common/p25/kmm/KMMFactory.cpp +++ b/src/common/p25/kmm/KMMFactory.cpp @@ -94,6 +94,8 @@ std::unique_ptr KMMFactory::create(const uint8_t* data) return decode(new KMMRekeyAck(), data); case KMM_MessageType::REKEY_CMD: return decode(new KMMRekeyCommand(), data); + case KMM_MessageType::WARM_START_CMD: + return decode(new KMMWarmStartCommand(), data); case KMM_MessageType::UNABLE_TO_DECRYPT: return decode(new KMMUnableToDecrypt(), data); default: @@ -161,6 +163,32 @@ std::unique_ptr KMMFactory::create(const uint8_t* data, uint32_t len) if (available < 2U || available < 2U + (uint32_t)data[body + 1U] * 4U) return nullptr; break; + case KMM_MessageType::WARM_START_CMD: + { + if (available < 11U || mnCode != 2U || macType != KMM_MAC::ENH_MAC || + ((data[3U] >> 6U) & 0x03U) != KMM_ResponseKind::IMMEDIATE) + return nullptr; + const uint8_t decryptFormat = data[body]; + if (decryptFormat != KMM_DECRYPT_INSTRUCT_NONE && decryptFormat != KMM_DECRYPT_INSTRUCT_MI) + return nullptr; + const uint32_t miLength = decryptFormat == KMM_DECRYPT_INSTRUCT_MI ? MI_LENGTH_BYTES : 0U; + if (available < 11U + miLength) + return nullptr; + const uint32_t keyLengthOffset = body + 4U + miLength; + const uint8_t keyLength = data[keyLengthOffset]; + const uint32_t keyFormatOffset = keyLengthOffset + 2U; + const uint16_t sln = (uint16_t)(((uint16_t)data[keyFormatOffset + 1U] << 8U) | + data[keyFormatOffset + 2U]); + const uint16_t temporaryKId = (uint16_t)(((uint16_t)data[keyFormatOffset + 3U] << 8U) | + data[keyFormatOffset + 4U]); + if (data[body + 1U] != ALGO_AES_256 || keyLength != P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES || + data[keyLengthOffset + 1U] != ALGO_AES_256 || temporaryKId == 0U || + available != 11U + miLength + keyLength || + (data[keyFormatOffset] & 0xE0U) != KMM_KEY_FORMAT_TEK || + (data[keyFormatOffset] & 0x1FU) != 0U || sln != 0U) + return nullptr; + } + break; case KMM_MessageType::NO_SERVICE: case KMM_MessageType::ZEROIZE_CMD: case KMM_MessageType::ZEROIZE_RSP: diff --git a/src/common/p25/kmm/KMMFactory.h b/src/common/p25/kmm/KMMFactory.h index a6f47c6a..c2ad95db 100644 --- a/src/common/p25/kmm/KMMFactory.h +++ b/src/common/p25/kmm/KMMFactory.h @@ -37,6 +37,7 @@ #include "common/p25/kmm/KMMRekeyAck.h" #include "common/p25/kmm/KMMRekeyCommand.h" #include "common/p25/kmm/KMMUnableToDecrypt.h" +#include "common/p25/kmm/KMMWarmStartCommand.h" #include "common/p25/kmm/KMMZeroize.h" namespace p25 diff --git a/src/common/p25/kmm/KMMFrame.cpp b/src/common/p25/kmm/KMMFrame.cpp index e7843a24..81990547 100644 --- a/src/common/p25/kmm/KMMFrame.cpp +++ b/src/common/p25/kmm/KMMFrame.cpp @@ -78,7 +78,7 @@ void KMMFrame::generateMAC(uint8_t* kek, uint8_t* data) return; } - if (m_macKId == 0U) { + if (m_macKId == 0U && m_messageId != KMM_MessageType::WARM_START_CMD) { ::LogError(LOG_P25, "KMMFrame::generateMAC(), MAC key ID is not set, aborting MAC signing"); return; } diff --git a/src/common/p25/kmm/KMMWarmStartCommand.cpp b/src/common/p25/kmm/KMMWarmStartCommand.cpp new file mode 100644 index 00000000..d34dbfef --- /dev/null +++ b/src/common/p25/kmm/KMMWarmStartCommand.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ + + #include "Defines.h" +#include "p25/P25Defines.h" +#include "p25/kmm/KMMWarmStartCommand.h" + +using namespace p25::defines; +using namespace p25::kmm; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the KMMWarmStartCommand class. */ + +KMMWarmStartCommand::KMMWarmStartCommand() : KMMFrame(), + m_decryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE), + m_kekAlgId(ALGO_UNENCRYPT), + m_kekKId(0U), + m_keyLength(0U), + m_tekAlgId(ALGO_UNENCRYPT), + m_key(), + m_mi() +{ + m_messageId = KMM_MessageType::WARM_START_CMD; + m_respKind = KMM_ResponseKind::IMMEDIATE; + ::memset(m_mi, 0x00U, sizeof(m_mi)); +} + +/* Gets the byte length of this KMMWarmStartCommand. */ + +uint32_t KMMWarmStartCommand::length() const +{ + return KMMFrame::length() + 11U + (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI ? P25DEF::MI_LENGTH_BYTES : 0U) + + m_keyLength; +} + +/* Decodes a KMM Warm-Start Command from the given data buffer. */ + +bool KMMWarmStartCommand::decode(const uint8_t* data) +{ + + assert(data != nullptr); + + KMMFrame::decodeHeader(data); + + uint32_t offset = 10U + m_bodyOffset; + m_decryptInfoFmt = data[offset++]; // Decryption information format + m_kekAlgId = data[offset++]; // KEK algorithm identifier + m_kekKId = GET_UINT16(data, offset); offset += 2U; // KEK key identifier + + if (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI) { + ::memcpy(m_mi, data + offset, sizeof(m_mi)); + offset += sizeof(m_mi); + } + + m_keyLength = data[offset++]; + m_tekAlgId = data[offset++]; + m_key.keyFormat(data[offset++] & 0xE0U); + + uint16_t sln = GET_UINT16(data, offset); offset += 2U; + m_key.sln(sln); + + uint16_t kid = GET_UINT16(data, offset); offset += 2U; + m_key.kId(kid); + + m_key.setKey(data + offset, m_keyLength); + + return true; +} + +/* Encodes this KMM Warm-Start Command into the given data buffer. */ + +void KMMWarmStartCommand::encode(uint8_t* data) +{ + assert(data != nullptr); + m_messageLength = length(); + + KMMFrame::encodeHeader(data); + + uint32_t offset = 10U + m_bodyOffset; + data[offset++] = m_decryptInfoFmt; // Decryption information format + data[offset++] = m_kekAlgId; // KEK algorithm identifier + SET_UINT16(m_kekKId, data, offset); offset += 2U; // KEK key identifier + + if (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI) { + ::memcpy(data + offset, m_mi, sizeof(m_mi)); + offset += sizeof(m_mi); + } + + data[offset++] = m_keyLength; // TEK key length in bytes + data[offset++] = m_tekAlgId; // TEK algorithm identifier + data[offset++] = m_key.keyFormat(); // TEK key format + + uint16_t sln = m_key.sln(); + SET_UINT16(sln, data, offset); + offset += 2U; + + uint16_t kid = m_key.kId(); + SET_UINT16(kid, data, offset); + offset += 2U; + + m_key.getKey(data + offset); +} + +/* Returns a string representation of this KMM Warm-Start Command. */ + +std::string KMMWarmStartCommand::toString() +{ + return std::string("KMM, WARM_START_CMD (Warm-Start Command)"); +} + +/* +** Encryption data +*/ + +/* Sets the encryption message indicator. */ + +void KMMWarmStartCommand::setMI(const uint8_t* mi) +{ + assert(mi != nullptr); + ::memcpy(m_mi, mi, sizeof(m_mi)); +} + +/* Gets the encryption message indicator. */ + +void KMMWarmStartCommand::getMI(uint8_t* mi) const +{ + assert(mi != nullptr); + ::memcpy(mi, m_mi, sizeof(m_mi)); +} + +// --------------------------------------------------------------------------- +// Protected Class Members +// --------------------------------------------------------------------------- + +/* Internal helper to copy the the class. */ + +void KMMWarmStartCommand::copy(const KMMWarmStartCommand& data) +{ + KMMFrame::copy(data); + + m_decryptInfoFmt = data.m_decryptInfoFmt; + + m_kekAlgId = data.m_kekAlgId; + m_kekKId = data.m_kekKId; + + m_keyLength = data.m_keyLength; + + m_tekAlgId = data.m_tekAlgId; + + m_key = data.m_key; + + if (data.m_mi != nullptr) { + ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); + ::memcpy(m_mi, data.m_mi, MI_LENGTH_BYTES); + } +} diff --git a/src/common/p25/kmm/KMMWarmStartCommand.h b/src/common/p25/kmm/KMMWarmStartCommand.h new file mode 100644 index 00000000..30410c16 --- /dev/null +++ b/src/common/p25/kmm/KMMWarmStartCommand.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file KMMWarmStartCommand.h + * @ingroup p25_kmm + * @file KMMWarmStartCommand.cpp + * @ingroup p25_kmm + */ +#if !defined(__P25_KMM__KMM_WARM_START_COMMAND_H__) +#define __P25_KMM__KMM_WARM_START_COMMAND_H__ + +#include "common/Defines.h" +#include "common/p25/kmm/KMMFrame.h" +#include "common/p25/kmm/KeysetItem.h" + +namespace p25 +{ + namespace kmm + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents a KMM Warm Start Command frame. + * @ingroup p25_kmm + */ + class DVM_COMMON_API KMMWarmStartCommand : public KMMFrame { + public: + /** + * @brief Initializes a new instance of the KMMWarmStartCommand class. + */ + KMMWarmStartCommand(); + /** + * @brief Finalizes a instance of the KMMWarmStartCommand class. + */ + ~KMMWarmStartCommand() = default; + + + /** + * @brief Gets the byte length of this KMMFrame. + * @return uint32_t Length of KMMFrame. + */ + uint32_t length() const override; + + /** + * @brief Decode a KMM Warm-Start. + * @param[in] data Buffer containing KMM frame data to decode. + * @returns bool True, if decoded, otherwise false. + */ + bool decode(const uint8_t* data) override; + /** + * @brief Encode a KMM Warm-Start. + * @param[out] data Buffer to encode KMM frame data to. + */ + void encode(uint8_t* data) override; + + /** + * @brief Returns a string that represents the current KMM frame. + * @returns std::string String representation of the KMM frame. + */ + std::string toString() override; + + /** @name Encryption data */ + /** + * @brief Sets the encryption message indicator. + * @param[in] mi Buffer containing the 9-byte Message Indicator. + */ + void setMI(const uint8_t* mi); + /** + * @brief Gets the encryption message indicator. + * @param[out] mi Buffer containing the 9-byte Message Indicator. + */ + void getMI(uint8_t* mi) const; + /** @} */ + + public: + /** + * @brief Gets or sets the decryption information format. + */ + DECLARE_PROPERTY(uint8_t, decryptInfoFmt, DecryptInfoFmt); + + /** + * @brief Gets or sets the KEK algorithm identifier. + */ + DECLARE_PROPERTY(uint8_t, kekAlgId, KEKAlgId); + /** + * @brief Gets or sets the KEK key identifier. + */ + DECLARE_PROPERTY(uint16_t, kekKId, KEKKId); + /** + * @brief Gets or sets the key length. + */ + DECLARE_PROPERTY(uint8_t, keyLength, KeyLength); + /** + * @brief Gets or sets the TEK algorithm identifier. + */ + DECLARE_PROPERTY(uint8_t, tekAlgId, TEKAlgId); + /** + * @brief Gets or sets the key item. + */ + DECLARE_PROPERTY(KeyItem, key, Key); + + DECLARE_COPY(KMMWarmStartCommand); + + private: + uint8_t m_mi[P25DEF::MI_LENGTH_BYTES]; + }; + } // namespace kmm +} // namespace p25 + +#endif // __P25_KMM__KMM_WARM_START_COMMAND_H__ diff --git a/src/fne/CryptoContainer.cpp b/src/fne/CryptoContainer.cpp index 90dae224..93c9a193 100644 --- a/src/fne/CryptoContainer.cpp +++ b/src/fne/CryptoContainer.cpp @@ -258,6 +258,26 @@ void CryptoContainer::addEntry(EKCKeyItem key) } } +/* Adds or replaces a User Key Encryption Key by RSI and KID. */ + +void CryptoContainer::addUKEK(EKCKeyItem key) +{ + if (key.isInvalid()) + return; + + std::lock_guard lock(s_mutex); + auto it = std::find_if(m_ukeks.begin(), m_ukeks.end(), + [&](const EKCKeyItem& item) + { + return item.rsiId() == key.rsiId() && item.kId() == key.kId(); + }); + if (it != m_ukeks.end()) { + *it = key; + } else { + m_ukeks.push_back(key); + } +} + /* Erases an existing entry from the lookup table by the specified unique ID. */ void CryptoContainer::eraseEntry(uint32_t id) diff --git a/src/fne/CryptoContainer.h b/src/fne/CryptoContainer.h index 7167bbd2..98d693a1 100644 --- a/src/fne/CryptoContainer.h +++ b/src/fne/CryptoContainer.h @@ -215,6 +215,11 @@ public: * @param key Key Item. */ void addEntry(EKCKeyItem key); + /** + * @brief Adds or replaces a User Key Encryption Key. + * @param key Key Item representing the UKEK. + */ + void addUKEK(EKCKeyItem key); /** * @brief Erases an existing entry from the lookup table by the specified unique ID. * @param id Unique ID to erase. diff --git a/src/fne/network/P25OTARService.cpp b/src/fne/network/P25OTARService.cpp index c0ca6c76..002d24c3 100644 --- a/src/fne/network/P25OTARService.cpp +++ b/src/fne/network/P25OTARService.cpp @@ -29,6 +29,13 @@ using namespace p25::kmm; #include #include #include +#include +#include +#include + +#if defined(ENABLE_SSL) +#include +#endif // --------------------------------------------------------------------------- // Macros @@ -46,10 +53,38 @@ using namespace p25::kmm; #define MAX_THREAD_CNT 4U +#define UPSTREAM_KEY_TIMEOUT_MS 1500U +// TIA-102.AACA-D section 7 permits the RK3 tTO response timer to be configured +// from 5 through 90 seconds (5-second default) - Harris subscribers can require +// more than the default to originate the encrypted Warm-Start acknowledgment so we +// set 10 seconds as the timeout value +#define WARM_START_TIMEOUT_MS 10000U + // --------------------------------------------------------------------------- // Global Functions // --------------------------------------------------------------------------- +/** + * @brief Returns the current time in milliseconds since an unspecified starting point (monotonic time). + * @return Current time in milliseconds. + */ +static uint64_t monotonicMilliseconds() +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +/** + * @brief Erases a block of memory by setting all bytes to zero. + * @param data Pointer to the memory block to erase. + * @param length Length of the memory block in bytes. + */ +static void eraseBytes(uint8_t* data, size_t length) +{ + volatile uint8_t* p = data; + while (length-- > 0U) + *p++ = 0U; +} + /** * @brief Determines if a KMM message type is supported. * @param messageId The KMM message type identifier. @@ -110,6 +145,7 @@ bool allowsUnauthenticatedKMM(uint8_t messageId) bool requiresEncryptedKMM(uint8_t messageId) { switch (messageId) { + case KMM_MessageType::WARM_START_CMD: case KMM_MessageType::HELLO: case KMM_MessageType::NO_SERVICE: case KMM_MessageType::DEREG_CMD: @@ -123,6 +159,36 @@ bool requiresEncryptedKMM(uint8_t messageId) } } +/** + * @brief Converts a peer key response into the local key representation. + * @param key The peer key item to convert. + * @param algId The algorithm ID associated with the key. + * @param keyLength The length of the key material. + * @param rsi The requesting RSI (default is 0U). + * @return The local representation of the key as an EKCKeyItem. + */ +static EKCKeyItem makeEKCKey(const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength, uint32_t rsi = 0U) +{ + if (keyLength == 0U || keyLength > P25DEF::MAX_ENC_KEY_LENGTH_BYTES) + return EKCKeyItem(); + + uint8_t material[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U }; + key.getKey(material); + + std::ostringstream hex; + hex << std::hex << std::uppercase << std::setfill('0'); + for (uint8_t i = 0U; i < keyLength; ++i) + hex << std::setw(2) << (uint32_t)material[i]; + + EKCKeyItem result; + result.rsiId(rsi); + result.algId(algId); + result.kId(key.kId()); + result.sln(key.sln()); + result.keyMaterial(hex.str()); + return result; +} + // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- @@ -173,68 +239,21 @@ P25OTARService::P25OTARService(TrafficNetwork* network, P25PacketData* packetDat P25OTARService::~P25OTARService() { + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + for (auto& entry : m_warmStartTransactions) + eraseBytes(entry.second.temporaryTEK.data(), entry.second.temporaryTEK.size()); + + m_warmStartTransactions.clear(); + } + if (m_frameQueue != nullptr) delete m_frameQueue; if (m_socket != nullptr) delete m_socket; } -/* Resolves the required outer-encryption context for a generated response. */ - -bool P25OTARService::resolveResponseSecurity(const uint8_t* data, uint32_t len, bool& encrypted, - uint8_t& algoId, uint16_t& kid) const -{ - if (data == nullptr) { - LogError(LOG_P25, P25_KMM_STR ", cannot resolve outer encryption for a null generated response"); - return false; - } - - const uint8_t messageId = len > 0U ? data[0U] : KMM_MessageType::NULL_CMD; - const uint32_t declaredLength = len >= 3U ? - ((((uint32_t)data[1U] << 8U) | data[2U]) + 3U) : 0U; - if (len < 10U || len > 512U || declaredLength != len) { - LogError(LOG_P25, P25_KMM_STR ", cannot outer-encrypt generated KMM: invalid length, messageId = $%02X, declared/actual = %u/%u", - messageId, declaredLength, len); - return false; - } - - if (encrypted || !requiresEncryptedKMM(messageId)) - return true; - - // This is locally generated and its total length has already been checked. - // Decode only the common header: the bounded air-facing factory intentionally - // rejects nested-count messages such as RK3 until it has a structural validator. - KMMOpaqueFrame response; - if (!response.decode(data) || response.getMACType() != KMM_MAC::ENH_MAC || - response.getMACAlgId() != ALGO_AES_256 || response.getMACKId() == 0U) { - LogError(LOG_P25, P25_KMM_STR ", generated KMM requires outer encryption but has no usable enhanced-MAC TEK context, messageId = $%02X, macType = $%02X, macAlgId = $%02X, macKId = $%04X", - messageId, response.getMACType(), response.getMACAlgId(), response.getMACKId()); - return false; - } - - EKCKeyItem outerTek = m_network->m_cryptoLookup->find(response.getMACKId()); - if (outerTek.isInvalid()) { - LogError(LOG_P25, P25_KMM_STR ", generated KMM requires outer encryption but MAC TEK was not found, messageId = $%02X, algId = $%02X, kId = $%04X", - messageId, response.getMACAlgId(), response.getMACKId()); - return false; - } - if (outerTek.algId() != response.getMACAlgId()) { - LogError(LOG_P25, P25_KMM_STR ", generated KMM outer TEK algorithm mismatch, messageId = $%02X, required/found algId = $%02X/$%02X, kId = $%04X", - messageId, response.getMACAlgId(), outerTek.algId(), response.getMACKId()); - return false; - } - - algoId = response.getMACAlgId(); - kid = response.getMACKId(); - encrypted = true; - if (m_debug) { - LogDebugEx(LOG_P25, "P25OTARService::resolveResponseSecurity()", - "selected outer encryption for generated KMM, messageId = $%02X, algId = $%02X, kId = $%04X", - messageId, algoId, kid); - } - return true; -} - /* Helper used to process KMM frames from PDU data. */ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId, uint8_t n, bool encrypted, @@ -262,8 +281,8 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId ::memcpy(resolvedMI, mi, MI_LENGTH_BYTES); } else { - // AACA-D 12.2 encrypted DLD requires the Auxiliary ES context. - // Do not guess at a non-standard payload-embedded layout. + // AACA-D 12.2 encrypted DLD requires the Auxiliary ES context + // do not guess at a non-standard payload-embedded layout LogError(LOG_P25, P25_KMM_STR ", encrypted DLD missing Auxiliary ES metadata"); sendNack(PDUAckType::NACK_ILLEGAL); return false; @@ -287,9 +306,9 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId } uint32_t payloadSize = 0U; - // The payload is already decrypted. Preserve the outer encryption context so + // the payload is already decrypted -- preserve the outer encryption context so // processKMM can construct the encrypted/authenticated NACKs required by - // AACA-D 6.22 and 7.4 without decrypting the request a second time. + // AACA-D 6.22 and 7.4 without decrypting the request a second time std::vector> additionalResponses; UInt8Array pduUserData = processKMM(kmmPayload.get(), len, llId, false, &payloadSize, encrypted ? resolvedAlgoId : ALGO_UNENCRYPT, encrypted ? resolvedKId : 0U, @@ -308,9 +327,9 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId return false; } - // Complete the inbound confirmed-delivery transaction before starting the - // independent outbound KMM transaction. The SU is still in stop-and-wait - // for this N(R) and may discard an RK3 transmitted ahead of it. + // complete the inbound confirmed-delivery transaction before starting the + // independent outbound KMM transaction -- the SU is still in stop-and-wait + // for this N(R) and may discard an RK3 transmitted ahead of it m_packetData->write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, n, llId, false); // lambda function to handle dispatching of KMM responses, including encryption if required @@ -319,6 +338,21 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId ::memset(responseMI, 0x00U, sizeof(responseMI)); const uint8_t* outgoing = response; + const bool traceWarmStart = m_debug && response != nullptr && responseLength >= 10U && response[0U] == KMM_MessageType::WARM_START_CMD; + if (traceWarmStart) { + const bool hasMessageNumber = ((response[3U] >> 4U) & 0x03U) == 0x02U; + const uint16_t declaredLength = ((uint16_t)response[1U] << 8U) | response[2U]; + const uint16_t messageNumber = hasMessageNumber && responseLength >= 12U ? (((uint16_t)response[10U] << 8U) | response[11U]) : 0U; + const uint32_t dstRSI = GET_UINT24(response, 4U); + const uint32_t srcRSI = GET_UINT24(response, 7U); + + LogDebugEx(LOG_P25, "P25OTARService::dispatchResponse()", + "outbound WARM_START_CMD, len = %u, declaredLen = %u, control = $%02X, responseKind = %u, hasMN = %u, MN = %u, complete = %u, dstRSI = %u, srcRSI = %u, outerEncrypted = %u, outerAlgId = $%02X, outerKId = $%04X", + responseLength, declaredLength, response[3U], (response[3U] >> 6U) & 0x03U, hasMessageNumber, messageNumber, (response[3U] & 0x01U) == 0U, dstRSI, srcRSI, responseEncrypted, resolvedAlgoId, resolvedKId); + + Utils::dump(1U, "P25OTARService::dispatchResponse(), Outbound WARM_START_CMD KMM", response, responseLength); + } + UInt8Array encryptedResponse; if (responseEncrypted) { encryptedResponse = cryptKMM(resolvedAlgoId, resolvedKId, responseMI, response, responseLength, true); @@ -328,6 +362,13 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId outgoing = encryptedResponse.get(); } + if (traceWarmStart) { + if (responseEncrypted) + Utils::dump(1U, "P25OTARService::dispatchResponse(), WARM_START_CMD Auxiliary ES MI", responseMI, MI_LENGTH_BYTES); + + Utils::dump(1U, "P25OTARService::dispatchResponse(), WARM_START_CMD KMM passed to PDU writer", outgoing, responseLength); + } + return m_packetData->write_PDU_KMM(outgoing, responseLength, llId, responseEncrypted, resolvedAlgoId, resolvedKId, responseEncrypted ? responseMI : nullptr); }; @@ -359,6 +400,99 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId void P25OTARService::clock(uint32_t ms) { + const uint64_t now = monotonicMilliseconds(); + std::vector timedOutWarmStarts; + + if (m_network->m_kmfWarmStart) { + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + // a missing Warm-Start acknowledgment does not prevent the Rekey + // attempt -- advance exactly once when the response timer expires + for (auto it = m_warmStartTransactions.begin(); it != m_warmStartTransactions.end();) { + if (it->second.deadline > now) { + ++it; + continue; + } + + if (it->second.state == WarmStartState::WAIT_WARM_ACK) { + LogWarning(LOG_P25, P25_KMM_STR ", Warm-Start acknowledgment timed out; beginning Rekey, RSI = %u", it->first); + it->second.state = WarmStartState::WAIT_REKEY_ACK; + it->second.deadline = now + WARM_START_TIMEOUT_MS; + timedOutWarmStarts.push_back(it->second); + ++it; + } else { + LogWarning(LOG_P25, P25_KMM_STR ", rekey transaction timed out, RSI = %u", it->first); + eraseBytes(it->second.temporaryTEK.data(), it->second.temporaryTEK.size()); + it = m_warmStartTransactions.erase(it); + } + } + } + + // construct and send Rekey outside the transaction lock -- the command is + // protected by the temporary TEK sent in the original Warm Start + for (WarmStartTransaction& transaction : timedOutWarmStarts) { + uint32_t payloadSize = 0U; + KMMAuthContext auth; + + std::vector> additionalResponses; + + // prepare the Rekey command for transmission + UInt8Array rekey = write_KMM_Rekey_Command(transaction.llId, transaction.rsi, + KMM_HelloFlag::REKEY_REQUEST_UKEK, &payloadSize, auth, &additionalResponses, true); + + bool sent = rekey != nullptr && payloadSize > 0U; + uint8_t algoId = ALGO_UNENCRYPT; + uint16_t kid = 0U; + bool encrypted = true; + if (sent) + sent = resolveResponseSecurity(rekey.get(), payloadSize, encrypted, algoId, kid); + + // lambda function to send the Rekey command, either via the data link independent endpoint or the standard PDU writer + auto sendRekey = [&](const uint8_t* response, uint32_t responseLength) -> bool { + uint8_t mi[MI_LENGTH_BYTES] = { 0U }; + UInt8Array encryptedResponse = cryptKMM(algoId, kid, mi, response, responseLength, true); + if (encryptedResponse == nullptr) + return false; + + if (transaction.dataLinkIndependent) { + if (!transaction.hasDLIEndpoint || m_frameQueue == nullptr) + return false; + + std::vector datagram(14U + responseLength, 0U); + datagram[0U] = 0U; + datagram[1U] = MFG_STANDARD; + datagram[2U] = algoId; + SET_UINT16(kid, datagram.data(), 3U); + ::memcpy(datagram.data() + 5U, mi, MI_LENGTH_BYTES); + ::memcpy(datagram.data() + 14U, encryptedResponse.get(), responseLength); + return m_frameQueue->write(datagram.data(), (uint32_t)datagram.size(), + transaction.dliAddress, transaction.dliAddressLength); + } + + return m_packetData->write_PDU_KMM(encryptedResponse.get(), responseLength, + transaction.llId, true, algoId, kid, mi); + }; + + if (sent) + sent = sendRekey(rekey.get(), payloadSize); + + if (sent) { + for (const std::vector& response : additionalResponses) { + if (!sendRekey(response.data(), (uint32_t)response.size())) { + sent = false; + break; + } + } + } + + LogInfoEx(LOG_P25, P25_KMM_STR ", Warm-Start timeout, rekey %s, RSI = %u", sent ? "start" : "failed", transaction.rsi); + if (!sent) + eraseWarmStart(transaction.rsi); + } + } + if (m_socket != nullptr) { sockaddr_storage address; uint32_t addrLen; @@ -421,6 +555,16 @@ bool P25OTARService::open(const std::string& address, uint16_t port) void P25OTARService::close() { + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + // erase all warm start TEKs from memory + for (auto& entry : m_warmStartTransactions) + eraseBytes(entry.second.temporaryTEK.data(), entry.second.temporaryTEK.size()); + m_warmStartTransactions.clear(); + } + if (m_socket != nullptr) { m_threadPool.stop(); m_threadPool.wait(); @@ -429,10 +573,91 @@ void P25OTARService::close() } } +/* Supplies a TEK returned asynchronously by an upstream master. */ + +void P25OTARService::cacheUpstreamTEK(const KeyItem& key, uint8_t algId, uint8_t keyLength) +{ + // validate the incoming upstream TEK before caching it + if (key.keyFormat() != P25DEF::KMM_KEY_FORMAT_TEK || key.kId() == 0U || + algId == ALGO_UNENCRYPT || keyLength == 0U || keyLength > P25DEF::MAX_ENC_KEY_LENGTH_BYTES) { + LogWarning(LOG_P25, P25_KMM_STR ", ignoring invalid upstream TEK response"); + return; + } + + UpstreamKey value; + value.key = key; + value.algorithmId = algId; + value.keyLength = keyLength; + value.receivedAt = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + + // scope is intentional + { + std::lock_guard lock(m_upstreamKeyMutex); + m_upstreamTEKs[((uint32_t)algId << 16U) | key.kId()] = value; + } + + m_upstreamKeyReady.notify_all(); +} + +/* Supplies a UKEK returned asynchronously by an upstream master. */ + +void P25OTARService::cacheUpstreamUKEK(uint32_t rsi, const KeyItem& key, uint8_t algId, uint8_t keyLength) +{ + // validate the incoming upstream UKEK before caching it + if (rsi == 0U || key.keyFormat() != P25DEF::KMM_KEY_FORMAT_KEK || + algId != ALGO_AES_256 || keyLength != P25DEF::MAX_ENC_KEY_LENGTH_BYTES) { + LogWarning(LOG_P25, P25_KMM_STR ", ignoring invalid upstream UKEK response"); + return; + } + + UpstreamKey value; + value.key = key; + value.algorithmId = algId; + value.keyLength = keyLength; + value.receivedAt = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + + // scope is intentional + { + std::lock_guard lock(m_upstreamKeyMutex); + m_upstreamUKEKs[rsi] = value; + } + + m_upstreamKeyReady.notify_all(); +} + // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- +/* Atomically reserves a contiguous range of outbound KMM message numbers. */ + +uint16_t P25OTARService::reserveOutboundMessageNumbers(uint32_t rsi, uint16_t count) +{ + assert(rsi != 0U); + assert(count != 0U); + + uint16_t first = 3U; + { + std::lock_guard lock(m_outboundMessageNumberMutex); + auto it = m_rsiMessageNumber.find(rsi); + if (it != m_rsiMessageNumber.end()) + first = it->second; + + // reserve before constructing or dispatching the KMM -- a failed attempt + // may skip numbers, which is valid; reusing a concurrently allocated MN + // is not + m_rsiMessageNumber[rsi] = (uint16_t)(first + count); + } + + if (m_debug) { + LogDebugEx(LOG_P25, "P25OTARService::reserveOutboundMessageNumbers()", + "reserved outbound KMM MN range, RSI = %u, first = %u, count = %u, next = %u", + rsi, first, count, (uint16_t)(first + count)); + } + + return first; +} + /* Process a data frames from the network. */ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) @@ -495,6 +720,21 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) return; } + if (network->m_network->m_kmfWarmStart) { + // check if the KMM response is a warm start command and update the warm start transaction accordingly + if (pduUserData[0U] == KMM_MessageType::WARM_START_CMD && payloadSize >= 10U) { + const uint32_t rsi = GET_UINT24(pduUserData.get(), 4U); + + std::lock_guard lock(network->m_warmStartMutex); + auto it = network->m_warmStartTransactions.find(rsi); + if (it != network->m_warmStartTransactions.end()) { + it->second.dliAddress = req->address; + it->second.dliAddressLength = req->addrLen; + it->second.hasDLIEndpoint = true; + } + } + } + bool responseEncrypted = encrypted; if (!network->resolveResponseSecurity(pduUserData.get(), payloadSize, responseEncrypted, algoId, kid)) { @@ -508,6 +748,8 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) auto sendResponse = [&](const uint8_t* response, uint32_t responseLength) -> bool { uint8_t responseMI[MI_LENGTH_BYTES] = { 0U }; const uint8_t* outgoing = response; + + // encrypt the response if required UInt8Array encryptedResponse; if (responseEncrypted) { encryptedResponse = network->cryptKMM(algoId, kid, responseMI, response, responseLength, true); @@ -553,72 +795,6 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) } } -/* Encrypt/decrypt KMM frame. */ - -UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, const uint8_t* buffer, uint32_t len, bool encrypt) -{ - assert(buffer != nullptr); - - // AACA-D 12.4: a DLD KMM is at most 512 octets including the three - // octets excluded from Message Length - if (len < 10U || len > 512U) { - LogError(LOG_P25, P25_KMM_STR ", invalid DLD KMM length, len = %u", len); - return nullptr; - } - - P25Crypto crypto; - if (!encrypt) - crypto.setMI(mi); - else { - crypto.generateMI(); - crypto.getMI(mi); - } - - UInt8Array outBuffer = std::make_unique(len); - ::memset(outBuffer.get(), 0x00U, len); - ::memcpy(outBuffer.get(), buffer, len); - - if (algoId == P25DEF::ALGO_UNENCRYPT) - return outBuffer; - - /* - ** bryanb: Architecturally this is a problem. Because KMF services would essentially be limited to the local FNE - ** because we aren't performing FNE KEY_REQ's to upstream peer'ed FNEs to find the key used to encrypt the KMM. - */ - - ::EKCKeyItem keyItem = m_network->m_cryptoLookup->find(kid); - // find() searches the existing EKC collection only. UKEKs are kept - // in the separate collection and are available only via findUKEK(). - if (!keyItem.isInvalid()) { - uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; - ::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); - uint8_t keyLength = keyItem.getKey(key); - - if (m_network->m_debug) - LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength); - - LogInfoEx(LOG_P25, P25_KMM_STR ", algId = $%02X, kID = $%04X", algoId, kid); - crypto.setTEKAlgoId(algoId); - crypto.setKey(key, keyLength); - crypto.generateKeystream(); - - switch (algoId) { - case P25DEF::ALGO_AES_256: - crypto.cryptAES_PDU(outBuffer.get(), len); - return outBuffer; - default: - LogError(LOG_P25, "unsupported KEK algorithm, algoId = $%02X", algoId); - break; - } - } - else { - LogError(LOG_P25, P25_KMM_STR ", unable to %s outer KMM, KEK not found, algId = $%02X, kId = $%04X", - encrypt ? "encrypt" : "decrypt", algoId, kid); - } - - return nullptr; -} - /* Helper used to process KMM frames. */ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_t llId, bool encrypted, uint32_t* payloadSize, @@ -678,14 +854,16 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ KMMOpaqueFrame opaque; opaque.decode(buffer.get()); + // check if the opaque frame meets the criteria for generating a negative acknowledgment if (opaque.getDstLLId() == WUID_FNE && opaque.getSrcLLId() != 0U && (llId == 0U || opaque.getSrcLLId() == llId) && opaque.getResponseKind() == KMM_ResponseKind::IMMEDIATE && opaque.getMACType() == KMM_MAC::ENH_MAC && opaque.getMACAlgId() == ALGO_AES_256 && (opaque.getMACFormat() == KMM_MAC_FORMAT_CBC || opaque.getMACFormat() == KMM_MAC_FORMAT_CMAC)) { - EKCKeyItem key = m_network->m_cryptoLookup->find(opaque.getMACKId()); + EKCKeyItem key = resolveTEK(opaque.getMACKId(), opaque.getMACAlgId(), opaque.getSrcLLId()); if (!key.isInvalid() && key.algId() == opaque.getMACAlgId()) { uint8_t tek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U }; key.getKey(tek); + // verify the MAC of the opaque frame using the resolved TEK if (opaque.verifyMAC(tek, buffer.get(), len) && algoId != ALGO_UNENCRYPT) { KMMAuthContext nackAuth; nackAuth.authenticated = true; @@ -730,8 +908,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ // AACA-D Table 79 requires DLI registration -- registration itself and a // repeated deregistration are allowed before registered service exists - if (dataLinkIndependent && frame->getMessageId() != KMM_MessageType::REG_CMD && - frame->getMessageId() != KMM_MessageType::DEREG_CMD && + if (dataLinkIndependent && frame->getMessageId() != KMM_MessageType::REG_CMD && frame->getMessageId() != KMM_MessageType::DEREG_CMD && m_dliRegistered.find(frame->getSrcLLId()) == m_dliRegistered.end()) { LogWarning(LOG_P25, P25_KMM_STR ", rejecting unregistered DLI RSI = %u", frame->getSrcLLId()); if (frame->getResponseKind() == KMM_ResponseKind::IMMEDIATE) @@ -746,7 +923,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ // the request MAC TEK; when it is unavailable, the outer AES TEK is the // only established common authentication key available to this service if (!auth.authenticated && algoId == ALGO_AES_256 && kid != 0U) { - EKCKeyItem outerTek = m_network->m_cryptoLookup->find(kid); + EKCKeyItem outerTek = resolveTEK(kid, algoId, frame->getSrcLLId()); if (!outerTek.isInvalid() && outerTek.algId() == ALGO_AES_256) { auth.authenticated = true; auth.algorithmId = ALGO_AES_256; @@ -755,8 +932,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ } } - if (frame->getResponseKind() != KMM_ResponseKind::IMMEDIATE || - algoId == ALGO_UNENCRYPT || !auth.authenticated) + if (frame->getResponseKind() != KMM_ResponseKind::IMMEDIATE || algoId == ALGO_UNENCRYPT || !auth.authenticated) return nullptr; return write_KMM_NegativeAck(frame->getSrcLLId(), frame->getMessageId(), @@ -778,13 +954,26 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ return nullptr; } - EKCKeyItem macTek = m_network->m_cryptoLookup->find(frame->getMACKId()); + // determine the appropriate MAC key ID and resolve the corresponding TEK + uint16_t macKeyId = frame->getMACKId(); + EKCKeyItem macTek; + if (frame->getMessageId() == KMM_MessageType::REKEY_ACK && + static_cast(frame.get())->getAckMessageId() == KMM_MessageType::WARM_START_CMD) { + std::lock_guard lock(m_warmStartMutex); + auto it = m_warmStartTransactions.find(frame->getSrcLLId()); + if (it != m_warmStartTransactions.end()) + macKeyId = it->second.temporaryKId; + } + + macTek = resolveTEK(macKeyId, frame->getMACAlgId(), frame->getSrcLLId()); + if (macTek.isInvalid() || macTek.algId() != frame->getMACAlgId()) { LogWarning(LOG_P25, P25_KMM_STR ", MAC TEK not found, RSI = %u", frame->getSrcLLId()); return makeNack(KMM_Status::ITEM_NOT_EXIST); } auth.authenticated = true; + auth.keyId = macKeyId; uint8_t tek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; ::memset(tek, 0x00U, sizeof(tek)); @@ -809,6 +998,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ const uint32_t rsi = frame->getSrcLLId(); const uint16_t received = frame->getMessageNumber(); + // compute a fingerprint for the received message to detect replay attacks uint64_t fingerprint = 1469598103934665603ULL; const uint32_t declared = (((uint32_t)buffer[1U] << 8U) | buffer[2U]) + 3U; for (uint32_t i = 0U; i < declared; ++i) { @@ -816,6 +1006,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ fingerprint *= 1099511628211ULL; } + // check the last received message number for this RSI to detect replayed messages auto lastIt = m_rsiInboundMessageNumber.find(rsi); if (lastIt != m_rsiInboundMessageNumber.end()) { const uint16_t last = lastIt->second; @@ -825,6 +1016,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ m_rsiInboundFingerprint.find(rsi) != m_rsiInboundFingerprint.end() && m_rsiInboundFingerprint[rsi] == fingerprint; + // log a warning if an identical retry is detected but the message number is otherwise invalid if (!identicalRetry && (distance == 0U || distance >= 1680U)) { LogWarning(LOG_P25, P25_KMM_STR ", invalid/replayed message number, RSI = %u, MN = %u", rsi, received); return makeNack(KMM_Status::INVALID_MSG_NUMBER); @@ -839,13 +1031,6 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ llId = frame->getSrcLLId(); } - // maintain outbound state independently from the validated inbound MN - // AACA-A 13.6.4 initializes the SU's last valid MN (MNL) to zero and - // applies a strict advance for a new RK3 message, so the first KMF MN must - // be one -- sending zero is rejected as a replay before the KMM is processed - if (m_rsiMessageNumber.find(llId) == m_rsiMessageNumber.end()) - m_rsiMessageNumber[llId] = 1U; - // handle the KMM message based on its type switch (frame->getMessageId()) { case KMM_MessageType::CHANGEOVER_RSP: @@ -861,8 +1046,8 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ KMMHello* kmm = static_cast(frame.get()); uint8_t respKind = kmm->getResponseKind(); if (m_verbose) { - LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, flag = $%02X", kmm->toString().c_str(), - llId, kmm->getFlag()); + LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, flag = $%02X, auth = %u", kmm->toString().c_str(), + llId, kmm->getFlag(), auth.authenticated); switch (kmm->getFlag()) { case KMM_HelloFlag::REKEY_REQUEST_UKEK: LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey requested with UKEK, llId = %u", kmm->toString().c_str(), llId); @@ -910,12 +1095,22 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ } // send rekey-command - EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(llId); + EKCKeyItem keyItem = resolveUKEK(kmm->getSrcLLId()); if (keyItem.isInvalid()) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, no UKEK found for rekey request, llId = %u", kmm->toString().c_str(), llId); return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } else { - return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize, auth, additionalResponses); + if (m_network->m_kmfWarmStart) { + // an authenticated Hello proves a common TEK already exists -- otherwise establish + // a temporary security context before sending the Rekey Command + if (auth.authenticated) + return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize, auth, additionalResponses); + + return write_KMM_WarmStart_Command(llId, kmm->getSrcLLId(), payloadSize, dataLinkIndependent); + } else { + LogInfoEx(LOG_P25, P25_KMM_STR ", %s, warm start disabled performing direct rekey, llId = %u", kmm->toString().c_str(), llId); + return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize, auth, additionalResponses); + } } } else { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey request denied, llId = %u", kmm->toString().c_str(), llId); @@ -948,15 +1143,101 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ logResponseStatus(llId, kmm->toString(), entry.status()); } } + + // handle Warm-Start acknowledgment if the received Rekey Ack corresponds to a Warm-Start command + if (kmm->getAckMessageId() == KMM_MessageType::WARM_START_CMD) { + bool validWarmAck = false; + uint16_t temporaryKId = 0U; + + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + auto it = m_warmStartTransactions.find(kmm->getSrcLLId()); + validWarmAck = it != m_warmStartTransactions.end() && it->second.state == WarmStartState::WAIT_WARM_ACK && + kmm->getHasMessageNumber() && kmm->getMessageNumber() == it->second.warmStartMN; + + // if the Warm-Start acknowledgment is valid, update the transaction state and deadline + if (validWarmAck) { + temporaryKId = it->second.temporaryKId; + it->second.state = WarmStartState::WAIT_REKEY_ACK; + it->second.deadline = monotonicMilliseconds() + WARM_START_TIMEOUT_MS; + } + } + + if (!validWarmAck) { + LogWarning(LOG_P25, P25_KMM_STR ", unsolicited or mismatched Warm-Start acknowledgment, RSI = %u", kmm->getSrcLLId()); + return nullptr; + } + + bool temporaryKeyAccepted = false; + for (const auto& status : kmm->getKeyStatus()) { + if (status.algId() == ALGO_AES_256 && status.kId() == temporaryKId && + status.status() == KMM_Status::CMD_PERFORMED) + temporaryKeyAccepted = true; + } + + if (!temporaryKeyAccepted) { + eraseWarmStart(kmm->getSrcLLId()); + return nullptr; + } + + UInt8Array rekey = write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), KMM_HelloFlag::REKEY_REQUEST_UKEK, + payloadSize, auth, additionalResponses, true); + if (rekey == nullptr) + eraseWarmStart(kmm->getSrcLLId()); + + return rekey; + } + + // rekey acknowledgments must correlate to every emitted Rekey batch and be + // authenticated by one of the permanent TEKs delivered by the transaction + { + std::lock_guard lock(m_warmStartMutex); + + // find the warm start transaction corresponding to the source LLID + auto it = m_warmStartTransactions.find(kmm->getSrcLLId()); + if (it != m_warmStartTransactions.end() && it->second.state == WarmStartState::WAIT_REKEY_ACK) { + auto mn = std::find(it->second.pendingRekeyMNs.begin(), it->second.pendingRekeyMNs.end(), kmm->getMessageNumber()); + bool statusesAccepted = kmm->getAckMessageId() == KMM_MessageType::REKEY_CMD && kmm->getHasMessageNumber() && mn != it->second.pendingRekeyMNs.end() && + kmm->getNumberOfKeyStatus() > 0U; + + // verify that each key status in the acknowledgment is accepted and corresponds to a permitted final key ID + for (const auto& status : kmm->getKeyStatus()) { + statusesAccepted = statusesAccepted && status.status() == KMM_Status::CMD_PERFORMED && + std::find(it->second.permittedFinalKIds.begin(), it->second.permittedFinalKIds.end(), + status.kId()) != it->second.permittedFinalKIds.end(); + } + + const bool authorized = statusesAccepted && auth.keyId != it->second.temporaryKId && + std::find(it->second.permittedFinalKIds.begin(), it->second.permittedFinalKIds.end(), auth.keyId) != it->second.permittedFinalKIds.end(); + + // check if the rekey acknowledgment is authorized + if (!authorized) { + LogWarning(LOG_P25, P25_KMM_STR ", rekey acknowledgment did not use a delivered permanent TEK, RSI = %u", kmm->getSrcLLId()); + eraseBytes(it->second.temporaryTEK.data(), it->second.temporaryTEK.size()); + m_warmStartTransactions.erase(it); + } else { + it->second.pendingRekeyMNs.erase(mn); + it->second.deadline = monotonicMilliseconds() + WARM_START_TIMEOUT_MS; + + // if there are no more pending rekey message numbers, clean up the warm start transaction + if (it->second.pendingRekeyMNs.empty()) { + eraseBytes(it->second.temporaryTEK.data(), it->second.temporaryTEK.size()); + m_warmStartTransactions.erase(it); + } + } + } + } } break; case KMM_MessageType::DEREG_CMD: { KMMDeregistrationCommand* kmm = static_cast(frame.get()); + eraseWarmStart(kmm->getSrcLLId()); uint8_t respKind = kmm->getResponseKind(); - LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u", kmm->toString().c_str(), - llId); + LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u", kmm->toString().c_str(), llId); // ignore Response Kind 2 command requests initiated from a SU if (respKind == KMM_ResponseKind::DELAYED) { @@ -1025,39 +1306,300 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ return nullptr; } +/* Encrypt/decrypt KMM frame. */ + +UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, const uint8_t* buffer, uint32_t len, bool encrypt) +{ + assert(buffer != nullptr); + + // AACA-D 12.4: a DLD KMM is at most 512 octets including the three + // octets excluded from Message Length + if (len < 10U || len > 512U) { + LogError(LOG_P25, P25_KMM_STR ", invalid DLD KMM length, len = %u", len); + return nullptr; + } + + P25Crypto crypto; + if (!encrypt) + crypto.setMI(mi); + else { + crypto.generateMI(); + crypto.getMI(mi); + } + + UInt8Array outBuffer = std::make_unique(len); + ::memset(outBuffer.get(), 0x00U, len); + ::memcpy(outBuffer.get(), buffer, len); + + if (algoId == P25DEF::ALGO_UNENCRYPT) + return outBuffer; + + ::EKCKeyItem keyItem = resolveTEK(kid, algoId); + // find() searches the existing EKC collection only. UKEKs are kept + // in the separate collection and are available only via findUKEK(). + if (!keyItem.isInvalid()) { + uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + ::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + uint8_t keyLength = keyItem.getKey(key); + + if (m_network->m_debug) + LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength); + + LogInfoEx(LOG_P25, P25_KMM_STR ", algId = $%02X, kID = $%04X", algoId, kid); + crypto.setTEKAlgoId(algoId); + crypto.setKey(key, keyLength); + crypto.generateKeystream(); + + switch (algoId) { + case P25DEF::ALGO_AES_256: + crypto.cryptAES_PDU(outBuffer.get(), len); + return outBuffer; + default: + LogError(LOG_P25, "unsupported KEK algorithm, algoId = $%02X", algoId); + break; + } + } + else { + LogError(LOG_P25, P25_KMM_STR ", unable to %s outer KMM, KEK not found, algId = $%02X, kId = $%04X", + encrypt ? "encrypt" : "decrypt", algoId, kid); + } + + return nullptr; +} + +/* Resolves a TEK locally, then from an upstream replica master. */ + +EKCKeyItem P25OTARService::resolveTEK(uint16_t kid, uint8_t algorithmId, uint32_t requestingRSI) const +{ + // attempt to resolve the TEK locally first, then from a warm start, and finally from an upstream replica master + EKCKeyItem local = m_network->m_cryptoLookup->find(kid); + if (!local.isInvalid() && (algorithmId == ALGO_UNENCRYPT || local.algId() == algorithmId)) + return local; + + // attempt to resolve the TEK from a warm start if it was not found locally + EKCKeyItem temporary = resolveWarmStartTEK(kid, requestingRSI); + if (!temporary.isInvalid() && (algorithmId == ALGO_UNENCRYPT || temporary.algId() == algorithmId)) + return temporary; + + const uint32_t lookupId = ((uint32_t)algorithmId << 16U) | kid; + const uint64_t requestedAt = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + + // scope is intentional + { + std::lock_guard lock(m_upstreamKeyMutex); + m_upstreamTEKs.erase(lookupId); + } + + if (!m_network->requestUpstreamTEK(kid, algorithmId, requestingRSI)) + return EKCKeyItem(); + + // wait for the upstream TEK to be received or until the timeout expires + std::unique_lock lock(m_upstreamKeyMutex); + const bool received = m_upstreamKeyReady.wait_for(lock, + std::chrono::milliseconds(UPSTREAM_KEY_TIMEOUT_MS), [&]() { + auto it = m_upstreamTEKs.find(lookupId); + return it != m_upstreamTEKs.end() && it->second.receivedAt >= requestedAt; + }); + + if (!received) + return EKCKeyItem(); + + const UpstreamKey result = m_upstreamTEKs[lookupId]; + + return makeEKCKey(result.key, result.algorithmId, result.keyLength); +} + +/* Resolves a UKEK locally, then from an upstream replica master. */ + +EKCKeyItem P25OTARService::resolveUKEK(uint32_t rsi) const +{ + // attempt to resolve the UKEK locally first, then from an upstream replica master + EKCKeyItem local = m_network->m_cryptoLookup->findUKEK(rsi); + if (!local.isInvalid()) + return local; + + // if the UKEK was not found locally, request it from an upstream replica master + const uint64_t requestedAt = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + + // scope is intentional + { + std::lock_guard lock(m_upstreamKeyMutex); + m_upstreamUKEKs.erase(rsi); + } + + if (!m_network->requestUpstreamUKEK(rsi)) + return EKCKeyItem(); + + // wait for the upstream UKEK to be received or until the timeout expires + std::unique_lock lock(m_upstreamKeyMutex); + const bool received = m_upstreamKeyReady.wait_for(lock, + std::chrono::milliseconds(UPSTREAM_KEY_TIMEOUT_MS), [&]() { + auto it = m_upstreamUKEKs.find(rsi); + return it != m_upstreamUKEKs.end() && it->second.receivedAt >= requestedAt; + }); + + if (!received) + return EKCKeyItem(); + + const UpstreamKey result = m_upstreamUKEKs[rsi]; + + return makeEKCKey(result.key, result.algorithmId, result.keyLength, rsi); +} + +/* Resolves a temporary TEK belonging to an active Warm-Start transaction. */ + +EKCKeyItem P25OTARService::resolveWarmStartTEK(uint16_t kid, uint32_t rsi) const +{ + std::lock_guard lock(m_warmStartMutex); + + // iterate through active warm-start transactions to find the matching temporary TEK + for (const auto& entry : m_warmStartTransactions) { + const WarmStartTransaction& transaction = entry.second; + if (transaction.temporaryKId != kid || (rsi != 0U && entry.first != rsi)) + continue; + + std::ostringstream hex; + hex << std::hex << std::uppercase << std::setfill('0'); + for (uint8_t byte : transaction.temporaryTEK) + hex << std::setw(2) << (uint32_t)byte; + + EKCKeyItem key; + key.rsiId(entry.first); + key.algId(ALGO_AES_256); + key.kId(kid); + key.sln(0U); + key.keyMaterial(hex.str()); + return key; + } + + return EKCKeyItem(); +} + +/* Erases a warm start transaction for the given requesting RSI. */ + +void P25OTARService::eraseWarmStart(uint32_t rsi) +{ + std::lock_guard lock(m_warmStartMutex); + + // find the warm start transaction for the given RSI and erase it if it exists + auto it = m_warmStartTransactions.find(rsi); + if (it != m_warmStartTransactions.end()) { + eraseBytes(it->second.temporaryTEK.data(), it->second.temporaryTEK.size()); + m_warmStartTransactions.erase(it); + } +} + +/* Resolves the required outer-encryption context for a generated response. */ + +bool P25OTARService::resolveResponseSecurity(const uint8_t* data, uint32_t len, bool& encrypted, + uint8_t& algoId, uint16_t& kid) const +{ + if (data == nullptr) { + LogError(LOG_P25, P25_KMM_STR ", cannot resolve outer encryption for a null generated response"); + return false; + } + + const uint8_t messageId = len > 0U ? data[0U] : KMM_MessageType::NULL_CMD; + const uint32_t declaredLength = len >= 3U ? + ((((uint32_t)data[1U] << 8U) | data[2U]) + 3U) : 0U; + if (len < 10U || len > 512U || declaredLength != len) { + LogError(LOG_P25, P25_KMM_STR ", cannot outer-encrypt generated KMM: invalid length, messageId = $%02X, declared/actual = %u/%u", + messageId, declaredLength, len); + return false; + } + + // check if the message is a warm start command, which does not require outer encryption + if (messageId == KMM_MessageType::WARM_START_CMD) { + encrypted = false; + algoId = ALGO_UNENCRYPT; + kid = 0U; + return true; + } + + if (encrypted || !requiresEncryptedKMM(messageId)) + return true; + + // this is locally generated and its total length has already been checked; + // decode only the common header: the bounded air-facing factory intentionally + // rejects nested-count messages such as RK3 until it has a structural validator + KMMOpaqueFrame response; + if (!response.decode(data) || response.getMACType() != KMM_MAC::ENH_MAC || + response.getMACAlgId() != ALGO_AES_256 || response.getMACKId() == 0U) { + LogError(LOG_P25, P25_KMM_STR ", generated KMM requires outer encryption but has no usable enhanced-MAC TEK context, messageId = $%02X, macType = $%02X, macAlgId = $%02X, macKId = $%04X", + messageId, response.getMACType(), response.getMACAlgId(), response.getMACKId()); + return false; + } + + // resolve the outer TEK for the generated KMM response + EKCKeyItem outerTek = resolveTEK(response.getMACKId(), response.getMACAlgId(), response.getDstLLId()); + if (outerTek.isInvalid()) { + LogError(LOG_P25, P25_KMM_STR ", generated KMM requires outer encryption but MAC TEK was not found, messageId = $%02X, algId = $%02X, kId = $%04X", + messageId, response.getMACAlgId(), response.getMACKId()); + return false; + } + + if (outerTek.algId() != response.getMACAlgId()) { + LogError(LOG_P25, P25_KMM_STR ", generated KMM outer TEK algorithm mismatch, messageId = $%02X, required/found algId = $%02X/$%02X, kId = $%04X", + messageId, response.getMACAlgId(), outerTek.algId(), response.getMACKId()); + return false; + } + + algoId = response.getMACAlgId(); + kid = response.getMACKId(); + encrypted = true; + + if (m_debug) { + LogDebugEx(LOG_P25, "P25OTARService::resolveResponseSecurity()", + "selected outer encryption for generated KMM, messageId = $%02X, algId = $%02X, kId = $%04X", + messageId, algoId, kid); + } + + return true; +} + /* Helper used to return a Rekey-Command KMM to the calling SU. */ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, - const KMMAuthContext& auth, std::vector>* additionalResponses) + const KMMAuthContext& auth, std::vector>* additionalResponses, bool useWarmStartTEK) { uint8_t mi[MI_LENGTH_BYTES]; ::memset(mi, 0x00U, MI_LENGTH_BYTES); - // Zero is the SU's initial MNL, not a valid first non-retry RK3 MNR. - uint16_t mn = 1U; - if (m_rsiMessageNumber.find(llId) != m_rsiMessageNumber.end()) { - mn = m_rsiMessageNumber[llId]; - } - P25Crypto crypto; crypto.generateMI(); crypto.getMI(mi); KMMRekeyCommand outKmm = KMMRekeyCommand(); - /* - ** bryanb: Architecturally this is a problem. Because KMF services would essentially be limited to the local FNE - ** because we aren't performing FNE KEY_REQ's to upstream peer'ed FNEs to find the key used to encrypt the KMM. - */ - uint8_t kekKey[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; ::memset(kekKey, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); uint8_t kekAlgId = P25DEF::ALGO_UNENCRYPT; uint16_t kekKId = 0U; - // Attempt to find the UKEK (User Key Encryption Key) for the given RSI (Rekey Sequence Identifier) - ::EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(kmmRSI); + // warm start makes its temporary TEK the KEK and MAC/outer-encryption key for + // the immediately following Rekey -- ordinary rekey continues to use the UKEK + ::EKCKeyItem keyItem; + if (useWarmStartTEK) { + uint16_t temporaryKId = 0U; + + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + // look up the warm start transaction for the given RSI + auto it = m_warmStartTransactions.find(kmmRSI); + if (it != m_warmStartTransactions.end()) + temporaryKId = it->second.temporaryKId; + } + + if (temporaryKId != 0U) + keyItem = resolveWarmStartTEK(temporaryKId, kmmRSI); + } else { + keyItem = resolveUKEK(kmmRSI); + } + + // if a valid key item was resolved, extract its key, algorithm ID, and key ID if (!keyItem.isInvalid()) { uint8_t keyLength = keyItem.getKey(kekKey); @@ -1085,7 +1627,6 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS outKmm.setMACType(KMM_MAC::ENH_MAC); outKmm.setMACFormat(auth.authenticated ? auth.format : KMM_MAC_FORMAT_CBC); outKmm.setHasMessageNumber(true); - outKmm.setMessageNumber(mn); outKmm.setAlgId(kekAlgId); outKmm.setKId(kekKId); @@ -1102,8 +1643,12 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS // AACA-D 13.5: the MAC key is a TEK, never the UKEK used for inner // key wrap; select an AES TEK already authorized for this subscriber EKCKeyItem macTekItem; - if (auth.authenticated) { - macTekItem = m_network->m_cryptoLookup->find(auth.keyId); + if (useWarmStartTEK) { + macTekItem = keyItem; + } else if (auth.authenticated) { + macTekItem = resolveTEK(auth.keyId, auth.algorithmId, kmmRSI); + + // ensure that the resolved MAC TEK is valid and authorized for this subscriber if (macTekItem.isInvalid() || macTekItem.algId() != auth.algorithmId || (!allowedKIds.empty() && std::find(allowedKIds.begin(), allowedKIds.end(), auth.keyId) == allowedKIds.end())) { LogError(LOG_P25, P25_KMM_STR ", %s, authenticated request MAC TEK is not authorized for response, RSI = %u", @@ -1111,6 +1656,7 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS return nullptr; } } else { + // attempt to find a suitable MAC TEK from the local key storage for (const EKCKeyItem& candidate : m_network->m_cryptoLookup->keys()) { if (candidate.algId() == ALGO_AES_256 && (allowedKIds.empty() || std::find(allowedKIds.begin(), allowedKIds.end(), @@ -1119,6 +1665,15 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS break; } } + + // if no suitable MAC TEK was found locally, attempt to resolve from upstream + if (macTekItem.isInvalid()) { + for (uint16_t allowedKId : allowedKIds) { + macTekItem = resolveTEK(allowedKId, ALGO_AES_256, kmmRSI); + if (!macTekItem.isInvalid()) + break; + } + } } // ensure that a valid MAC TEK (Traffic Encryption Key) has been selected for the KMM response @@ -1142,8 +1697,23 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS else ks.keyLength(P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + // build the authorized TEK set from local storage and fill policy-listed + // misses from an upstream replica master + std::vector rekeyItems = m_network->m_cryptoLookup->keys(); + for (uint16_t allowedKId : allowedKIds) { + const bool present = std::any_of(rekeyItems.begin(), rekeyItems.end(), + [allowedKId](const EKCKeyItem& item) { return item.kId() == allowedKId; }); + + // if the allowed key ID is not present locally, attempt to resolve it from upstream + if (!present) { + EKCKeyItem upstream = resolveTEK(allowedKId, ALGO_AES_256, kmmRSI); + if (!upstream.isInvalid()) + rekeyItems.push_back(upstream); + } + } + // iterate through all available AES-256 keys and prepare them for inclusion in the KMM response - for (EKCKeyItem keyItem : m_network->m_cryptoLookup->keys()) { + for (EKCKeyItem keyItem : rekeyItems) { if (keyItem.algId() != ALGO_AES_256) { LogWarning(LOG_P25, P25_KMM_STR", %s, ignoring kId = %u, is not an AES-256 key, llId = %u, RSI = %u", outKmm.toString().c_str(), keyItem.kId(), outKmm.getSrcLLId(), outKmm.getDstLLId()); @@ -1196,6 +1766,7 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS static constexpr uint32_t MAX_NATIVE_ENCRYPTED_KMM_LENGTH = (19U * P25_PDU_CONFIRMED_DATA_LENGTH_BYTES) - 13U - 4U; const std::vector keys = ks.keys(); const size_t batchCount = (keys.size() + MAX_KEYS_PER_REKEY - 1U) / MAX_KEYS_PER_REKEY; + const uint16_t mn = reserveOutboundMessageNumbers(llId, (uint16_t)batchCount); UInt8Array firstFrame; // prepare to batch keys into multiple RK3 Rekey Commands if necessary @@ -1246,8 +1817,8 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS batch.generateMAC(macTek, encoded.data()); if (m_verbose) { - LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, RSI = %u, batch = %u/%u, keyCount = %u, moreToFollow = %u", - batch.toString().c_str(), batch.getSrcLLId(), batch.getDstLLId(), uint32_t(batchIndex + 1U), + LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, kId = $%04X, RSI = %u, batch = %u/%u, keyCount = %u, moreToFollow = %u", + batch.toString().c_str(), batch.getSrcLLId(), batch.getKId(), batch.getDstLLId(), uint32_t(batchIndex + 1U), uint32_t(batchCount), uint32_t(end - begin), batch.getComplete() ? 0U : 1U); } @@ -1262,10 +1833,169 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS } } - m_rsiMessageNumber[llId] = (uint16_t)(mn + batchCount); + // if using the Warm Start TEK, update the corresponding warm start transaction with the pending rekey message numbers and deadline + if (useWarmStartTEK) { + std::lock_guard lock(m_warmStartMutex); + + // look up the warm start transaction for the given RSI + auto it = m_warmStartTransactions.find(kmmRSI); + if (it != m_warmStartTransactions.end()) { + it->second.pendingRekeyMNs.clear(); + + for (size_t batchIndex = 0U; batchIndex < batchCount; ++batchIndex) + it->second.pendingRekeyMNs.push_back((uint16_t)(mn + batchIndex)); + + it->second.deadline = monotonicMilliseconds() + WARM_START_TIMEOUT_MS; + } + } + return firstFrame; } +/* Builds the clear, temporary-TEK-authenticated Warm-Start Command. */ + +UInt8Array P25OTARService::write_KMM_WarmStart_Command(uint32_t llId, uint32_t kmmRSI, + uint32_t* payloadSize, bool dataLinkIndependent) +{ + if (payloadSize != nullptr) + *payloadSize = 0U; + + // ensure that the UKEK is resolved and valid before proceeding + EKCKeyItem ukek = resolveUKEK(kmmRSI); + if (ukek.isInvalid() || ukek.algId() != ALGO_AES_256) { + LogWarning(LOG_P25, P25_KMM_STR ", cannot start Warm Start; AES-256 UKEK unavailable, RSI = %u", kmmRSI); + return nullptr; + } + + uint8_t ukekBytes[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U }; + if (ukek.getKey(ukekBytes) != P25DEF::MAX_ENC_KEY_LENGTH_BYTES) + return nullptr; + + // initialize a new warm start transaction for this RSI + WarmStartTransaction transaction; + transaction.llId = llId; + transaction.rsi = kmmRSI; + transaction.warmStartMN = reserveOutboundMessageNumbers(llId); + transaction.deadline = monotonicMilliseconds() + WARM_START_TIMEOUT_MS; + transaction.dataLinkIndependent = dataLinkIndependent; + + lookups::RadioId ridEntry = m_network->m_ridLookup->find(kmmRSI); + if (ridEntry.radioDefault() || !ridEntry.radioEnabled() || !ridEntry.canRekey()) + return nullptr; + + transaction.permittedFinalKIds = ridEntry.allowedKIds(); + if (transaction.permittedFinalKIds.empty()) { + for (const EKCKeyItem& item : m_network->m_cryptoLookup->keys()) { + if (item.algId() == ALGO_AES_256) + transaction.permittedFinalKIds.push_back((uint16_t)item.kId()); + } + } + + if (transaction.permittedFinalKIds.empty()) + return nullptr; + +#if defined(ENABLE_SSL) + // generate a cryptographically secure random temporary TEK for the warm start transaction + if (RAND_bytes(transaction.temporaryTEK.data(), transaction.temporaryTEK.size()) != 1) { + LogError(LOG_P25, P25_KMM_STR ", cryptographic random generation failed for Warm Start, RSI = %u", kmmRSI); + return nullptr; + } +#else + LogError(LOG_P25, P25_KMM_STR ", Warm Start requires OpenSSL cryptographic random generation"); + return nullptr; +#endif + + // allocate a nonzero ALGID/KID pair not used by the local store or another + // outstanding Warm-Start transaction + std::random_device random; + for (uint32_t attempts = 0U; attempts < 65535U && transaction.temporaryKId == 0U; ++attempts) { + const uint16_t candidate = (uint16_t)random(); + if (candidate == 0U || !m_network->m_cryptoLookup->find(candidate).isInvalid()) + continue; + bool pending = false; + + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + // check if the candidate KID is already pending in another warm start transaction + for (const auto& item : m_warmStartTransactions) { + if (item.second.temporaryKId == candidate) { + pending = true; + break; + } + } + } + + if (!pending) + transaction.temporaryKId = candidate; + } + + if (transaction.temporaryKId == 0U) { + eraseBytes(transaction.temporaryTEK.data(), transaction.temporaryTEK.size()); + return nullptr; + } + + P25Crypto crypto; + UInt8Array wrapped = crypto.cryptAES_TEK(ukekBytes, transaction.temporaryTEK.data(), P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + eraseBytes(ukekBytes, sizeof(ukekBytes)); + if (wrapped == nullptr) { + eraseBytes(transaction.temporaryTEK.data(), transaction.temporaryTEK.size()); + return nullptr; + } + + KeyItem warmKey; + warmKey.keyFormat(KMM_KEY_FORMAT_TEK); + warmKey.sln(0U); + warmKey.kId(transaction.temporaryKId); + warmKey.setKey(wrapped.get(), P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); + + KMMWarmStartCommand command; + command.setSrcLLId(WUID_FNE); + command.setDstLLId(kmmRSI); + command.setResponseKind(KMM_ResponseKind::IMMEDIATE); + command.setHasMessageNumber(true); + command.setMessageNumber(transaction.warmStartMN); + command.setMACType(KMM_MAC::ENH_MAC); + command.setMACAlgId(ALGO_AES_256); + // AACA-D defines the Warm-Start MAC trailer KID as undefined. Correlation is + // performed from the RSI transaction; do not expose a meaningful lookup ID. + command.setMACKId(0U); + command.setMACFormat(KMM_MAC_FORMAT_CBC); + command.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE); + command.setKEKAlgId(ukek.algId()); + command.setKEKKId((uint16_t)ukek.kId()); + command.setKeyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); + command.setTEKAlgId(ALGO_AES_256); + command.setKey(warmKey); + + const uint32_t length = command.fullLength(); + UInt8Array encoded = std::make_unique(length); + ::memset(encoded.get(), 0x00U, length); + command.encode(encoded.get()); + command.generateMAC(transaction.temporaryTEK.data(), encoded.get()); + + // scope is intentional + { + std::lock_guard lock(m_warmStartMutex); + + // erase any previous warm start transaction for this RSI before storing the new one + auto old = m_warmStartTransactions.find(kmmRSI); + if (old != m_warmStartTransactions.end()) + eraseBytes(old->second.temporaryTEK.data(), old->second.temporaryTEK.size()); + + m_warmStartTransactions[kmmRSI] = transaction; + } + + eraseBytes(transaction.temporaryTEK.data(), transaction.temporaryTEK.size()); + + if (payloadSize != nullptr) + *payloadSize = length; + + LogInfoEx(LOG_P25, P25_KMM_STR ", Warm-Start Command issued, llId = %u, RSI = %u, temporaryKId = $%04X", llId, kmmRSI, warmKey.kId()); + return encoded; +} + /* Helper used to build a Changeover-Command KMM for an SU. */ UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t kmmRSI, @@ -1280,6 +2010,7 @@ UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t return nullptr; } + // determine the allowed KIDs for this RSI based on its key policy const std::vector allowedKIds = ridEntry.allowedKIds(); EKCKeyItem macTekItem; for (const EKCKeyItem& candidate : m_network->m_cryptoLookup->keys()) { @@ -1291,14 +2022,21 @@ UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t } } + // if no suitable MAC TEK was found in the local key store, attempt to resolve one from the network + if (macTekItem.isInvalid()) { + for (uint16_t allowedKId : allowedKIds) { + macTekItem = resolveTEK(allowedKId, ALGO_AES_256, kmmRSI); + if (!macTekItem.isInvalid()) + break; + } + } + if (macTekItem.isInvalid()) { LogError(LOG_P25, P25_KMM_STR ", cannot build Changeover Command; no authorized AES TEK for RSI = %u", kmmRSI); return nullptr; } - uint16_t mn = 1U; - if (m_rsiMessageNumber.find(llId) != m_rsiMessageNumber.end()) - mn = m_rsiMessageNumber[llId]; + const uint16_t mn = reserveOutboundMessageNumbers(llId); KMMChangeover outKmm; outKmm.setSrcLLId(WUID_FNE); @@ -1323,8 +2061,6 @@ UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t if (payloadSize != nullptr) *payloadSize = frameLength; - m_rsiMessageNumber[llId] = (uint16_t)(mn + 1U); - if (m_verbose) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, RSI = %u, supersededKeysetId = $%02X, activeKeysetId = $%02X", outKmm.toString().c_str(), llId, kmmRSI, supersededKeysetId, activeKeysetId); @@ -1420,7 +2156,7 @@ UInt8Array P25OTARService::encode_KMM_Response(KMMFrame& frame, uint32_t* payloa ::memset(macTek, 0x00U, sizeof(macTek)); if (auth.authenticated) { - EKCKeyItem key = m_network->m_cryptoLookup->find(auth.keyId); + EKCKeyItem key = resolveTEK(auth.keyId, auth.algorithmId, frame.getDstLLId()); if (key.isInvalid() || key.algId() != auth.algorithmId) return nullptr; diff --git a/src/fne/network/P25OTARService.h b/src/fne/network/P25OTARService.h index f2c94975..3250e6f0 100644 --- a/src/fne/network/P25OTARService.h +++ b/src/fne/network/P25OTARService.h @@ -25,11 +25,17 @@ class FNETestHooks; #include "common/p25/P25Defines.h" #include "common/p25/Crypto.h" #include "common/p25/kmm/KMMFrame.h" +#include "common/p25/kmm/KeysetItem.h" #include "common/network/udp/Socket.h" #include "common/network/RawFrameQueue.h" #include "network/TrafficNetwork.h" #include "network/callhandler/packetdata/P25PacketData.h" +#include +#include +#include +#include + namespace network { // --------------------------------------------------------------------------- @@ -105,6 +111,23 @@ namespace network */ void close(); + /** + * @brief Supplies a TEK returned asynchronously by an upstream master. + * @param key The TEK key item. + * @param algId The algorithm identifier for the TEK. + * @param keyLength The length of the TEK key in bytes. + * + */ + void cacheUpstreamTEK(const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength); + /** + * @brief Supplies a UKEK returned asynchronously by an upstream master. + * @param rsi The Radio Subscriber Identifier associated with the UKEK. + * @param key The UKEK key item. + * @param algId The algorithm identifier for the UKEK. + * @param keyLength The length of the UKEK key in bytes. + */ + void cacheUpstreamUKEK(uint32_t rsi, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength); + private: /** * @brief Context for KMM message authentication. @@ -118,6 +141,50 @@ namespace network uint16_t format = P25DEF::KMM_MAC_FORMAT_CBC; //!< MAC format used for the KMM message. }; + /** + * @brief Represents an upstream key received from the master. + */ + struct UpstreamKey { + p25::kmm::KeyItem key; //!< The key item for the upstream key. + uint8_t algorithmId = P25DEF::ALGO_UNENCRYPT; //!< Algorithm ID for the upstream key. + uint8_t keyLength = 0U; //!< Length of the upstream key in bytes. + uint64_t receivedAt = 0U; //!< Timestamp when the upstream key was received. + }; + + /** + * @brief Represents the state of a warm start transaction. + */ + enum class WarmStartState : uint8_t + { + /** + * @brief Waiting for the warm start acknowledgment from the master. + */ + WAIT_WARM_ACK, + /** + * @brief Waiting for the rekey acknowledgment from the master. + */ + WAIT_REKEY_ACK + }; + + /** + * @brief Represents a warm start transaction. + */ + struct WarmStartTransaction { + WarmStartState state = WarmStartState::WAIT_WARM_ACK; //!< Current state of the warm start transaction. + uint32_t llId = 0U; //!< Logical link ID associated with the warm start transaction. + uint32_t rsi = 0U; //!< Subscriber RSI associated with the warm start transaction. + uint16_t temporaryKId = 0U; //!< Temporary key ID used in the warm start transaction. + uint16_t warmStartMN = 0U; //!< Message number for the warm start transaction. + uint64_t deadline = 0U; //!< Deadline for the warm start transaction. + bool dataLinkIndependent = false; //!< Indicates if the warm start transaction is data link independent. + bool hasDLIEndpoint = false; //!< Indicates if the warm start transaction has a data link independent endpoint. + sockaddr_storage dliAddress{}; //!< Address of the data link independent endpoint. + uint32_t dliAddressLength = 0U; //!< Length of the data link independent endpoint address. + std::array temporaryTEK{}; //!< Temporary TEK used in the warm start transaction. + std::vector permittedFinalKIds; //!< Permitted final key IDs for the warm start transaction. + std::vector pendingRekeyMNs; //!< Pending rekey message numbers for the warm start transaction. + }; + network::udp::Socket* m_socket; network::RawFrameQueue* m_frameQueue; @@ -131,17 +198,53 @@ namespace network concurrent::unordered_map m_rsiInboundFingerprint; concurrent::unordered_map m_dliRegistered; + mutable std::mutex m_outboundMessageNumberMutex; + bool m_allowNoUKEKRekey; bool m_debug; bool m_verbose; + mutable std::mutex m_upstreamKeyMutex; + mutable std::condition_variable m_upstreamKeyReady; + mutable std::unordered_map m_upstreamTEKs; + mutable std::unordered_map m_upstreamUKEKs; + mutable std::mutex m_warmStartMutex; + mutable std::unordered_map m_warmStartTransactions; + + /** + * @brief Atomically reserves one or more outbound KMM message numbers. + * @param[in] rsi Radio Subscriber Identifier (RSI) for which to reserve message numbers. + * @param[in] count Number of outbound message numbers to reserve. + * @returns The first reserved outbound message number. + * + */ + uint16_t reserveOutboundMessageNumbers(uint32_t rsi, uint16_t count = 1U); + /** * @brief Entry point to process a given network packet. * @param arg Instance of the OTARPacketRequest structure. */ static void taskNetworkRx(OTARPacketRequest* req); + /** + * @brief Helper used to process KMM frames. + * @param[in] data Network data buffer. + * @param len Length of data. + * @param encrypted Flag indicating whether or not the KMM frame is encrypted. + * @param llId Logical Link ID. + * @param[out] payloadSize Size of the returned KMM payload. + * @param algoId Algorithm ID. + * @param kid Key ID. + * @param mi Message Indicator. + * @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent. + * @param additionalResponses Optional container for additional KMM responses. + * @returns UInt8Array Buffer containing the processed KMM frame (if any). + */ + UInt8Array processKMM(const uint8_t* data, uint32_t len, uint32_t llId, bool encrypted, uint32_t* payloadSize, + uint8_t algoId = P25DEF::ALGO_UNENCRYPT, uint16_t kid = 0U, const uint8_t* mi = nullptr, + bool dataLinkIndependent = false, std::vector>* additionalResponses = nullptr); + /** * @brief Encrypt/decrypt KMM frame. * @param[in] algoId Algorithm ID. @@ -154,6 +257,35 @@ namespace network */ UInt8Array cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, const uint8_t* buffer, uint32_t len, bool encrypt = false); + /** + * @brief Resolves a TEK locally, then from an upstream replica master. + * @param[in] kid Key ID. + * @param[in] algorithmId Algorithm ID. + * @param[in] requestingRSI Requesting RSI (default is 0U). + * @return Resolved TEK as an EKCKeyItem. + * + */ + EKCKeyItem resolveTEK(uint16_t kid, uint8_t algorithmId, uint32_t requestingRSI = 0U) const; + /** + * @brief Resolves a UKEK locally, then from an upstream replica master. + * @param[in] rsi Requesting RSI (default is 0U). + * @return Resolved UKEK as an EKCKeyItem. + */ + EKCKeyItem resolveUKEK(uint32_t rsi) const; + + /** + * @brief Resolves a warm start TEK locally, then from an upstream replica master. + * @param[in] kid Key ID. + * @param[in] rsi Requesting RSI (default is 0U). + * @return Resolved warm start TEK as an EKCKeyItem. + */ + EKCKeyItem resolveWarmStartTEK(uint16_t kid, uint32_t rsi = 0U) const; + /** + * @brief Erases a warm start transaction for the given requesting RSI. + * @param[in] rsi Requesting RSI. + */ + void eraseWarmStart(uint32_t rsi); + /** * @brief Resolves the required outer-encryption context for a locally generated KMM response. * @param[in] data Encoded plaintext KMM response. @@ -167,35 +299,29 @@ namespace network uint8_t& algoId, uint16_t& kid) const; /** - * @brief Helper used to process KMM frames. - * @param[in] data Network data buffer. - * @param len Length of data. - * @param encrypted Flag indicating whether or not the KMM frame is encrypted. - * @param llId Logical Link ID. + * @brief Helper used to return a Rekey-Command KMM to the calling SU. + * @param llId Logical Link Address. + * @param kmmRSI KMM Radio Set Identifier. + * @param flags Hello KMM flags. * @param[out] payloadSize Size of the returned KMM payload. - * @param algoId Algorithm ID. - * @param kid Key ID. - * @param mi Message Indicator. - * @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent. + * @param auth Authentication context containing MAC/MN fields to mirror. * @param additionalResponses Optional container for additional KMM responses. * @returns UInt8Array Buffer containing the processed KMM frame (if any). */ - UInt8Array processKMM(const uint8_t* data, uint32_t len, uint32_t llId, bool encrypted, uint32_t* payloadSize, - uint8_t algoId = P25DEF::ALGO_UNENCRYPT, uint16_t kid = 0U, const uint8_t* mi = nullptr, - bool dataLinkIndependent = false, std::vector>* additionalResponses = nullptr); + UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, + const KMMAuthContext& auth, std::vector>* additionalResponses = nullptr, + bool useWarmStartTEK = false); /** - * @brief Helper used to return a Rekey-Command KMM to the calling SU. + * @brief Builds and records an RK3 Warm-Start transaction. * @param llId Logical Link Address. * @param kmmRSI KMM Radio Set Identifier. - * @param flags Hello KMM flags. * @param[out] payloadSize Size of the returned KMM payload. - * @param auth Authentication context containing MAC/MN fields to mirror. - * @param additionalResponses Optional container for additional KMM responses. + * @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent. * @returns UInt8Array Buffer containing the processed KMM frame (if any). */ - UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, - const KMMAuthContext& auth, std::vector>* additionalResponses = nullptr); + UInt8Array write_KMM_WarmStart_Command(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize, + bool dataLinkIndependent = false); /** * @brief Builds an authenticated Changeover-Command KMM for an SU. diff --git a/src/fne/network/TrafficNetwork.cpp b/src/fne/network/TrafficNetwork.cpp index 528c9ab5..6d206504 100644 --- a/src/fne/network/TrafficNetwork.cpp +++ b/src/fne/network/TrafficNetwork.cpp @@ -50,7 +50,7 @@ const uint32_t FIXED_HA_UPDATE_INTERVAL = 30U; // 30s // --------------------------------------------------------------------------- std::timed_mutex TrafficNetwork::s_keyQueueMutex; -std::timed_mutex TrafficNetwork::s_llaKeyQueueMutex; +std::timed_mutex TrafficNetwork::s_kekKeyQueueMutex; std::array TrafficNetwork::s_peerStateLocks; @@ -92,6 +92,7 @@ TrafficNetwork::TrafficNetwork(HostFNE* host, const std::string& address, uint16 m_kmfAllowRID0(false), m_kmfEncKeyRequest(false), m_kmfPresharedKey(nullptr), + m_kmfWarmStart(true), m_ridLookup(nullptr), m_ridAliasLookup(nullptr), m_tidLookup(nullptr), @@ -104,7 +105,7 @@ TrafficNetwork::TrafficNetwork(HostFNE* host, const std::string& address, uint16 m_peerAffiliations(), m_ccPeerMap(), m_peerReplicaKeyQueue(), - m_peerReplicaLLAKeyQueue(), + m_peerReplicaKEKKeyQueue(), m_globalAff(nullptr), m_treeRoot(nullptr), m_treeLock(), @@ -336,6 +337,7 @@ void TrafficNetwork::setOptions(yaml::Node& conf, bool printOptions) LogWarning(LOG_MASTER, "FNE is compiled without OpenSSL support, KMF services are unavailable."); #endif // ENABLE_SSL m_kmfAllowRID0 = conf["kmfAllowRID0"].as(false); + m_kmfWarmStart = conf["kmfWarmStart"].as(true); // scope is intentional { @@ -529,6 +531,7 @@ void TrafficNetwork::setOptions(yaml::Node& conf, bool printOptions) LogInfo(" P25 OTAR KMF Listening Address: %s", m_address.c_str()); LogInfo(" P25 OTAR KMF Listening Port: %u", kmfOtarPort); LogInfo(" P25 KMF Allow RID 0 Requests: %s", m_kmfAllowRID0 ? "yes" : "no"); + LogInfo(" P25 KMF Warm Start Requests Allowed: %s", m_kmfWarmStart ? "yes" : "no"); LogInfo(" P25 KMF Peer Request Encrypted: %s", m_kmfEncKeyRequest ? "yes" : "no"); if (!m_encryptedTrafficConn && !m_kmfEncKeyRequest) { LogWarning(LOG_MASTER, "Peers can make key requests, but the encrypted traffic connection is not enabled and KMF requests are not encrypted! Key requests will be sent in the clear."); @@ -800,13 +803,15 @@ void TrafficNetwork::clock(uint32_t ms) // perform peer replica maintainence tasks if (peer.second->isEnabled() && peer.second->getRemotePeerId() > 0U && peer.second->isReplica()) { + // attach key response handlers if not already attached if (!peer.second->getAttachedKeyRSPHandler()) { peer.second->setAttachedKeyRSPHandler(true); // this is the only place this should happen peer.second->setKeyResponseCallback([=](p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) { processTEKResponse(&ki, algId, keyLength); }); - peer.second->setLLAKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t keyLength) { - processLLAResponse(srcId, &ki, keyLength); + + peer.second->setKEKKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) { + processKEKResponse(srcId, &ki, algId, keyLength); }); } @@ -1216,7 +1221,7 @@ void TrafficNetwork::taskNetworkRx(NetPacketRequest* req) { NET_FUNC::INCALL_CTRL, &TrafficNetwork::PacketHandler::inCallControl }, { NET_FUNC::KEY_REQ, &TrafficNetwork::PacketHandler::keyRequest }, - { NET_FUNC::KEY_LLA_REQ, &TrafficNetwork::PacketHandler::llaKeyRequest }, + { NET_FUNC::KEY_KEK_REQ, &TrafficNetwork::PacketHandler::kekKeyRequest }, }; // dispatch to the appropriate handler based on the function opcode @@ -2961,6 +2966,64 @@ bool TrafficNetwork::writePeerNAK(uint32_t peerId, const char* tag, NET_CONN_NAK { NET_FUNC::NAK, NET_SUBFUNC::NOP }, RTP_END_OF_CALL_SEQ, addr, addrLen); } +/* +** Key Request +*/ + +/* Sends a TEK query to each enabled upstream replica master. */ + +bool TrafficNetwork::requestUpstreamTEK(uint16_t kid, uint8_t algId, uint32_t requestingRSI) +{ + bool sent = false; + for (auto& peer : m_host->m_peerNetworks) { + PeerNetwork* upstream = peer.second; + if (upstream == nullptr || !upstream->isEnabled() || !upstream->isReplica()) + continue; + + // attach key response handlers if not already attached + if (!upstream->getAttachedKeyRSPHandler()) { + upstream->setAttachedKeyRSPHandler(true); + upstream->setKeyResponseCallback([this](p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) { + processTEKResponse(&ki, responseAlgId, keyLength); + }); + + upstream->setKEKKeyResponseCallback([this](uint32_t rsi, p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) { + processKEKResponse(rsi, &ki, responseAlgId, keyLength); + }); + } + + sent = upstream->writeKeyReq(kid, algId, requestingRSI) || sent; + } + return sent; +} + +/* Sends a UKEK query to each enabled upstream replica master. */ + +bool TrafficNetwork::requestUpstreamUKEK(uint32_t rsi) +{ + bool sent = false; + for (auto& peer : m_host->m_peerNetworks) { + PeerNetwork* upstream = peer.second; + if (upstream == nullptr || !upstream->isEnabled() || !upstream->isReplica()) + continue; + + // attach key response handlers if not already attached + if (!upstream->getAttachedKeyRSPHandler()) { + upstream->setAttachedKeyRSPHandler(true); + upstream->setKeyResponseCallback([this](p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) { + processTEKResponse(&ki, responseAlgId, keyLength); + }); + + upstream->setKEKKeyResponseCallback([this](uint32_t responseRSI, p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) { + processKEKResponse(responseRSI, &ki, responseAlgId, keyLength); + }); + } + + sent = upstream->writeUKEKReq(rsi) || sent; + } + return sent; +} + /* ** Internal KMM Callback. */ @@ -2977,6 +3040,9 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId, LogInfoEx(LOG_PEER, "upstream master enc. key, algId = $%02X, kID = $%04X", algId, rspKi->kId()); + if (m_p25OTARService != nullptr) + m_p25OTARService->cacheUpstreamTEK(*rspKi, algId, keyLength); + s_keyQueueMutex.lock(); std::vector peersToRemove; @@ -3033,9 +3099,9 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId, s_keyQueueMutex.unlock(); } -/* Helper to process a FNE KMM LLA response. */ +/* Helper to process a FNE KMM UKEK/LLA response. */ -void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi, uint8_t keyLength) +void TrafficNetwork::processKEKResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi, uint8_t algId, uint8_t keyLength) { using namespace p25::defines; using namespace p25::kmm; @@ -3045,12 +3111,16 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi LogInfoEx(LOG_PEER, "upstream master LLA enc. key, rsi = %u", srcId); - s_llaKeyQueueMutex.lock(); + if (algId == ALGO_AES_256 && m_p25OTARService != nullptr) + m_p25OTARService->cacheUpstreamUKEK(srcId, *rspKi, algId, keyLength); + + s_kekKeyQueueMutex.lock(); std::vector peersToRemove; - for (auto entry : m_peerReplicaLLAKeyQueue) { - uint32_t requestingRid = entry.second; - if (requestingRid == srcId) { + for (auto entry : m_peerReplicaKEKKeyQueue) { + uint8_t requestedAlgId = (uint8_t)(entry.second >> 24U); + uint32_t requestingRid = entry.second & 0xFFFFFFU; + if (requestingRid == srcId && requestedAlgId == algId) { uint32_t peerId = entry.first; uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; @@ -3058,8 +3128,8 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi rspKi->getKey(key); if (m_debug) { - LogDebugEx(LOG_HOST, "TrafficNetwork::processLLAResponse()", "keyLength = %u", keyLength); - Utils::dump(1U, "TrafficNetwork::processLLAResponse(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + LogDebugEx(LOG_HOST, "TrafficNetwork::processKEKResponse()", "keyLength = %u", keyLength); + Utils::dump(1U, "TrafficNetwork::processKEKResponse(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); } // build response buffer @@ -3068,14 +3138,14 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi KMMModifyKey modifyKeyRsp = KMMModifyKey(); modifyKeyRsp.setDecryptInfoFmt(m_kmfEncKeyRequest ? KMM_DECRYPT_PEER_ENC : KMM_DECRYPT_INSTRUCT_NONE); - modifyKeyRsp.setAlgId(ALGO_AES_128); + modifyKeyRsp.setAlgId(algId); modifyKeyRsp.setKId(0U); modifyKeyRsp.setSrcLLId(WUID_FNE); modifyKeyRsp.setDstLLId(srcId); KeysetItem ks = KeysetItem(); ks.keysetId(1U); - ks.algId(ALGO_AES_128); + ks.algId(algId); ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); @@ -3089,18 +3159,18 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi modifyKeyRsp.encode(buffer + 11U); - writePeer(peerId, m_peerId, { NET_FUNC::KEY_LLA_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U, + writePeer(peerId, m_peerId, { NET_FUNC::KEY_KEK_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U, RTP_END_OF_CALL_SEQ, createStreamId()); peersToRemove.push_back(peerId); } else { - LogError(LOG_PEER, "upstream master LLA enc. key, peerId = %u, requestingRSI = %u, rsi = %u -- mismatch!", entry.first, requestingRid, srcId); + LogError(LOG_PEER, "upstream master UKEK/LLA enc. key, peerId = %u, requestingRSI = %u, rsi = %u -- mismatch!", entry.first, requestingRid, srcId); } } // remove peers who were sent keys for (auto& peerId : peersToRemove) - m_peerReplicaLLAKeyQueue.erase(peerId); + m_peerReplicaKEKKeyQueue.erase(peerId); - s_llaKeyQueueMutex.unlock(); + s_kekKeyQueueMutex.unlock(); } diff --git a/src/fne/network/TrafficNetwork.h b/src/fne/network/TrafficNetwork.h index ab9a675e..6f824c83 100644 --- a/src/fne/network/TrafficNetwork.h +++ b/src/fne/network/TrafficNetwork.h @@ -375,6 +375,7 @@ namespace network bool m_kmfAllowRID0; bool m_kmfEncKeyRequest; uint8_t* m_kmfPresharedKey; + bool m_kmfWarmStart; lookups::RadioIdLookup* m_ridLookup; lookups::RadioAliasLookup* m_ridAliasLookup; @@ -395,8 +396,8 @@ namespace network concurrent::shared_unordered_map> m_ccPeerMap; static std::timed_mutex s_keyQueueMutex; std::unordered_map m_peerReplicaKeyQueue; - static std::timed_mutex s_llaKeyQueueMutex; - std::unordered_map m_peerReplicaLLAKeyQueue; + static std::timed_mutex s_kekKeyQueueMutex; + std::unordered_map m_peerReplicaKEKKeyQueue; // value packs algId in high byte, and RSI in low 24 bits fne_lookups::AffiliationLookup* m_globalAff; @@ -623,7 +624,7 @@ namespace network */ static void keyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now); /** - * @brief Handles NET_FUNC::KEY_LLA_REQ packets. + * @brief Handles NET_FUNC::KEY_KEK_REQ packets. * @param network Instance of the TrafficNetwork class. * @param req Instance of the NetPacketRequest structure. * @param peerId Peer ID. @@ -631,7 +632,7 @@ namespace network * @param streamId Stream ID. * @param now Current time in milliseconds. */ - static void llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now); + static void kekKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now); }; /** @@ -808,6 +809,21 @@ namespace network */ void replicatePatchStatus(json::object obj, uint32_t exceptPeerId = 0U); + /** + * @brief Serializes and queues a patch status transfer payload. + * @param connection Destination connection. + * @param obj Patch status JSON payload. + * @returns bool True, if message was queued, otherwise false. + */ + bool writePatchStatusPayload(FNEPeerConnection* connection, json::object obj); + /** + * @brief Serializes and queues a patch status replication payload. + * @param connection Destination neighbor connection. + * @param obj Patch status JSON payload. + * @returns bool True, if message was queued, otherwise false. + */ + bool writePatchStatusReplicationPayload(FNEPeerConnection* connection, json::object obj); + /* ** ACL Message Writing */ @@ -1072,20 +1088,22 @@ namespace network */ bool writePeerNAK(uint32_t peerId, const char* tag, NET_CONN_NAK_REASON reason, sockaddr_storage& addr, uint32_t addrLen); - /** - * @brief Serializes and queues a patch status transfer payload. - * @param connection Destination connection. - * @param obj Patch status JSON payload. - * @returns bool True, if message was queued, otherwise false. + /* + ** Key Request + */ + + /** + * @brief Sends a TEK query to each enabled upstream replica master. + * @param kid Key ID for the TEK query. + * @param algId Algorithm ID for the TEK query. + * @param requestingRSI RSI of the requesting entity. */ - bool writePatchStatusPayload(FNEPeerConnection* connection, json::object obj); - /** - * @brief Serializes and queues a patch status replication payload. - * @param connection Destination neighbor connection. - * @param obj Patch status JSON payload. - * @returns bool True, if message was queued, otherwise false. + bool requestUpstreamTEK(uint16_t kid, uint8_t algId, uint32_t requestingRSI); + /** + * @brief Sends a UKEK query to each enabled upstream replica master. + * @param rsi RSI of the requesting entity. */ - bool writePatchStatusReplicationPayload(FNEPeerConnection* connection, json::object obj); + bool requestUpstreamUKEK(uint32_t rsi); /* ** Internal KMM Callback. @@ -1095,17 +1113,18 @@ namespace network * @brief Helper to process a FNE KMM TEK response. * @param ki Key Item. * @param algId Algorithm ID. + * @param algId Algorithm ID. * @param keyLength Length of key in bytes. */ void processTEKResponse(p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength); /** - * @brief Helper to process a FNE KMM LLA response. - * @param srcId Source Radio ID for the LLA response. + * @brief Helper to process a FNE KMM UKEK/LLA response. + * @param srcId Source Radio ID for the UKEK/LLA response. * @param ki Key Item. * @param keyLength Length of key in bytes. */ - void processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* ki, uint8_t keyLength); + void processKEKResponse(uint32_t srcId, p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength); /* ** Metrics Helpers diff --git a/src/fne/network/packet/LLAKeyRequest.cpp b/src/fne/network/packet/KEKKeyRequest.cpp similarity index 79% rename from src/fne/network/packet/LLAKeyRequest.cpp rename to src/fne/network/packet/KEKKeyRequest.cpp index 595da6a3..9ac87a52 100644 --- a/src/fne/network/packet/LLAKeyRequest.cpp +++ b/src/fne/network/packet/KEKKeyRequest.cpp @@ -21,9 +21,9 @@ using namespace network; // Public Class Members // --------------------------------------------------------------------------- -/* Handles NET_FUNC::KEY_LLA_REQ packets. */ +/* Handles NET_FUNC::KEY_KEK_REQ packets. */ -void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now) +void TrafficNetwork::PacketHandler::kekKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now) { using namespace p25::defines; using namespace p25::kmm; @@ -59,18 +59,27 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa { KMMModifyKey* modifyKey = static_cast(frame.get()); - if (modifyKey->getAlgId() == ALGO_AES_128 && modifyKey->getDstLLId() > 0U) { + if ((modifyKey->getAlgId() == ALGO_AES_128 || modifyKey->getAlgId() == ALGO_AES_256) && + modifyKey->getDstLLId() > 0U) { if (network->m_debug) - LogDebugEx(LOG_MASTER, "TrafficNetwork::taskNetworkRx()", "PEER %u (%s) LLA enc. key request received, dstLLId = %u, algId = %u, kId = %u", peerId, connection->identWithQualifier().c_str(), + LogDebugEx(LOG_MASTER, "TrafficNetwork::taskNetworkRx()", "PEER %u (%s) UKEK/LLA enc. key request received, dstLLId = %u, algId = %u, kId = %u", peerId, connection->identWithQualifier().c_str(), modifyKey->getDstLLId(), modifyKey->getAlgId(), modifyKey->getKId()); uint32_t requestingRid = modifyKey->getDstLLId(); - LogInfoEx(LOG_MASTER, "PEER %u (%s) requested LLA enc. key, rsi = %u", peerId, connection->identWithQualifier().c_str(), - requestingRid); + // determine if the request is for a UKEK or LLA encryption key + const bool ukekRequest = modifyKey->getAlgId() == ALGO_AES_256; + if (ukekRequest && connection->peerClass() != PEER_CONN_CLASS_NEIGHBOR) { + LogError(LOG_MASTER, "PEER %u (%s) requested UKEK but is not a neighbor FNE, no response", + peerId, connection->identWithQualifier().c_str()); + return; + } + + LogInfoEx(LOG_MASTER, "PEER %u (%s) requested %s key, rsi = %u", peerId, connection->identWithQualifier().c_str(), ukekRequest ? "UKEK" : "LLA enc.", requestingRid); - ::EKCKeyItem keyItem = network->m_cryptoLookup->findLLA(requestingRid); - if (!keyItem.isInvalid()) { + ::EKCKeyItem keyItem = ukekRequest ? network->m_cryptoLookup->findUKEK(requestingRid) : + network->m_cryptoLookup->findLLA(requestingRid); + if (!keyItem.isInvalid() && keyItem.algId() == modifyKey->getAlgId()) { uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; ::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); uint8_t keyLength = keyItem.getKey(key); @@ -120,7 +129,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KMM_KEY_FORMAT_TEK); + ki.keyFormat(ukekRequest ? KMM_KEY_FORMAT_KEK : KMM_KEY_FORMAT_TEK); ki.kId((uint16_t)keyItem.kId()); ki.sln((uint16_t)keyItem.sln()); ki.setKey(key, keyLength); @@ -130,7 +139,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa modifyKeyRsp.encode(buffer + 11U); - network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_LLA_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U, + network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_KEK_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U, RTP_END_OF_CALL_SEQ, network->createStreamId()); } else { // attempt to forward KMM key request to replica masters @@ -141,13 +150,13 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, rsi = %u", peerId, connection->identWithQualifier().c_str(), modifyKey->getAlgId(), requestingRid); - bool locked = network->s_llaKeyQueueMutex.try_lock_for(std::chrono::milliseconds(60)); - network->m_peerReplicaLLAKeyQueue[peerId] = modifyKey->getDstLLId(); - - if (locked) - network->s_llaKeyQueueMutex.unlock(); + { + std::lock_guard lock(network->s_kekKeyQueueMutex); + network->m_peerReplicaKEKKeyQueue[peerId] = + ((uint32_t)modifyKey->getAlgId() << 24U) | (modifyKey->getDstLLId() & 0xFFFFFFU); + } - peer.second->writeMaster({ NET_FUNC::KEY_LLA_REQ, NET_SUBFUNC::NOP }, + peer.second->writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP }, req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false); } } diff --git a/src/fne/network/packet/KeyRequest.cpp b/src/fne/network/packet/KeyRequest.cpp index d29b884d..bc49eeb2 100644 --- a/src/fne/network/packet/KeyRequest.cpp +++ b/src/fne/network/packet/KeyRequest.cpp @@ -101,7 +101,7 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke LogInfoEx(LOG_MASTER, "PEER %u (%s) requested enc. key, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(), modifyKey->getAlgId(), modifyKey->getKId()); ::EKCKeyItem keyItem = network->m_cryptoLookup->find(modifyKey->getKId()); - if (!keyItem.isInvalid()) { + if (!keyItem.isInvalid() && keyItem.algId() == modifyKey->getAlgId()) { uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; ::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); uint8_t keyLength = keyItem.getKey(key); @@ -170,11 +170,10 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(), modifyKey->getAlgId(), modifyKey->getKId()); - bool locked = network->s_keyQueueMutex.try_lock_for(std::chrono::milliseconds(60)); - network->m_peerReplicaKeyQueue[peerId] = modifyKey->getKId(); - - if (locked) - network->s_keyQueueMutex.unlock(); + { + std::lock_guard lock(network->s_keyQueueMutex); + network->m_peerReplicaKeyQueue[peerId] = modifyKey->getKId(); + } peer.second->writeMaster({ NET_FUNC::KEY_REQ, NET_SUBFUNC::NOP }, req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false); diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 28b51ecf..5dea949a 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -257,7 +257,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw if (m_control->m_requireLLAForReg) { if (m_network != nullptr) { - m_network->setLLAKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t keyLength) { + m_network->setKEKKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) { + (void)algId; processLLAResponse(srcId, &ki, keyLength); }); } diff --git a/src/host/p25/packet/Data.cpp b/src/host/p25/packet/Data.cpp index c7681248..deb4aaa6 100644 --- a/src/host/p25/packet/Data.cpp +++ b/src/host/p25/packet/Data.cpp @@ -1102,8 +1102,10 @@ bool Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ac // generate NID m_p25->m_nid.encode(data + 2U, DUID::PDU); - // add status bits - P25Utils::addStatusBits(data + 2U, newBitLength, m_inbound, true); + // advertise the inbound channel as idle when no RF PDU is being received + // the remaining, alternating status-symbol positions stay "unknown" as + // required by P25Utils::addStatusBits() + P25Utils::addStatusBits(data + 2U, newBitLength, m_inbound, false); P25Utils::setStatusBitsStartIdle(data + 2U); //Utils::dump("P25, Data::writeRF_PDU(), Raw PDU OSP", data, newByteLength + 2U); diff --git a/tests/fne/FNETestHooks.cpp b/tests/fne/FNETestHooks.cpp index b09ad206..90d0ff2f 100644 --- a/tests/fne/FNETestHooks.cpp +++ b/tests/fne/FNETestHooks.cpp @@ -125,8 +125,15 @@ std::vector> FNETestHooks::buildOTARRekey(TrafficNetwork& n return frames; } -/** Builds an OTAR Changeover Command without dispatching it. */ - +/** + * @brief Builds an OTAR Changeover Command without dispatching it. + * @param network The TrafficNetwork instance. + * @param llId The logical link ID. + * @param kmmRSI The KMM RSI value. + * @param supersededKeysetId The ID of the superseded keyset. + * @param activeKeysetId The ID of the active keyset. + * @return A vector containing the encoded OTAR Changeover Command. + */ std::vector FNETestHooks::buildOTARChangeover(TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId) { @@ -142,6 +149,102 @@ std::vector FNETestHooks::buildOTARChangeover(TrafficNetwork& network, return std::vector(encoded.get(), encoded.get() + length); } +/** + * @brief Builds an OTAR Warm Start Command without dispatching it. + * @param network The TrafficNetwork instance. + * @param llId The logical link ID. + * @param kmmRSI The KMM RSI value. + * @return A vector containing the encoded OTAR Warm Start Command. + */ +std::vector FNETestHooks::buildOTARWarmStart(TrafficNetwork& network, + uint32_t llId, uint32_t kmmRSI) +{ + if (network.m_p25OTARService == nullptr) + return {}; + uint32_t length = 0U; + UInt8Array encoded = network.m_p25OTARService->write_KMM_WarmStart_Command(llId, kmmRSI, &length); + if (encoded == nullptr || length == 0U) + return {}; + return std::vector(encoded.get(), encoded.get() + length); +} + +/** + * @brief Retrieves the temporary TEK for an OTAR Warm Start transaction. + * @param network The TrafficNetwork instance. + * @param rsi The RSI associated with the Warm Start transaction. + * @param kid Output parameter for the key ID. + * @param key Output parameter for the temporary TEK. + * @return True if the temporary TEK was successfully retrieved, false otherwise. + */ +bool FNETestHooks::getOTARWarmStartTEK(TrafficNetwork& network, uint32_t rsi, + uint16_t& kid, std::vector& key) +{ + if (network.m_p25OTARService == nullptr) + return false; + std::lock_guard lock(network.m_p25OTARService->m_warmStartMutex); + auto it = network.m_p25OTARService->m_warmStartTransactions.find(rsi); + if (it == network.m_p25OTARService->m_warmStartTransactions.end()) + return false; + kid = it->second.temporaryKId; + key.assign(it->second.temporaryTEK.begin(), it->second.temporaryTEK.end()); + return true; +} + +/** + * @brief Checks if there is an active OTAR Warm Start transaction for the given RSI. + * @param network The TrafficNetwork instance. + * @param rsi The RSI to check. + * @return True if there is an active Warm Start transaction, false otherwise. + */ +bool FNETestHooks::hasOTARWarmStart(TrafficNetwork& network, uint32_t rsi) +{ + if (network.m_p25OTARService == nullptr) + return false; + std::lock_guard lock(network.m_p25OTARService->m_warmStartMutex); + return network.m_p25OTARService->m_warmStartTransactions.find(rsi) != + network.m_p25OTARService->m_warmStartTransactions.end(); +} + +/** + * @brief Injects and resolves an upstream TEK response through the OTAR cache. + * @param network The TrafficNetwork instance. + * @param key The KMM key item representing the TEK. + * @param algId The algorithm ID used for the TEK. + * @param keyLength The length of the TEK. + * @return True if the TEK was successfully cached and resolved, false otherwise. + */ +bool FNETestHooks::cacheOTARTEK(TrafficNetwork& network, + const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength) +{ + if (network.m_p25OTARService == nullptr) + return false; + network.m_p25OTARService->cacheUpstreamTEK(key, algId, keyLength); + const uint32_t id = ((uint32_t)algId << 16U) | key.kId(); + auto it = network.m_p25OTARService->m_upstreamTEKs.find(id); + return it != network.m_p25OTARService->m_upstreamTEKs.end() && + it->second.algorithmId == algId && it->second.keyLength == keyLength; +} + +/** + * @brief Injects and resolves an upstream UKEK response through the OTAR cache. + * @param network The TrafficNetwork instance. + * @param rsi The RSI associated with the UKEK. + * @param key The KMM key item representing the UKEK. + * @param algId The algorithm ID used for the UKEK. + * @param keyLength The length of the UKEK. + * @return True if the UKEK was successfully cached and resolved, false otherwise. + */ +bool FNETestHooks::cacheOTARUKEK(TrafficNetwork& network, uint32_t rsi, + const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength) +{ + if (network.m_p25OTARService == nullptr) + return false; + network.m_p25OTARService->cacheUpstreamUKEK(rsi, key, algId, keyLength); + auto it = network.m_p25OTARService->m_upstreamUKEKs.find(rsi); + return it != network.m_p25OTARService->m_upstreamUKEKs.end() && + it->second.algorithmId == algId && it->second.keyLength == keyLength; +} + /** * @brief Resolves the security parameters of an OTAR response packet. * @param network The TrafficNetwork instance. @@ -383,6 +486,16 @@ void FNETestHooks::addCryptoKey(TrafficNetwork& network, const EKCKeyItem& key) network.m_cryptoLookup->addEntry(key); } +/** + * @brief Adds a UKEK to the test crypto container. + * @param network The TrafficNetwork instance. + * @param key The UKEK key item to add. + */ +void FNETestHooks::addCryptoUKEK(TrafficNetwork& network, const EKCKeyItem& key) +{ + network.m_cryptoLookup->addUKEK(key); +} + // --------------------------------------------------------------------------- // Static Class Members // --------------------------------------------------------------------------- diff --git a/tests/fne/FNETestHooks.h b/tests/fne/FNETestHooks.h index 14b1059d..c9865efb 100644 --- a/tests/fne/FNETestHooks.h +++ b/tests/fne/FNETestHooks.h @@ -118,6 +118,55 @@ public: */ static std::vector buildOTARChangeover(network::TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId); + /** + * @brief Builds and records an OTAR Warm-Start Command. + * @param network The TrafficNetwork instance. + * @param llId Logical Link ID used for message-number tracking. + * @param kmmRSI Destination KMM RSI. + * @return Encoded command, or an empty vector when it cannot be built. + * + */ + static std::vector buildOTARWarmStart(network::TrafficNetwork& network, + uint32_t llId, uint32_t kmmRSI); + /** + * @brief Retrieves an active Warm-Start temporary key for transaction tests. + * @param network The TrafficNetwork instance. + * @param rsi The RSI for which to retrieve the temporary key. + * @param kid Output parameter for the key ID of the temporary key. + * @param key Output parameter for the temporary key bytes. + * @return True if an active Warm-Start temporary key exists, false otherwise. + * + */ + static bool getOTARWarmStartTEK(network::TrafficNetwork& network, uint32_t rsi, + uint16_t& kid, std::vector& key); + /** + * @brief Returns whether an active Warm-Start transaction exists. + * @param network The TrafficNetwork instance. + * @param rsi The RSI for which to check the Warm-Start transaction. + * @return True if an active Warm-Start transaction exists, false otherwise. + */ + static bool hasOTARWarmStart(network::TrafficNetwork& network, uint32_t rsi); + /** + * @brief Injects and verifies an upstream TEK response in the OTAR cache. + * @param network The TrafficNetwork instance. + * @param key The key item to cache. + * @param algId The algorithm ID of the key. + * @param keyLength The length of the key in bytes. + * @return True if the key was successfully cached, false otherwise. + */ + static bool cacheOTARTEK(network::TrafficNetwork& network, + const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength); + /** + * @brief Injects and verifies an upstream UKEK response in the OTAR cache. + * @param network The TrafficNetwork instance. + * @param rsi The RSI for which to cache the UKEK. + * @param key The key item to cache. + * @param algId The algorithm ID of the key. + * @param keyLength The length of the key in bytes. + * @return True if the key was successfully cached, false otherwise. + */ + static bool cacheOTARUKEK(network::TrafficNetwork& network, uint32_t rsi, + const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength); /** * @brief Resolves the outer security context selected for a generated response. * @param network The TrafficNetwork instance. @@ -224,6 +273,12 @@ public: * @param key The cryptographic key to add. */ static void addCryptoKey(network::TrafficNetwork& network, const EKCKeyItem& key); + /** + * @brief Adds a UKEK to the test crypto container. + * @param network The TrafficNetwork instance. + * @param key The UKEK key item to add. + */ + static void addCryptoUKEK(network::TrafficNetwork& network, const EKCKeyItem& key); }; #endif // __FNE_TEST_HOOKS_H__ diff --git a/tests/p25/P25_KMM_Tests.cpp b/tests/p25/P25_KMM_Tests.cpp index dd70d10c..3baaacc2 100644 --- a/tests/p25/P25_KMM_Tests.cpp +++ b/tests/p25/P25_KMM_Tests.cpp @@ -9,6 +9,7 @@ */ #include "host/Defines.h" #include "common/p25/P25Defines.h" +#include "common/p25/Crypto.h" #include "common/p25/kmm/KMMFactory.h" #include "common/p25/kmm/KMMChangeover.h" #include "common/p25/kmm/KMMDeregistrationCommand.h" @@ -19,6 +20,7 @@ #include "common/p25/kmm/KMMNegativeAck.h" #include "common/p25/kmm/KMMRekeyAck.h" #include "common/p25/kmm/KMMRekeyCommand.h" +#include "common/p25/kmm/KMMWarmStartCommand.h" #include "fne/FNETestHooks.h" #include "fne/HostFNE.h" #include "common/lookups/RadioIdLookup.h" @@ -135,6 +137,160 @@ TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm] } } +TEST_CASE("KMM Warm-Start Command round-trips its wrapped temporary TEK", "[p25][kmm][warm-start]") +{ + uint8_t wrapped[P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES]; + for (uint8_t i = 0U; i < sizeof(wrapped); ++i) + wrapped[i] = i; + + KeyItem key; + key.keyFormat(KMM_KEY_FORMAT_TEK); + key.sln(0U); + key.kId(0x4567U); + key.setKey(wrapped, sizeof(wrapped)); + + KMMWarmStartCommand tx; + tx.setDstLLId(0x123456U); + tx.setSrcLLId(WUID_FNE); + tx.setHasMessageNumber(true); + tx.setMessageNumber(7U); + tx.setMACType(KMM_MAC::ENH_MAC); + tx.setMACAlgId(ALGO_AES_256); + tx.setMACKId(0x4567U); + tx.setMACFormat(KMM_MAC_FORMAT_CMAC); + tx.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE); + tx.setKEKAlgId(ALGO_AES_256); + tx.setKEKKId(0x1122U); + tx.setKeyLength(sizeof(wrapped)); + tx.setTEKAlgId(ALGO_AES_256); + tx.setKey(key); + + std::vector encoded = encodeKMM(tx); + std::unique_ptr base = KMMFactory::create(encoded.data(), encoded.size()); + REQUIRE(base != nullptr); + KMMWarmStartCommand* rx = dynamic_cast(base.get()); + REQUIRE(rx != nullptr); + REQUIRE(rx->getMessageNumber() == 7U); + REQUIRE(rx->getKEKAlgId() == ALGO_AES_256); + REQUIRE(rx->getKEKKId() == 0x1122U); + REQUIRE(rx->getKeyLength() == sizeof(wrapped)); + REQUIRE(rx->getTEKAlgId() == ALGO_AES_256); + REQUIRE(rx->getKey().keyFormat() == KMM_KEY_FORMAT_TEK); + REQUIRE(rx->getKey().sln() == 0U); + REQUIRE(rx->getKey().kId() == 0x4567U); + + uint8_t decoded[sizeof(wrapped)] = { 0U }; + rx->getKey().getKey(decoded); + REQUIRE(::memcmp(decoded, wrapped, sizeof(wrapped)) == 0); + + encoded[10U + 2U + 4U + 2U] = KMM_KEY_FORMAT_KEK; + REQUIRE(KMMFactory::create(encoded.data(), encoded.size()) == nullptr); +} + +TEST_CASE("P25 OTAR Warm Start transitions through temporary TEK Rekey", "[p25][kmm][otar][warm-start]") +{ + KMMFNEHarness harness; + constexpr uint32_t SU_RSI = 0x654321U; + constexpr uint16_t UKEK_KID = 0x1001U; + constexpr uint16_t FINAL_KID = 0x2001U; + const char* ukekHex = "101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F"; + const char* finalHex = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"; + + EKCKeyItem ukek; + ukek.id(1U); ukek.rsiId(SU_RSI); ukek.algId(ALGO_AES_256); + ukek.kId(UKEK_KID); ukek.sln(0U); ukek.keyMaterial(ukekHex); + FNETestHooks::addCryptoUKEK(harness.traffic, ukek); + + EKCKeyItem finalTEK; + finalTEK.id(2U); finalTEK.algId(ALGO_AES_256); finalTEK.kId(FINAL_KID); + finalTEK.sln(1U); finalTEK.keyMaterial(finalHex); + FNETestHooks::addCryptoKey(harness.traffic, finalTEK); + harness.rid.addEntry(SU_RSI, true, "warm-start-test", "", true, true, { FINAL_KID }); + + FNETestHooks::setKMFServicesEnabled(harness.traffic, true); + KMMHello hello; + hello.setDstLLId(WUID_FNE); hello.setSrcLLId(SU_RSI); + hello.setResponseKind(KMM_ResponseKind::IMMEDIATE); + hello.setFlag(KMM_HelloFlag::REKEY_REQUEST_UKEK); + std::vector helloBytes = encodeKMM(hello); + uint32_t commandLength = 0U; + UInt8Array commandResponse = FNETestHooks::processOTARKMM(harness.traffic, + helloBytes, SU_RSI, commandLength); + REQUIRE(commandResponse != nullptr); + std::vector commandBytes(commandResponse.get(), commandResponse.get() + commandLength); + REQUIRE_FALSE(commandBytes.empty()); + std::unique_ptr commandBase = KMMFactory::create(commandBytes.data(), commandBytes.size()); + REQUIRE(commandBase != nullptr); + KMMWarmStartCommand* command = dynamic_cast(commandBase.get()); + REQUIRE(command != nullptr); + CHECK(command->getMessageNumber() == 3U); + CHECK(command->getMACFormat() == KMM_MAC_FORMAT_CBC); + CHECK(command->getKEKKId() == UKEK_KID); + CHECK(command->getKeyLength() == P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); + + uint16_t temporaryKId = 0U; + std::vector temporaryTEK; + REQUIRE(FNETestHooks::getOTARWarmStartTEK(harness.traffic, SU_RSI, + temporaryKId, temporaryTEK)); + REQUIRE(temporaryTEK.size() == P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + CHECK(command->getKey().kId() == temporaryKId); + REQUIRE(command->verifyMAC(temporaryTEK.data(), commandBytes.data(), commandBytes.size())); + + uint8_t wrapped[P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES] = { 0U }; + command->getKey().getKey(wrapped); + uint8_t ukekBytes[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { + 0x10U,0x11U,0x12U,0x13U,0x14U,0x15U,0x16U,0x17U, + 0x18U,0x19U,0x1AU,0x1BU,0x1CU,0x1DU,0x1EU,0x1FU, + 0x20U,0x21U,0x22U,0x23U,0x24U,0x25U,0x26U,0x27U, + 0x28U,0x29U,0x2AU,0x2BU,0x2CU,0x2DU,0x2EU,0x2FU }; + p25::crypto::P25Crypto crypto; + UInt8Array unwrapped = crypto.decryptAES_TEK(ukekBytes, wrapped, sizeof(wrapped)); + REQUIRE(unwrapped != nullptr); + REQUIRE(::memcmp(unwrapped.get(), temporaryTEK.data(), temporaryTEK.size()) == 0); + + KeyStatus warmStatus; + warmStatus.algId(ALGO_AES_256); warmStatus.kId(temporaryKId); + warmStatus.status(KMM_Status::CMD_PERFORMED); + KMMRekeyAck warmAck; + warmAck.setDstLLId(WUID_FNE); warmAck.setSrcLLId(SU_RSI); + warmAck.setHasMessageNumber(true); warmAck.setMessageNumber(command->getMessageNumber()); + warmAck.setMACType(KMM_MAC::ENH_MAC); warmAck.setMACAlgId(ALGO_AES_256); + warmAck.setMACKId(temporaryKId); warmAck.setMACFormat(KMM_MAC_FORMAT_CMAC); + warmAck.setAckMessageId(KMM_MessageType::WARM_START_CMD); + warmAck.setNumberOfKeyStatus(1U); warmAck.setKeyStatus({ warmStatus }); + std::vector warmAckBytes = encodeKMM(warmAck); + warmAck.generateMAC(temporaryTEK.data(), warmAckBytes.data()); + + uint32_t rekeyLength = 0U; + UInt8Array rekeyBytes = FNETestHooks::processOTARKMM(harness.traffic, warmAckBytes, + SU_RSI, rekeyLength, ALGO_AES_256, temporaryKId); + REQUIRE(rekeyBytes != nullptr); + std::unique_ptr rekeyBase = KMMFactory::create(rekeyBytes.get()); + KMMRekeyCommand* rekey = dynamic_cast(rekeyBase.get()); + REQUIRE(rekey != nullptr); + CHECK(rekey->getKId() == temporaryKId); + REQUIRE(rekey->verifyMAC(temporaryTEK.data(), rekeyBytes.get(), rekeyLength)); + + uint8_t finalKey[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + finalTEK.getKey(finalKey); + KeyStatus rekeyStatus; + rekeyStatus.algId(ALGO_AES_256); rekeyStatus.kId(FINAL_KID); + rekeyStatus.status(KMM_Status::CMD_PERFORMED); + KMMRekeyAck finalAck; + finalAck.setDstLLId(WUID_FNE); finalAck.setSrcLLId(SU_RSI); + finalAck.setHasMessageNumber(true); finalAck.setMessageNumber(rekey->getMessageNumber()); + finalAck.setMACType(KMM_MAC::ENH_MAC); finalAck.setMACAlgId(ALGO_AES_256); + finalAck.setMACKId(FINAL_KID); finalAck.setMACFormat(KMM_MAC_FORMAT_CMAC); + finalAck.setAckMessageId(KMM_MessageType::REKEY_CMD); + finalAck.setNumberOfKeyStatus(1U); finalAck.setKeyStatus({ rekeyStatus }); + std::vector finalAckBytes = encodeKMM(finalAck); + finalAck.generateMAC(finalKey, finalAckBytes.data()); + uint32_t finalResponseLength = 0U; + REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, finalAckBytes, SU_RSI, + finalResponseLength, ALGO_AES_256, FINAL_KID) == nullptr); + CHECK_FALSE(FNETestHooks::hasOTARWarmStart(harness.traffic, SU_RSI)); +} + TEST_CASE("KMM air-facing factory rejects malformed lengths", "[p25][kmm][security]") { KMMHello hello; @@ -613,9 +769,9 @@ TEST_CASE("P25 OTAR Rekey Commands batch four keys per KMM", "[p25][kmm][otar][r REQUIRE(rekey != nullptr); REQUIRE(rekey->getKeysets().size() == 1U); CHECK(rekey->getKeysets()[0U].keys().size() == expectedCounts[i]); - // A new RK3 uses Rule 1 message-number validation. The SU initializes - // MNL to zero, so the first valid outbound KMF message number is one. - CHECK(rekey->getMessageNumber() == i + 1U); + // A new RK3 uses Rule 1 message-number validation. The interoperating + // Bootstrap past the Harris subscriber's retained MNL values one and two. + CHECK(rekey->getMessageNumber() == i + 3U); CHECK(rekey->getComplete() == (i + 1U == frames.size())); CHECK(frames[i].size() <= 287U); } @@ -654,7 +810,7 @@ TEST_CASE("P25 OTAR builds authenticated Changeover Commands without dispatching CHECK(changeover->getSupersededKeysetId() == SUPERSEDED_KEYSET); CHECK(changeover->getActiveKeysetId() == ACTIVE_KEYSET); CHECK(changeover->getHasMessageNumber()); - CHECK(changeover->getMessageNumber() == 1U); + CHECK(changeover->getMessageNumber() == 3U); CHECK(changeover->getMACType() == KMM_MAC::ENH_MAC); CHECK(changeover->getMACAlgId() == ALGO_AES_256); CHECK(changeover->getMACKId() == TEK_KID); @@ -673,6 +829,30 @@ TEST_CASE("P25 OTAR builds authenticated Changeover Commands without dispatching CHECK(kid == TEK_KID); } +TEST_CASE("P25 OTAR accepts upstream TEK and UKEK responses", "[p25][kmm][otar][upstream-keys]") +{ + KMMFNEHarness harness; + constexpr uint32_t SU_RSI = 0x654321U; + uint8_t material[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + for (uint32_t i = 0U; i < sizeof(material); ++i) + material[i] = (uint8_t)i; + + p25::kmm::KeyItem tek; + tek.keyFormat(KMM_KEY_FORMAT_TEK); + tek.kId(0x1234U); + tek.sln(1U); + tek.setKey(material, sizeof(material)); + CHECK(FNETestHooks::cacheOTARTEK(harness.traffic, tek, ALGO_AES_256, sizeof(material))); + + p25::kmm::KeyItem ukek; + ukek.keyFormat(KMM_KEY_FORMAT_KEK); + ukek.kId(0x4321U); + ukek.sln(0U); + ukek.setKey(material, sizeof(material)); + CHECK(FNETestHooks::cacheOTARUKEK(harness.traffic, SU_RSI, ukek, + ALGO_AES_256, sizeof(material))); +} + #if !defined(_WIN32) TEST_CASE("P25 OTAR DLI traverses the real UDP endpoint", "[p25][kmm][otar][dli][integration]") {