From 808aa8b70f30d833ee5ae0a79d5517322de5956f Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 7 Oct 2026 10:36:49 -0400 Subject: [PATCH] correct bad KMM authentication and replay protection; correctly negotiate proper response authentication; correct key-format semantics (there was a serious mismatch in P25Defines causing incorrect handling of values); harden KMM parsing and framing; fix PDU AES OFB processing; expand the P25 OTAR and crypto tests; --- src/common/p25/Crypto.cpp | 18 +- src/common/p25/Crypto.h | 2 +- src/common/p25/P25Defines.h | 23 +- src/common/p25/kmm/KMMFactory.cpp | 70 ++++++ src/common/p25/kmm/KMMFactory.h | 7 + src/common/p25/kmm/KMMFrame.cpp | 24 ++ src/common/p25/kmm/KMMFrame.h | 12 +- src/common/p25/kmm/KMMModifyKey.h | 2 +- src/common/p25/kmm/KMMNegativeAck.cpp | 4 +- src/common/p25/kmm/KMMRekeyCommand.h | 2 +- src/common/p25/kmm/KMMUnableToDecrypt.cpp | 8 +- src/common/p25/kmm/KeysetItem.h | 5 +- src/fne/network/P25OTARService.cpp | 293 +++++++++++++++++----- src/fne/network/P25OTARService.h | 36 ++- src/fne/network/TrafficNetwork.cpp | 4 +- src/fne/network/packet/KeyRequest.cpp | 2 +- src/fne/network/packet/LLAKeyRequest.cpp | 2 +- tests/fne/FNETestHooks.cpp | 83 ++++++ tests/fne/FNETestHooks.h | 41 ++- tests/p25/KMM_Rekey_CBC_Test.cpp | 45 +++- tests/p25/KMM_Rekey_CMAC_Test.cpp | 30 ++- tests/p25/P25_Crypto_Tests.cpp | 91 +++++++ tests/p25/P25_KMM_Tests.cpp | 205 ++++++++++++++- 23 files changed, 900 insertions(+), 109 deletions(-) create mode 100644 tests/p25/P25_Crypto_Tests.cpp diff --git a/src/common/p25/Crypto.cpp b/src/common/p25/Crypto.cpp index c4a96b0a..69284450 100644 --- a/src/common/p25/Crypto.cpp +++ b/src/common/p25/Crypto.cpp @@ -171,8 +171,8 @@ void P25Crypto::generateKeystream() case ALGO_AES_256: { if (m_keystream == nullptr) - m_keystream = new uint8_t[240U]; - ::memset(m_keystream, 0x00U, 240U); + m_keystream = new uint8_t[528U]; + ::memset(m_keystream, 0x00U, 528U); uint8_t* iv = expandMIToIV(); @@ -182,7 +182,7 @@ void P25Crypto::generateKeystream() ::memset(input, 0x00U, 16U); ::memcpy(input, iv, 16U); - for (uint32_t i = 0U; i < (240U / 16U); i++) { + for (uint32_t i = 0U; i < (528U / 16U); i++) { uint8_t* output = aes.encryptECB(input, 16U, m_tek.get()); ::memcpy(m_keystream + (i * 16U), output, 16U); ::memcpy(input, output, 16U); @@ -671,16 +671,18 @@ UInt8Array P25Crypto::cryptAES_KMM_CMAC(const uint8_t* macKey, const uint8_t* ms /* Helper to crypt a P25 PDU frame using AES-256. */ -void P25Crypto::cryptAES_PDU(uint8_t* frame, uint8_t frameLen) +void P25Crypto::cryptAES_PDU(uint8_t* frame, uint32_t frameLen) { if (m_keystream == nullptr) return; + // per AAAD-B the first OFB iteration is discarded -- generate enough + // contiguous stream for the complete AACA-D 512-octet logical message; + // repeating a shorter stream is not OFB and leaks plaintext relationships uint32_t offset = 16U; - for (uint8_t i = 0U; i < frameLen; i++) { - if (offset > 240U) { - offset = 16U; - } + for (uint32_t i = 0U; i < frameLen; i++) { + if (offset >= 528U) + return; frame[i] ^= m_keystream[offset]; offset++; diff --git a/src/common/p25/Crypto.h b/src/common/p25/Crypto.h index 9a06dfa9..aff5e48c 100644 --- a/src/common/p25/Crypto.h +++ b/src/common/p25/Crypto.h @@ -137,7 +137,7 @@ namespace p25 * @param frame Data frame * @param frameLen Data frame Length */ - void cryptAES_PDU(uint8_t* frame, uint8_t frameLen); + void cryptAES_PDU(uint8_t* frame, uint32_t frameLen); /** * @brief Helper to crypt P25 IMBE audio using DES. diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index 7f8f34b0..5d32ec45 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -605,15 +605,20 @@ namespace p25 /** @brief KMM MAC Format - CMAC */ const uint8_t KMM_MAC_FORMAT_CMAC = 0x41U; - /** @brief KMM Keyset Format - KEKs */ - const uint8_t KEYSET_FORMAT_KEK = 0x80U; - - /** @brief KMM Body/Key Format - TEK Included */ - const uint8_t KEY_FORMAT_TEK = 0x80U; - /** @brief KMM Body/Key Format - KEK Exists */ - const uint8_t KEY_FORMAT_KEK_EXISTS = 0x40U; - /** @brief KMM Key Format - Delete Key */ - const uint8_t KEY_FORMAT_DELETE = 0x20U; + /** @brief AACA-D Table 64 Body Format - reverse-warm-start TEK segment included. */ + const uint8_t KMM_BODY_FORMAT_TEK_INCLUDED = 0x80U; + /** @brief AACA-D Table 64 Body Format - KEK does not exist. */ + const uint8_t KMM_BODY_FORMAT_KEK_MISSING = 0x40U; + + /** @brief AACA-D Table 70 Key Format - item is a TEK (T bit clear). */ + const uint8_t KMM_KEY_FORMAT_TEK = 0x00U; + /** @brief AACA-D Table 70 Key Format - item is a KEK (T bit set). */ + const uint8_t KMM_KEY_FORMAT_KEK = 0x80U; + /** @brief AACA-D Table 70 Key Format - delete/revoke item. */ + const uint8_t KMM_KEY_FORMAT_DELETE = 0x20U; + + /** @brief AACA-D Table 71 Keyset Format - keyset contains KEKs. */ + const uint8_t KMM_KEYSET_FORMAT_KEK = 0x80U; /** @brief SNDCP version 1 */ const uint8_t SNDCP_VERSION_1 = 0x01U; diff --git a/src/common/p25/kmm/KMMFactory.cpp b/src/common/p25/kmm/KMMFactory.cpp index 5fb15c0a..5e7b4574 100644 --- a/src/common/p25/kmm/KMMFactory.cpp +++ b/src/common/p25/kmm/KMMFactory.cpp @@ -101,6 +101,76 @@ std::unique_ptr KMMFactory::create(const uint8_t* data) return nullptr; } +/* Create an instance of a KMMFrame with a specified length. */ + +std::unique_ptr KMMFactory::create(const uint8_t* data, uint32_t len) +{ + if (data == nullptr || len < 10U || len > 512U) + return nullptr; + + const uint32_t fullLength = (((uint32_t)data[1U] << 8U) | data[2U]) + 3U; + if (fullLength < 10U || fullLength > len || fullLength > 512U) + return nullptr; + + const uint8_t mnCode = (data[3U] >> 4U) & 0x03U; + const uint8_t macType = (data[3U] >> 2U) & 0x03U; + if ((mnCode != 0U && mnCode != 2U) || macType == 1U) + return nullptr; + + const uint32_t body = 10U + ((mnCode == 2U) ? 2U : 0U); + uint32_t bodyEnd = fullLength; + if (macType == KMM_MAC::DES_MAC) { + if (bodyEnd < 7U) return nullptr; + bodyEnd -= 7U; + } else if (macType == KMM_MAC::ENH_MAC) { + if (bodyEnd < 13U || data[bodyEnd - 5U] != P25DEF::KMM_AES_MAC_LENGTH) + return nullptr; + bodyEnd -= 13U; + } + if (body > bodyEnd) + return nullptr; + + const uint32_t available = bodyEnd - body; + switch (data[0U]) { + case KMM_MessageType::HELLO: + case KMM_MessageType::INVENTORY_CMD: + case KMM_MessageType::DEREG_RSP: + case KMM_MessageType::REG_RSP: + if (available < 1U) + return nullptr; + break; + case KMM_MessageType::DEREG_CMD: + case KMM_MessageType::REG_CMD: + if (available < 4U) + return nullptr; + break; + case KMM_MessageType::NAK: + if (available < 4U) + return nullptr; + break; + case KMM_MessageType::REKEY_ACK: + if (available < 2U || available < 2U + (uint32_t)data[body + 1U] * 4U) + return nullptr; + break; + case KMM_MessageType::NO_SERVICE: + case KMM_MessageType::ZEROIZE_CMD: + case KMM_MessageType::ZEROIZE_RSP: + break; + case KMM_MessageType::UNABLE_TO_DECRYPT: + // the optional reverse-warm-start body is not accepted until its full + // nested length validation is implemented + if (available < 7U || (data[body] & 0x80U) != 0U) + return nullptr; + break; + default: + // other decoders contain nested count fields; keep the air-facing path + // fail-closed until each has a dedicated structural validator + return nullptr; + } + + return create(data); +} + // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- diff --git a/src/common/p25/kmm/KMMFactory.h b/src/common/p25/kmm/KMMFactory.h index 38cb85b2..a55cd87a 100644 --- a/src/common/p25/kmm/KMMFactory.h +++ b/src/common/p25/kmm/KMMFactory.h @@ -67,6 +67,13 @@ namespace p25 * @returns KMMFrame* Instance of a KMMFrame representing the decoded data. */ static std::unique_ptr create(const uint8_t* data); + /** + * @brief Create an instance of a KMMFrame with a specified length. + * @param[in] data Buffer containing KMM frame packet data to decode. + * @param[in] len Length of the buffer. + * @returns KMMFrame* Instance of a KMMFrame representing the decoded data. + */ + static std::unique_ptr create(const uint8_t* data, uint32_t len); private: /** diff --git a/src/common/p25/kmm/KMMFrame.cpp b/src/common/p25/kmm/KMMFrame.cpp index 38a4a43f..e7843a24 100644 --- a/src/common/p25/kmm/KMMFrame.cpp +++ b/src/common/p25/kmm/KMMFrame.cpp @@ -136,6 +136,30 @@ void KMMFrame::generateMAC(uint8_t* kek, uint8_t* data) } } +/* Verify the MAC code for the given KMM frame. */ + +bool KMMFrame::verifyMAC(const uint8_t* tek, const uint8_t* data, uint32_t len) +{ + if (tek == nullptr || data == nullptr || m_macType != KMM_MAC::ENH_MAC || + m_messageFullLength > len || m_messageFullLength < P25DEF::KMM_AES_MAC_LENGTH + 5U) + return false; + + UInt8Array candidate = std::make_unique(m_messageFullLength); + ::memcpy(candidate.get(), data, m_messageFullLength); + + uint8_t expected[P25DEF::KMM_AES_MAC_LENGTH]; + const uint32_t macOffset = m_messageFullLength - (P25DEF::KMM_AES_MAC_LENGTH + 5U); + ::memcpy(expected, candidate.get() + macOffset, sizeof(expected)); + + generateMAC(const_cast(tek), candidate.get()); + + uint8_t different = 0U; + for (uint32_t i = 0U; i < sizeof(expected); ++i) + different |= expected[i] ^ candidate[macOffset + i]; + + return different == 0U; +} + /* Returns a string that represents the current KMM frame. */ std::string KMMFrame::toString() diff --git a/src/common/p25/kmm/KMMFrame.h b/src/common/p25/kmm/KMMFrame.h index 3e8f529f..2543b8ea 100644 --- a/src/common/p25/kmm/KMMFrame.h +++ b/src/common/p25/kmm/KMMFrame.h @@ -39,7 +39,9 @@ namespace p25 * @{ */ - const uint32_t KMM_FRAME_LENGTH = 9U; + // message length counts Format + Dest RSI + Source RSI (7 octets), + // followed by optional MN/body/MAC; ID and Length are excluded + const uint32_t KMM_FRAME_LENGTH = 7U; /** @} */ @@ -111,6 +113,14 @@ namespace p25 * @param[out] data Buffer to encode KMM MAC to. */ void generateMAC(uint8_t* kek, uint8_t* data); + /** + * @brief Verify the MAC code for the given KMM frame. + * @param tek Traffic Encryption Key + * @param[in] data Buffer containing KMM frame data to verify. + * @param len Length of the buffer. + * @returns bool True, if the MAC is valid, otherwise false. + */ + bool verifyMAC(const uint8_t* tek, const uint8_t* data, uint32_t len); /** * @brief Returns a string that represents the current KMM frame. diff --git a/src/common/p25/kmm/KMMModifyKey.h b/src/common/p25/kmm/KMMModifyKey.h index 4a7e568c..9a78f9e0 100644 --- a/src/common/p25/kmm/KMMModifyKey.h +++ b/src/common/p25/kmm/KMMModifyKey.h @@ -37,7 +37,7 @@ namespace p25 * @{ */ - const uint32_t KMM_BODY_MODIFY_KEY_LENGTH = 8U; + const uint32_t KMM_BODY_MODIFY_KEY_LENGTH = 4U; // decryption instruction block /** @} */ diff --git a/src/common/p25/kmm/KMMNegativeAck.cpp b/src/common/p25/kmm/KMMNegativeAck.cpp index f80ad309..c480f5b9 100644 --- a/src/common/p25/kmm/KMMNegativeAck.cpp +++ b/src/common/p25/kmm/KMMNegativeAck.cpp @@ -29,8 +29,8 @@ KMMNegativeAck::KMMNegativeAck() : KMMFrame(), m_messageNo(0U), m_status(KMM_Status::CMD_NOT_PERFORMED) { - m_messageId = KMM_MessageType::NAK; - m_respKind = KMM_ResponseKind::IMMEDIATE; + KMMFrame::m_messageId = KMM_MessageType::NAK; + m_respKind = KMM_ResponseKind::NONE; } /* Finalizes a instance of the KMMNegativeAck class. */ diff --git a/src/common/p25/kmm/KMMRekeyCommand.h b/src/common/p25/kmm/KMMRekeyCommand.h index aee3792d..56bb5b3c 100644 --- a/src/common/p25/kmm/KMMRekeyCommand.h +++ b/src/common/p25/kmm/KMMRekeyCommand.h @@ -37,7 +37,7 @@ namespace p25 * @{ */ - const uint32_t KMM_BODY_REKEY_CMD_LENGTH = 4U; + const uint32_t KMM_BODY_REKEY_CMD_LENGTH = 5U; // DI block + keyset count /** @} */ diff --git a/src/common/p25/kmm/KMMUnableToDecrypt.cpp b/src/common/p25/kmm/KMMUnableToDecrypt.cpp index 9512d2e4..277943a0 100644 --- a/src/common/p25/kmm/KMMUnableToDecrypt.cpp +++ b/src/common/p25/kmm/KMMUnableToDecrypt.cpp @@ -58,7 +58,7 @@ KMMUnableToDecrypt::~KMMUnableToDecrypt() uint32_t KMMUnableToDecrypt::length() const { uint32_t len = KMMFrame::length() + KMM_BODY_UNABLE_TO_DECRYPT_LENGTH; - if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) { + if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) { len += 3U; } @@ -85,7 +85,7 @@ bool KMMUnableToDecrypt::decode(const uint8_t* data) m_status = data[15U + m_bodyOffset]; // Status uint8_t offset = 0U; - if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) { + if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) { m_decryptInfoFmt = data[16U + m_bodyOffset]; // Decrypt Info Format m_decryptAlgId = data[17U + m_bodyOffset]; // Decrypt Algorithm ID m_decryptKId = GET_UINT16(data, 18U + m_bodyOffset); // Decrypt Key ID @@ -134,7 +134,7 @@ void KMMUnableToDecrypt::encode(uint8_t* data) data[15U + m_bodyOffset] = m_status; // Status uint8_t offset = 0U; - if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) { + if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) { data[16U + m_bodyOffset] = m_decryptInfoFmt; // Decrypt Info Format data[17U + m_bodyOffset] = m_decryptAlgId; // Decrypt Algorithm ID SET_UINT16(m_decryptKId, data, 18U + m_bodyOffset); // Decrypt Key ID @@ -146,6 +146,8 @@ void KMMUnableToDecrypt::encode(uint8_t* data) } data[16U + (m_bodyOffset + offset)] = m_key.getLength(); + data[17U + (m_bodyOffset + offset)] = m_algId; + data[18U + (m_bodyOffset + offset)] = m_key.keyFormat(); uint16_t sln = m_key.sln(); SET_UINT16(sln, data, 19U + (m_bodyOffset + offset)); diff --git a/src/common/p25/kmm/KeysetItem.h b/src/common/p25/kmm/KeysetItem.h index 9b22e9e5..148eeb77 100644 --- a/src/common/p25/kmm/KeysetItem.h +++ b/src/common/p25/kmm/KeysetItem.h @@ -15,6 +15,7 @@ #define __P25_KMM__KEYSET_ITEM_H__ #include "common/Defines.h" +#include "common/p25/P25Defines.h" #include "common/Utils.h" #include @@ -51,7 +52,7 @@ namespace p25 * @brief Initializes a new instance of the KeyItem class. */ KeyItem() : - m_keyFormat(0x80U/*P25DEF::KEY_FORMAT_TEK*/), + m_keyFormat(P25DEF::KMM_KEY_FORMAT_TEK), m_sln(0U), m_kId(0U), m_keyLength(0U), @@ -182,7 +183,7 @@ namespace p25 */ uint32_t length() const { - uint32_t len = 4U; + uint32_t len = 5U; // format, keyset ID, ALGID, key length, key count uint32_t keyItemLength = m_keys.size() * 5U; uint32_t combinedKeyLength = m_keys.size() * m_keyLength; diff --git a/src/fne/network/P25OTARService.cpp b/src/fne/network/P25OTARService.cpp index 178028e1..500a09d9 100644 --- a/src/fne/network/P25OTARService.cpp +++ b/src/fne/network/P25OTARService.cpp @@ -46,6 +46,24 @@ using namespace p25::kmm; #define MAX_THREAD_CNT 4U +namespace { +bool allowsUnauthenticatedKMM(uint8_t messageId) +{ + switch (messageId) { + case KMM_MessageType::HELLO: + case KMM_MessageType::NO_SERVICE: + case KMM_MessageType::DEREG_CMD: + case KMM_MessageType::DEREG_RSP: + case KMM_MessageType::REG_CMD: + case KMM_MessageType::REG_RSP: + case KMM_MessageType::UNABLE_TO_DECRYPT: + return true; + default: + return false; + } +} +} + // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -59,6 +77,8 @@ P25OTARService::P25OTARService(TrafficNetwork* network, P25PacketData* packetDat m_network(network), m_packetData(packetData), m_rsiMessageNumber(), + m_rsiInboundMessageNumber(), + m_rsiInboundFingerprint(), m_allowNoUKEKRekey(false), m_debug(debug), m_verbose(verbose) @@ -127,7 +147,7 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId } } - std::unique_ptr frame = KMMFactory::create(kmmPayload.get()); + std::unique_ptr frame = KMMFactory::create(kmmPayload.get(), len); if (frame == nullptr) { LogWarning(LOG_P25, P25_KMM_STR ", undecodable KMM packet"); sendNack(PDUAckType::NACK_ILLEGAL); @@ -247,25 +267,34 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) return; } - if (req->length >= 13) { - // read crypto parameters from KMM header + // AACA-D 12.3: Version-0 DLI preamble is Format(1), MFID(1), + // ALGID(1), KID(2), MI(9) = 14 octets + if (req->length >= 24 && (req->buffer[0U] & 0xFFU) == 0U && req->buffer[1U] == MFG_STANDARD) { uint8_t algoId = req->buffer[2U]; uint16_t kid = GET_UINT16(req->buffer, 3U); uint8_t mi[MI_LENGTH_BYTES]; ::memset(mi, 0x00U, MI_LENGTH_BYTES); for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) { - mi[i] = req->buffer[4U + i]; + mi[i] = req->buffer[5U + i]; } // KMM frame - UInt8Array buffer = std::make_unique(req->length - 13U); - ::memset(buffer.get(), 0x00U, req->length - 13U); - ::memcpy(buffer.get(), req->buffer + 13U, req->length - 13U); + const uint32_t kmmLength = req->length - 14U; + if (kmmLength > 465U) { + LogWarning(LOG_P25, P25_KMM_STR ", invalid DLI KMM length, len = %u", kmmLength); + if (req->buffer != nullptr) delete[] req->buffer; + delete req; + return; + } + + UInt8Array buffer = std::make_unique(kmmLength); + ::memset(buffer.get(), 0x00U, kmmLength); + ::memcpy(buffer.get(), req->buffer + 14U, kmmLength); bool encrypted = (algoId != ALGO_UNENCRYPT); if (encrypted) { - buffer = network->cryptKMM(algoId, kid, mi, buffer.get(), req->length - 13U, false); + buffer = network->cryptKMM(algoId, kid, mi, buffer.get(), kmmLength, false); if (buffer == nullptr) { LogError(LOG_P25, P25_KMM_STR ", unable to decrypt KMM, algoId = $%02X, kID = $%04X", algoId, kid); if (req->buffer != nullptr) @@ -276,7 +305,7 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) } uint32_t payloadSize = 0U; - UInt8Array pduUserData = network->processKMM(buffer.get(), req->length - 13U, 0U, false, &payloadSize); + UInt8Array pduUserData = network->processKMM(buffer.get(), kmmLength, 0U, false, &payloadSize); if (pduUserData == nullptr || payloadSize == 0U) { if (network->m_debug) LogDebug(LOG_P25, P25_KMM_STR ", no KMM response generated for network request"); @@ -299,7 +328,19 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req) } } - network->m_frameQueue->write(pduUserData.get(), payloadSize, req->address, req->addrLen); + UInt8Array datagram = std::make_unique(payloadSize + 14U); + ::memset(datagram.get(), 0x00U, payloadSize + 14U); + + datagram[0U] = 0U; // Version-0 preamble format + datagram[1U] = MFG_STANDARD; + datagram[2U] = encrypted ? algoId : ALGO_UNENCRYPT; + SET_UINT16(encrypted ? kid : 0U, datagram.get(), 3U); + + if (encrypted) + ::memcpy(datagram.get() + 5U, mi, MI_LENGTH_BYTES); + ::memcpy(datagram.get() + 14U, pduUserData.get(), payloadSize); + + network->m_frameQueue->write(datagram.get(), payloadSize + 14U, req->address, req->addrLen); } if (req->buffer != nullptr) @@ -314,6 +355,13 @@ UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, c { 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); @@ -340,12 +388,11 @@ UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, c ::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); uint8_t keyLength = keyItem.getKey(key); - if (m_network->m_debug) { + if (m_network->m_debug) LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength); - Utils::dump(1U, "P25OTARService::cryptKMM(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); - } LogInfoEx(LOG_P25, P25_KMM_STR ", algId = $%02X, kID = $%04X", algoId, kid); + crypto.setTEKAlgoId(algoId); crypto.setKey(key, keyLength); crypto.generateKeystream(); @@ -413,26 +460,95 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ Utils::dump(1U, "P25OTARService::processKMM(), (Decrypted) KMM Network Message", buffer.get(), len); } - std::unique_ptr frame = KMMFactory::create(buffer.get()); + std::unique_ptr frame = KMMFactory::create(buffer.get(), len); if (frame == nullptr) { LogWarning(LOG_P25, P25_KMM_STR ", undecodable KMM packet"); return nullptr; } + + // AACA-D 7.4: validate addressing, MN/MAC coupling, authentication and + // anti-replay state before executing message-specific behavior + if (frame->getDstLLId() != WUID_FNE || frame->getSrcLLId() == 0U || + (llId != 0U && frame->getSrcLLId() != llId)) { + LogWarning(LOG_P25, P25_KMM_STR ", invalid source/destination RSI"); + return nullptr; + } + + if (frame->getMACType() == KMM_MAC::NO_MAC && + (frame->getHasMessageNumber() || !allowsUnauthenticatedKMM(frame->getMessageId()))) { + LogWarning(LOG_P25, P25_KMM_STR ", MAC required for message type/MN, RSI = %u", frame->getSrcLLId()); + return nullptr; + } + + if (frame->getMACType() != KMM_MAC::NO_MAC) { + if (frame->getMACType() != KMM_MAC::ENH_MAC || frame->getMACAlgId() != ALGO_AES_256 || + (frame->getMACFormat() != KMM_MAC_FORMAT_CBC && frame->getMACFormat() != KMM_MAC_FORMAT_CMAC)) { + LogWarning(LOG_P25, P25_KMM_STR ", unsupported MAC type/algorithm, RSI = %u", frame->getSrcLLId()); + return nullptr; + } + + EKCKeyItem macTek = m_network->m_cryptoLookup->find(frame->getMACKId()); + if (macTek.isInvalid() || macTek.algId() != frame->getMACAlgId()) { + LogWarning(LOG_P25, P25_KMM_STR ", MAC TEK not found, RSI = %u", frame->getSrcLLId()); + return nullptr; + } + + uint8_t tek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + ::memset(tek, 0x00U, sizeof(tek)); + macTek.getKey(tek); + + if (!frame->verifyMAC(tek, buffer.get(), len)) { + LogWarning(LOG_P25, P25_KMM_STR ", invalid MAC, RSI = %u", frame->getSrcLLId()); + return nullptr; + } + } + + KMMAuthContext auth; + auth.authenticated = frame->getMACType() == KMM_MAC::ENH_MAC; + auth.hasMessageNumber = frame->getHasMessageNumber(); + auth.messageNumber = frame->getMessageNumber(); + auth.algorithmId = frame->getMACAlgId(); + auth.keyId = frame->getMACKId(); + auth.format = frame->getMACFormat(); + + if (frame->getHasMessageNumber()) { + const uint32_t rsi = frame->getSrcLLId(); + const uint16_t received = frame->getMessageNumber(); + + uint64_t fingerprint = 1469598103934665603ULL; + const uint32_t declared = (((uint32_t)buffer[1U] << 8U) | buffer[2U]) + 3U; + for (uint32_t i = 0U; i < declared; ++i) { + fingerprint ^= buffer[i]; + fingerprint *= 1099511628211ULL; + } + + auto lastIt = m_rsiInboundMessageNumber.find(rsi); + if (lastIt != m_rsiInboundMessageNumber.end()) { + const uint16_t last = lastIt->second; + const uint16_t distance = (uint16_t)(received - last); + const bool identicalRetry = distance == 0U && + frame->getResponseKind() == KMM_ResponseKind::IMMEDIATE && + m_rsiInboundFingerprint.find(rsi) != m_rsiInboundFingerprint.end() && + m_rsiInboundFingerprint[rsi] == fingerprint; + + if (!identicalRetry && (distance == 0U || distance >= 1680U)) { + LogWarning(LOG_P25, P25_KMM_STR ", invalid/replayed message number, RSI = %u, MN = %u", rsi, received); + return nullptr; + } + } + + m_rsiInboundMessageNumber[rsi] = received; + m_rsiInboundFingerprint[rsi] = fingerprint; + } + if (llId == 0U) { llId = frame->getSrcLLId(); } - // does this llId have a RSI message number setup? - if (m_rsiMessageNumber.find(llId) == m_rsiMessageNumber.end()) { + // maintain outbound state independently from the validated inbound MN + if (m_rsiMessageNumber.find(llId) == m_rsiMessageNumber.end()) m_rsiMessageNumber[llId] = 0U; - } else { - // roll message number - uint16_t mn = m_rsiMessageNumber[llId]; - mn++; - - m_rsiMessageNumber[llId] = mn; - } switch (frame->getMessageId()) { case KMM_MessageType::HELLO: @@ -468,37 +584,37 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ // respond with No-Service if KMF services are disabled if (!m_network->m_kmfServicesEnabled) - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); else { if (kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_UKEK || (kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_NO_UKEK && m_allowNoUKEKRekey)) { lookups::RadioId ridEntry = m_network->m_ridLookup->find(kmm->getSrcLLId()); if (ridEntry.radioDefault()) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u has no key policy entry", kmm->toString().c_str(), kmm->getSrcLLId()); - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } if (!ridEntry.radioEnabled()) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u disabled", kmm->toString().c_str(), kmm->getSrcLLId()); - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } if (!ridEntry.canRekey()) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u not rekeyable", kmm->toString().c_str(), kmm->getSrcLLId()); - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } // send rekey-command EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(llId); 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); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } else { - return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize); + return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize, auth); } } else { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey request denied, llId = %u", kmm->toString().c_str(), llId); - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } } } @@ -552,9 +668,9 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ // respond with No-Service if KMF services are disabled if (!m_network->m_kmfServicesEnabled) - return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); else - return write_KMM_Dereg_Response(llId, kmm->getSrcLLId(), payloadSize); + return write_KMM_Dereg_Response(llId, kmm->getSrcLLId(), payloadSize, auth); } break; case KMM_MessageType::REG_RSP: @@ -583,7 +699,8 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ /* 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) +UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, + const KMMAuthContext& auth) { uint8_t mi[MI_LENGTH_BYTES]; ::memset(mi, 0x00U, MI_LENGTH_BYTES); @@ -617,10 +734,8 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS kekAlgId = keyItem.algId(); kekKId = keyItem.kId(); - if (m_network->m_debug) { + if (m_network->m_debug) LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength); - Utils::dump(1U, "P25OTARService::cryptKMM(), Key", kekKey, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); - } } else { if (!m_allowNoUKEKRekey) { @@ -638,10 +753,8 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS outKmm.setDstLLId(kmmRSI); outKmm.setMACType(KMM_MAC::ENH_MAC); - outKmm.setMACAlgId(kekAlgId); - outKmm.setMACKId(kekKId); - outKmm.setMACFormat(KMM_MAC_FORMAT_CBC); - + outKmm.setMACFormat(auth.authenticated ? auth.format : KMM_MAC_FORMAT_CBC); + outKmm.setHasMessageNumber(true); outKmm.setMessageNumber(mn); outKmm.setAlgId(kekAlgId); @@ -656,6 +769,40 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS allowedKIds = ridEntry.allowedKIds(); + // 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 (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", + outKmm.toString().c_str(), kmmRSI); + return nullptr; + } + } else { + for (const EKCKeyItem& candidate : m_network->m_cryptoLookup->keys()) { + if (candidate.algId() == ALGO_AES_256 && + (allowedKIds.empty() || std::find(allowedKIds.begin(), allowedKIds.end(), + (uint16_t)candidate.kId()) != allowedKIds.end())) { + macTekItem = candidate; + break; + } + } + } + + if (macTekItem.isInvalid()) { + LogError(LOG_P25, P25_KMM_STR ", %s, no common AES TEK available for MAC, RSI = %u", + outKmm.toString().c_str(), kmmRSI); + return nullptr; + } + + uint8_t macTek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + ::memset(macTek, 0x00U, sizeof(macTek)); + macTekItem.getKey(macTek); + outKmm.setMACAlgId(macTekItem.algId()); + outKmm.setMACKId((uint16_t)macTekItem.kId()); + KeysetItem ks; ks.keysetId(1U); ks.algId(ALGO_AES_256); // we currently can only OTAR AES256 keys @@ -695,7 +842,7 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS } p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KEY_FORMAT_TEK); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.kId((uint16_t)keyItem.kId()); ki.sln((uint16_t)keyItem.sln()); ki.setKey(wrappedKey.get(), keyLength); @@ -714,6 +861,12 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS outKmm.setKeysets(keysets); + if (outKmm.fullLength() > 512U) { + LogError(LOG_P25, P25_KMM_STR ", %s, rekey exceeds AACA-D DLD maximum, len = %u, llId = %u, RSI = %u", + outKmm.toString().c_str(), outKmm.fullLength(), outKmm.getSrcLLId(), outKmm.getDstLLId()); + return nullptr; + } + if (m_verbose) { LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, RSI = %u, keyCount = %u", outKmm.toString().c_str(), outKmm.getSrcLLId(), outKmm.getDstLLId(), ks.keys().size()); @@ -725,7 +878,9 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS UInt8Array kmmFrame = std::make_unique(frameLength); outKmm.encode(kmmFrame.get()); - outKmm.generateMAC(kekKey, kmmFrame.get()); + outKmm.generateMAC(macTek, kmmFrame.get()); + + m_rsiMessageNumber[llId] = (uint16_t)(mn + 1U); return kmmFrame; } @@ -753,7 +908,8 @@ UInt8Array P25OTARService::write_KMM_Reg_Command(uint32_t llId, uint32_t kmmRSI, /* Helper used to return a Deregistration-Response KMM to the calling SU. */ -UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize) +UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize, + const KMMAuthContext& auth) { KMMDeregistrationResponse outKmm = KMMDeregistrationResponse(); outKmm.setSrcLLId(WUID_FNE); @@ -765,18 +921,13 @@ UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmR outKmm.getSrcLLId(), outKmm.getDstLLId()); } - const uint32_t frameLength = outKmm.fullLength(); - if (payloadSize != nullptr) - *payloadSize = frameLength; - - UInt8Array kmmFrame = std::make_unique(frameLength); - outKmm.encode(kmmFrame.get()); - return kmmFrame; + return encode_KMM_Response(outKmm, payloadSize, auth); } /* Helper used to return a No-Service KMM to the calling SU. */ -UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize) +UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize, + const KMMAuthContext& auth) { KMMNoService outKmm = KMMNoService(); outKmm.setSrcLLId(WUID_FNE); @@ -787,13 +938,43 @@ UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, u outKmm.getSrcLLId(), outKmm.getDstLLId()); } - const uint32_t frameLength = outKmm.fullLength(); + return encode_KMM_Response(outKmm, payloadSize, auth); +} + +/* Encodes a response using the authenticated request's MAC format and MN. */ + +UInt8Array P25OTARService::encode_KMM_Response(KMMFrame& frame, uint32_t* payloadSize, + const KMMAuthContext& auth) +{ + uint8_t macTek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES]; + ::memset(macTek, 0x00U, sizeof(macTek)); + + if (auth.authenticated) { + EKCKeyItem key = m_network->m_cryptoLookup->find(auth.keyId); + if (key.isInvalid() || key.algId() != auth.algorithmId) + return nullptr; + + key.getKey(macTek); + frame.setMACType(KMM_MAC::ENH_MAC); + frame.setMACAlgId(auth.algorithmId); + frame.setMACKId(auth.keyId); + frame.setMACFormat(auth.format); + if (auth.hasMessageNumber) { + frame.setHasMessageNumber(true); + frame.setMessageNumber(auth.messageNumber); + } + } + + const uint32_t frameLength = frame.fullLength(); if (payloadSize != nullptr) *payloadSize = frameLength; - - UInt8Array kmmFrame = std::make_unique(frameLength); - outKmm.encode(kmmFrame.get()); - return kmmFrame; + + UInt8Array encoded = std::make_unique(frameLength); + ::memset(encoded.get(), 0x00U, frameLength); + frame.encode(encoded.get()); + if (auth.authenticated) + frame.generateMAC(macTek, encoded.get()); + return encoded; } /* Helper used to return a Zeroize KMM to the calling SU. */ diff --git a/src/fne/network/P25OTARService.h b/src/fne/network/P25OTARService.h index 439b6b15..1614559f 100644 --- a/src/fne/network/P25OTARService.h +++ b/src/fne/network/P25OTARService.h @@ -24,6 +24,7 @@ class FNETestHooks; #include "common/concurrent/unordered_map.h" #include "common/p25/P25Defines.h" #include "common/p25/Crypto.h" +#include "common/p25/kmm/KMMFrame.h" #include "common/network/udp/Socket.h" #include "common/network/RawFrameQueue.h" #include "network/TrafficNetwork.h" @@ -105,6 +106,18 @@ namespace network void close(); private: + /** + * @brief Context for KMM message authentication. + */ + struct KMMAuthContext { + bool authenticated = false; //!< Indicates if the KMM message is authenticated. + bool hasMessageNumber = false; //!< Indicates if the KMM message has a message number. + uint16_t messageNumber = 0U; //!< Message number of the KMM message. + uint8_t algorithmId = P25DEF::ALGO_UNENCRYPT; //!< Algorithm ID used for the KMM message. + uint16_t keyId = 0U; //!< Key ID used for the KMM message. + uint16_t format = P25DEF::KMM_MAC_FORMAT_CBC; //!< MAC format used for the KMM message. + }; + network::udp::Socket* m_socket; network::RawFrameQueue* m_frameQueue; @@ -114,6 +127,8 @@ namespace network network::callhandler::packetdata::P25PacketData* m_packetData; concurrent::unordered_map m_rsiMessageNumber; + concurrent::unordered_map m_rsiInboundMessageNumber; + concurrent::unordered_map m_rsiInboundFingerprint; bool m_allowNoUKEKRekey; @@ -156,9 +171,11 @@ namespace network * @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. * @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); + UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, + const KMMAuthContext& auth); /** * @brief Helper used to return a Registration-Command KMM to the calling SU. @@ -174,18 +191,31 @@ namespace network * @param llId Logical Link Address. * @param kmmRSI KMM Radio Set Identifier. * @param[out] payloadSize Size of the returned KMM payload. + * @param auth Authentication context containing MAC/MN fields to mirror. * @returns UInt8Array Buffer containing the processed KMM frame (if any). */ - UInt8Array write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize); + UInt8Array write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize, + const KMMAuthContext& auth); /** * @brief Helper used to return a No-Service KMM to the calling SU. * @param llId Logical Link Address. * @param kmmRSI KMM Radio Set Identifier. * @param[out] payloadSize Size of the returned KMM payload. + * @param auth Authentication context containing MAC/MN fields to mirror. * @returns UInt8Array Buffer containing the processed KMM frame (if any). */ - UInt8Array write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize); + UInt8Array write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize, + const KMMAuthContext& auth); + + /** + * @brief Encodes a response and mirrors an authenticated request's MAC/MN fields. + * @param frame KMM frame to encode. + * @param[out] payloadSize Size of the returned KMM payload. + * @param auth Authentication context containing MAC/MN fields to mirror. + */ + UInt8Array encode_KMM_Response(p25::kmm::KMMFrame& frame, uint32_t* payloadSize, + const KMMAuthContext& auth); /** * @brief Helper used to return a Zeroize KMM to the calling SU. diff --git a/src/fne/network/TrafficNetwork.cpp b/src/fne/network/TrafficNetwork.cpp index 65b81673..4127c4b2 100644 --- a/src/fne/network/TrafficNetwork.cpp +++ b/src/fne/network/TrafficNetwork.cpp @@ -3009,7 +3009,7 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId, ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KEY_FORMAT_TEK); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.kId(rspKi->kId()); ki.sln(rspKi->sln()); ki.setKey(key, keyLength); @@ -3079,7 +3079,7 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KEY_FORMAT_TEK); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.kId(rspKi->kId()); ki.sln(rspKi->sln()); ki.setKey(key, keyLength); diff --git a/src/fne/network/packet/KeyRequest.cpp b/src/fne/network/packet/KeyRequest.cpp index 2c2629a5..62ee1155 100644 --- a/src/fne/network/packet/KeyRequest.cpp +++ b/src/fne/network/packet/KeyRequest.cpp @@ -149,7 +149,7 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KEY_FORMAT_TEK); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.kId((uint16_t)keyItem.kId()); ki.sln((uint16_t)keyItem.sln()); ki.setKey(key, keyLength); diff --git a/src/fne/network/packet/LLAKeyRequest.cpp b/src/fne/network/packet/LLAKeyRequest.cpp index 99c72f28..3cdd63bb 100644 --- a/src/fne/network/packet/LLAKeyRequest.cpp +++ b/src/fne/network/packet/LLAKeyRequest.cpp @@ -120,7 +120,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa ks.keyLength(keyLength); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(KEY_FORMAT_TEK); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.kId((uint16_t)keyItem.kId()); ki.sln((uint16_t)keyItem.sln()); ki.setKey(key, keyLength); diff --git a/tests/fne/FNETestHooks.cpp b/tests/fne/FNETestHooks.cpp index f546f70d..690e9e11 100644 --- a/tests/fne/FNETestHooks.cpp +++ b/tests/fne/FNETestHooks.cpp @@ -70,6 +70,14 @@ FNEPeerConnection& FNETestHooks::addPeer(TrafficNetwork& network, uint32_t peerI return *connection; } +/** + * @brief Passes an encoded KMM through the FNE OTAR message dispatcher. + * @param network The TrafficNetwork instance. + * @param packet The KMM packet data. + * @param llId The logical link ID. + * @param payloadSize The size of the processed payload. + * @return A unique pointer to the processed KMM frame, or nullptr if processing failed. + */ std::unique_ptr FNETestHooks::processOTARKMM(TrafficNetwork& network, const std::vector& packet, uint32_t llId, uint32_t& payloadSize) { @@ -81,11 +89,86 @@ std::unique_ptr FNETestHooks::processOTARKMM(TrafficNetwork& network, false, &payloadSize); } +/** + * @brief Passes a DLD KMM through the public P25 OTAR bearer entry point. + * @param network The TrafficNetwork instance. + * @param packet The DLD packet data. + * @param llId The logical link ID. + * @param n The sequence number. + * @param encrypted Whether the packet is encrypted. + * @param algoId The algorithm ID used for encryption. + * @param kid The key ID used for encryption. + * @param mi The message integrity value. + * @return True if the packet was successfully processed, false otherwise. + */ +bool FNETestHooks::processOTARDLD(TrafficNetwork& network, const std::vector& packet, + uint32_t llId, uint8_t n, bool encrypted, uint8_t algoId, uint16_t kid, const uint8_t* mi) +{ + if (packet.empty() || network.m_p25OTARService == nullptr) + return false; + + return network.m_p25OTARService->processDLD(packet.data(), (uint32_t)packet.size(), llId, n, + encrypted, algoId, kid, mi); +} + +/** + * @brief Passes a complete Version-0 DLI datagram through the network receive task. + * @param network The TrafficNetwork instance. + * @param datagram The DLI packet data. + * @return True if the packet was successfully processed, false otherwise. + */ +void FNETestHooks::processOTARDLI(TrafficNetwork& network, const std::vector& datagram) +{ + if (datagram.empty() || network.m_p25OTARService == nullptr) + return; + + OTARPacketRequest* req = new OTARPacketRequest(); + req->obj = network.m_p25OTARService; + req->address = {}; + req->addrLen = 0U; + req->length = (int)datagram.size(); + req->buffer = new uint8_t[datagram.size()]; + ::memcpy(req->buffer, datagram.data(), datagram.size()); + + P25OTARService::taskNetworkRx(req); +} + +/** + * @brief Checks if the specified inbound message number exists for the given RSI. + * @param network The TrafficNetwork instance. + * @param rsi The RSI to check. + * @param mn The message number to check. + * @return True if the inbound message number exists and matches, false otherwise. + */ +bool FNETestHooks::hasOTARInboundMessageNumber(TrafficNetwork& network, uint32_t rsi, uint16_t mn) +{ + if (network.m_p25OTARService == nullptr) + return false; + + auto it = network.m_p25OTARService->m_rsiInboundMessageNumber.find(rsi); + return it != network.m_p25OTARService->m_rsiInboundMessageNumber.end() && it->second == mn; +} + +/** + * @brief Enables or disables KMF services for the specified TrafficNetwork. + * @param network The TrafficNetwork instance. + * @param enabled True to enable KMF services, false to disable. + */ void FNETestHooks::setKMFServicesEnabled(TrafficNetwork& network, bool enabled) { network.m_kmfServicesEnabled = enabled; } +/** + * @brief Adds a cryptographic key to the network's crypto lookup. + * @param network The TrafficNetwork instance. + * @param key The cryptographic key to add. + */ +void FNETestHooks::addCryptoKey(TrafficNetwork& network, const EKCKeyItem& key) +{ + network.m_cryptoLookup->addEntry(key); +} + // --------------------------------------------------------------------------- // Static Class Members // --------------------------------------------------------------------------- diff --git a/tests/fne/FNETestHooks.h b/tests/fne/FNETestHooks.h index 3821627e..c70d8bd7 100644 --- a/tests/fne/FNETestHooks.h +++ b/tests/fne/FNETestHooks.h @@ -95,8 +95,47 @@ public: */ static std::unique_ptr processOTARKMM(network::TrafficNetwork& network, const std::vector& packet, uint32_t llId, uint32_t& payloadSize); - /** @brief Enables or disables KMF services for a test TrafficNetwork. */ + /** + * @brief Passes a DLD KMM through the public P25 OTAR bearer entry point. + * @param network The TrafficNetwork that owns the OTAR service. + * @param packet Encoded DLD bytes. + * @param llId Logical Link ID associated with the message. + * @param n Sequence number of the DLD packet. + * @param encrypted Whether the packet is encrypted. + * @param algoId The algorithm ID used for encryption. + * @param kid The key ID used for encryption. + * @param mi The message integrity value. + * @return True if the packet was successfully processed, false otherwise. + */ + static bool processOTARDLD(network::TrafficNetwork& network, const std::vector& packet, + uint32_t llId, uint8_t n, bool encrypted = false, uint8_t algoId = P25DEF::ALGO_UNENCRYPT, + uint16_t kid = 0U, const uint8_t* mi = nullptr); + /** + * @brief Passes a complete Version-0 DLI datagram through the network receive task. + * @param network The TrafficNetwork that owns the OTAR service. + * @param datagram Encoded DLI bytes. + */ + static void processOTARDLI(network::TrafficNetwork& network, const std::vector& datagram); + /** + * @brief Checks if the specified inbound message number exists for the given RSI. + * @param network The TrafficNetwork that owns the OTAR service. + * @param rsi The RSI to check. + * @param mn The message number to check. + * @return True if the inbound message number exists and matches, false otherwise. + */ + static bool hasOTARInboundMessageNumber(network::TrafficNetwork& network, uint32_t rsi, uint16_t mn); + /** + * @brief Enables or disables KMF services for the specified TrafficNetwork. + * @param network The TrafficNetwork that owns the KMF services. + * @param enabled True to enable KMF services, false to disable. + */ static void setKMFServicesEnabled(network::TrafficNetwork& network, bool enabled); + /** + * @brief Adds a cryptographic key to the network's crypto lookup. + * @param network The TrafficNetwork instance. + * @param key The cryptographic key to add. + */ + static void addCryptoKey(network::TrafficNetwork& network, const EKCKeyItem& key); }; #endif // __FNE_TEST_HOOKS_H__ diff --git a/tests/p25/KMM_Rekey_CBC_Test.cpp b/tests/p25/KMM_Rekey_CBC_Test.cpp index 6f572dee..3c9d7114 100644 --- a/tests/p25/KMM_Rekey_CBC_Test.cpp +++ b/tests/p25/KMM_Rekey_CBC_Test.cpp @@ -97,7 +97,7 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") { ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(0U); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.sln(0U); ki.kId(0x4983U); @@ -113,6 +113,11 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") { outKmm.encode(kmmFrame.get()); outKmm.generateMAC(macTek, kmmFrame.get()); + REQUIRE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength())); + kmmFrame[20U] ^= 0x01U; + REQUIRE_FALSE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength())); + kmmFrame[20U] ^= 0x01U; + Utils::dump(2U, "P25_KMM_ReKey_CBC_Test, GeneratedDataBlock", kmmFrame.get(), outKmm.fullLength()); for (uint32_t i = 0; i < outKmm.fullLength(); i++) { @@ -141,3 +146,41 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") { REQUIRE(failed==false); } + +TEST_CASE("KMM Key Format follows AACA-D Table 70", "[p25][kmm][key-format]") +{ + REQUIRE(KMM_KEY_FORMAT_TEK == 0x00U); + REQUIRE(KMM_KEY_FORMAT_KEK == 0x80U); + REQUIRE(KMM_KEY_FORMAT_DELETE == 0x20U); + REQUIRE(KMM_BODY_FORMAT_TEK_INCLUDED == 0x80U); + REQUIRE(KMM_BODY_FORMAT_KEK_MISSING == 0x40U); + + auto encodedKeyFormat = [](uint8_t format) { + KMMRekeyCommand command; + command.setSrcLLId(0x010203U); + command.setDstLLId(0x040506U); + command.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE); + + KeysetItem keyset; + keyset.keysetId(1U); + keyset.algId(ALGO_AES_256); + keyset.keyLength(1U); + + KeyItem key; + const uint8_t material = 0x5AU; + key.keyFormat(format); + key.sln(1U); + key.kId(2U); + key.setKey(&material, 1U); + keyset.push_back(key); + command.setKeysets({ keyset }); + + std::vector encoded(command.fullLength(), 0U); + command.encode(encoded.data()); + return encoded[20U]; + }; + + REQUIRE(encodedKeyFormat(KMM_KEY_FORMAT_TEK) == 0x00U); + REQUIRE(encodedKeyFormat(KMM_KEY_FORMAT_KEK) == 0x80U); + REQUIRE(encodedKeyFormat(KMM_KEY_FORMAT_TEK | KMM_KEY_FORMAT_DELETE) == 0x20U); +} diff --git a/tests/p25/KMM_Rekey_CMAC_Test.cpp b/tests/p25/KMM_Rekey_CMAC_Test.cpp index a774680b..aa30f89b 100644 --- a/tests/p25/KMM_Rekey_CMAC_Test.cpp +++ b/tests/p25/KMM_Rekey_CMAC_Test.cpp @@ -11,6 +11,7 @@ #include "common/p25/P25Defines.h" #include "common/p25/Crypto.h" #include "common/p25/kmm/KMMRekeyCommand.h" +#include "common/p25/kmm/KMMHello.h" #include "common/Log.h" #include "common/Utils.h" @@ -79,7 +80,7 @@ TEST_CASE("KMM ReKey Command CMAC Test", "[p25][kmm_cmac][cap]") { ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(0U); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.sln(0U); ki.kId(0x4983U); @@ -95,6 +96,11 @@ TEST_CASE("KMM ReKey Command CMAC Test", "[p25][kmm_cmac][cap]") { outKmm.encode(kmmFrame.get()); outKmm.generateMAC(macTek, kmmFrame.get()); + REQUIRE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength())); + kmmFrame[20U] ^= 0x01U; + REQUIRE_FALSE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength())); + kmmFrame[20U] ^= 0x01U; + Utils::dump(2U, "P25_KMM_ReKey_CMAC_Test, GeneratedDataBlock", kmmFrame.get(), outKmm.fullLength()); for (uint32_t i = 0; i < outKmm.fullLength(); i++) { @@ -144,9 +150,25 @@ TEST_CASE("KMM Hello CMAC Test (No Message Number)", "[p25][kmm_cmac][cap]") { } REQUIRE(failed==false); + + KMMHello hello; + hello.setDstLLId(0x643BA8U); + hello.setSrcLLId(0x712B1DU); + hello.setResponseKind(KMM_ResponseKind::NONE); + hello.setMACType(KMM_MAC::ENH_MAC); + hello.setMACAlgId(ALGO_AES_256); + hello.setMACKId(0x2F62U); + hello.setMACFormat(KMM_MAC_FORMAT_CMAC); + hello.setFlag(KMM_HelloFlag::IDENT_ONLY); + REQUIRE(hello.fullLength() == sizeof(expectedFrame)); + UInt8Array encoded = std::make_unique(hello.fullLength()); + ::memset(encoded.get(), 0x00U, hello.fullLength()); + hello.encode(encoded.get()); + hello.generateMAC(macTek, encoded.get()); + REQUIRE(::memcmp(encoded.get(), expectedFrame, sizeof(expectedFrame)) == 0); } -TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac][cap]") { +TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cbc][cap]") { // Verify explicit MN presence works even when the message number value is zero. KMMRekeyCommand outKmm = KMMRekeyCommand(); @@ -157,7 +179,7 @@ TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac] outKmm.setMACType(KMM_MAC::ENH_MAC); outKmm.setMACAlgId(ALGO_AES_256); outKmm.setMACKId(0x2F62U); - outKmm.setMACFormat(KMM_MAC_FORMAT_CMAC); + outKmm.setMACFormat(KMM_MAC_FORMAT_CBC); outKmm.setHasMessageNumber(true); outKmm.setMessageNumber(0x0000U); @@ -171,7 +193,7 @@ TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac] ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES); p25::kmm::KeyItem ki = p25::kmm::KeyItem(); - ki.keyFormat(0U); + ki.keyFormat(KMM_KEY_FORMAT_TEK); ki.sln(0U); ki.kId(0x4983U); diff --git a/tests/p25/P25_Crypto_Tests.cpp b/tests/p25/P25_Crypto_Tests.cpp new file mode 100644 index 00000000..ba6054b2 --- /dev/null +++ b/tests/p25/P25_Crypto_Tests.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * 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 "host/Defines.h" +#include "common/p25/P25Defines.h" +#include "common/p25/Crypto.h" + +#include + +#include +#include +#include + +using namespace p25::crypto; +using namespace p25::defines; + +TEST_CASE("AES PDU OFB covers messages larger than 255 octets", "[p25][crypto][otar]") +{ + uint8_t key[32U]; + uint8_t mi[MI_LENGTH_BYTES]; + for (uint32_t i = 0U; i < sizeof(key); ++i) key[i] = (uint8_t)i; + for (uint32_t i = 0U; i < sizeof(mi); ++i) mi[i] = (uint8_t)(0xA0U + i); + + std::vector original(512U), encrypted(512U); + for (uint32_t i = 0U; i < original.size(); ++i) original[i] = (uint8_t)i; + encrypted = original; + + P25Crypto enc; + enc.setMI(mi); enc.setTEKAlgoId(ALGO_AES_256); enc.setKey(key, sizeof(key)); enc.generateKeystream(); + enc.cryptAES_PDU(encrypted.data(), (uint32_t)encrypted.size()); + REQUIRE(encrypted[300U] != original[300U]); + + P25Crypto dec; + dec.setMI(mi); dec.setTEKAlgoId(ALGO_AES_256); dec.setKey(key, sizeof(key)); dec.generateKeystream(); + dec.cryptAES_PDU(encrypted.data(), (uint32_t)encrypted.size()); + REQUIRE(encrypted == original); +} + +TEST_CASE("AES voice encryption retains the AAAD-B IMBE keystream mapping", "[p25][crypto][voice]") +{ + // TIA-102.AAAD-B Annex C.4 AES-256 key and MI. The expected first + // LDU1 IMBE mask is octets 12-22 of the usable FDMA keystream after + // discarding the first OFB iteration (AAAD-B Table 5-5). + const uint8_t key[32U] = { + 0x01U, 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, + 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U, + 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U, 0x23U, + 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U, 0x23U, 0x45U + }; + const uint8_t mi[MI_LENGTH_BYTES] = { + 0x12U, 0x34U, 0x56U, 0x78U, 0x90U, 0xABU, 0xCDU, 0xEFU, 0x00U + }; + const uint8_t expected[RAW_IMBE_LENGTH_BYTES] = { + 0xF5U, 0x71U, 0xCBU, 0x02U, 0x9AU, 0x93U, + 0xAEU, 0xFFU, 0x9BU, 0xDBU, 0x7BU + }; + + uint8_t imbe[RAW_IMBE_LENGTH_BYTES] = { 0U }; + P25Crypto vectorCrypto; + vectorCrypto.setMI(mi); + vectorCrypto.setTEKAlgoId(ALGO_AES_256); + vectorCrypto.setKey(key, sizeof(key)); + vectorCrypto.generateKeystream(); + vectorCrypto.cryptAES_IMBE(imbe, DUID::LDU1); + REQUIRE(::memcmp(imbe, expected, sizeof(expected)) == 0); + + for (DUID::E duid : { DUID::LDU1, DUID::LDU2 }) { + std::array, 9U> plain{}; + for (size_t frame = 0U; frame < plain.size(); ++frame) + for (size_t octet = 0U; octet < plain[frame].size(); ++octet) + plain[frame][octet] = (uint8_t)(frame * plain[frame].size() + octet); + auto encrypted = plain; + + P25Crypto tx; + tx.setMI(mi); tx.setTEKAlgoId(ALGO_AES_256); tx.setKey(key, sizeof(key)); tx.generateKeystream(); + for (auto& frame : encrypted) + tx.cryptAES_IMBE(frame.data(), duid); + REQUIRE(encrypted != plain); + + P25Crypto rx; + rx.setMI(mi); rx.setTEKAlgoId(ALGO_AES_256); rx.setKey(key, sizeof(key)); rx.generateKeystream(); + for (auto& frame : encrypted) + rx.cryptAES_IMBE(frame.data(), duid); + REQUIRE(encrypted == plain); + } +} diff --git a/tests/p25/P25_KMM_Tests.cpp b/tests/p25/P25_KMM_Tests.cpp index 7f63409b..9db7780c 100644 --- a/tests/p25/P25_KMM_Tests.cpp +++ b/tests/p25/P25_KMM_Tests.cpp @@ -26,33 +26,45 @@ using namespace p25; using namespace p25::defines; using namespace p25::kmm; -namespace { - +/** + * @brief Test harness for KMM-related FNE functionality. + */ class KMMFNEHarness { public: + /** + * @brief Initializes a new instance of the KMMFNEHarness class. + */ KMMFNEHarness() : host("fne-kmm-test.yml"), + crypto("", "", "", false, 0U, true), traffic(&host, "127.0.0.1", 62031U, 999999U, "test-password", "test-fne", false, false, false, false, true, true, true, true, true, 0U, false, true, true, 5U, 10U, 2U) { - /* stub */ + traffic.setLookups(nullptr, nullptr, nullptr, nullptr, &crypto, nullptr); } HostFNE host; + CryptoContainer crypto; network::TrafficNetwork traffic; }; -template -std::vector encodeKMM(T& frame) -{ - std::vector bytes(frame.fullLength(), 0U); - frame.encode(bytes.data()); - return bytes; +namespace { + /** + * @brief Encodes a KMM frame into a byte vector. + * @tparam T The type of the KMM frame. + * @param frame The KMM frame to encode. + * @return A byte vector containing the encoded KMM frame. + */ + template + std::vector encodeKMM(T& frame) + { + std::vector bytes(frame.fullLength(), 0U); + frame.encode(bytes.data()); + return bytes; + } } -} // namespace - TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm][factory]") { SECTION("HELLO frame round-trips with message number") { @@ -107,11 +119,29 @@ TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm] } } +TEST_CASE("KMM air-facing factory rejects malformed lengths", "[p25][kmm][security]") +{ + KMMHello hello; + hello.setDstLLId(WUID_FNE); + hello.setSrcLLId(0x654321U); + std::vector bytes = encodeKMM(hello); + + REQUIRE(KMMFactory::create(bytes.data(), (uint32_t)bytes.size()) != nullptr); + REQUIRE(KMMFactory::create(bytes.data(), 9U) == nullptr); + + std::vector truncated(bytes.begin(), bytes.end() - 1U); + REQUIRE(KMMFactory::create(truncated.data(), (uint32_t)truncated.size()) == nullptr); + + bytes[1U] = 0x02U; + bytes[2U] = 0x00U; + REQUIRE(KMMFactory::create(bytes.data(), (uint32_t)bytes.size()) == nullptr); +} + TEST_CASE("FNE OTAR dispatcher follows AACA response-kind procedures", "[p25][kmm][otar]") { KMMFNEHarness harness; constexpr uint32_t SU_RSI = 0x654321U; - constexpr uint32_t KMF_RSI = 0x123456U; + constexpr uint32_t KMF_RSI = WUID_FNE; SECTION("Response Kind 1 Hello produces no OTAR response") { KMMHello request; @@ -186,3 +216,154 @@ TEST_CASE("FNE OTAR dispatcher follows AACA response-kind procedures", "[p25][km REQUIRE(decoded->getSrcLLId() == WUID_FNE); } } + +TEST_CASE("FNE OTAR authenticates and replay-checks KMM before dispatch", "[p25][kmm][otar][security]") +{ + KMMFNEHarness harness; + constexpr uint32_t SU_RSI = 0x654321U; + uint8_t tek[32U]; + for (uint32_t i = 0U; i < sizeof(tek); ++i) tek[i] = (uint8_t)i; + + EKCKeyItem item; + item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U); + item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"); + FNETestHooks::addCryptoKey(harness.traffic, item); + + auto makeSigned = [&](uint16_t mn, uint8_t flag, uint16_t macFormat = KMM_MAC_FORMAT_CBC) { + KMMHello hello; + hello.setDstLLId(WUID_FNE); hello.setSrcLLId(SU_RSI); + hello.setResponseKind(KMM_ResponseKind::IMMEDIATE); + hello.setHasMessageNumber(true); hello.setMessageNumber(mn); hello.setFlag(flag); + hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256); + hello.setMACKId(0x1234U); hello.setMACFormat(macFormat); + std::vector bytes = encodeKMM(hello); + hello.generateMAC(tek, bytes.data()); + return bytes; + }; + + uint32_t size = 0U; + std::vector valid = makeSigned(100U, KMM_HelloFlag::REKEY_REQUEST_UKEK); + UInt8Array cbcResponse = FNETestHooks::processOTARKMM(harness.traffic, valid, SU_RSI, size); + REQUIRE(cbcResponse != nullptr); + std::unique_ptr decodedCBC = KMMFactory::create(cbcResponse.get(), size); + REQUIRE(decodedCBC != nullptr); + REQUIRE(decodedCBC->getMACFormat() == KMM_MAC_FORMAT_CBC); + REQUIRE(decodedCBC->getMessageNumber() == 100U); + REQUIRE(decodedCBC->verifyMAC(tek, cbcResponse.get(), size)); + REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, valid, SU_RSI, size) != nullptr); // identical RK3 retry + + std::vector changedSameMN = makeSigned(100U, KMM_HelloFlag::IDENT_ONLY); + REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, changedSameMN, SU_RSI, size) == nullptr); + + std::vector badMac = makeSigned(101U, KMM_HelloFlag::REKEY_REQUEST_UKEK); + badMac[10U] ^= 0x01U; + REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, badMac, SU_RSI, size) == nullptr); + + std::vector cmac = makeSigned(101U, KMM_HelloFlag::REKEY_REQUEST_UKEK, KMM_MAC_FORMAT_CMAC); + UInt8Array cmacResponse = FNETestHooks::processOTARKMM(harness.traffic, cmac, SU_RSI, size); + REQUIRE(cmacResponse != nullptr); + std::unique_ptr decodedCMAC = KMMFactory::create(cmacResponse.get(), size); + REQUIRE(decodedCMAC != nullptr); + REQUIRE(decodedCMAC->getMACFormat() == KMM_MAC_FORMAT_CMAC); + REQUIRE(decodedCMAC->getMessageNumber() == 101U); + REQUIRE(decodedCMAC->verifyMAC(tek, cmacResponse.get(), size)); + + std::vector next = makeSigned(102U, KMM_HelloFlag::REKEY_REQUEST_UKEK); + REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, next, SU_RSI, size) != nullptr); +} + +TEST_CASE("P25 OTAR DLD processes KMM through the packet-data service entry point", "[p25][kmm][otar][dld]") +{ + KMMFNEHarness harness; + constexpr uint32_t SU_RSI = 0x654321U; + constexpr uint16_t MN = 0x1234U; + uint8_t tek[32U]; + for (uint32_t i = 0U; i < sizeof(tek); ++i) + tek[i] = (uint8_t)i; + + EKCKeyItem item; + item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U); + item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"); + FNETestHooks::addCryptoKey(harness.traffic, item); + + KMMHello hello; + hello.setDstLLId(WUID_FNE); + hello.setSrcLLId(SU_RSI); + hello.setResponseKind(KMM_ResponseKind::NONE); + hello.setHasMessageNumber(true); hello.setMessageNumber(MN); + hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256); + hello.setMACKId(0x1234U); hello.setMACFormat(KMM_MAC_FORMAT_CBC); + hello.setFlag(KMM_HelloFlag::IDENT_ONLY); + std::vector encoded = encodeKMM(hello); + hello.generateMAC(tek, encoded.data()); + + REQUIRE(FNETestHooks::processOTARDLD(harness.traffic, encoded, SU_RSI, 7U)); + REQUIRE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN)); + + std::vector truncated(encoded.begin(), encoded.end() - 1U); + REQUIRE_FALSE(FNETestHooks::processOTARDLD(harness.traffic, truncated, SU_RSI, 8U)); + + KMMHello wrongSource = hello; + wrongSource.setSrcLLId(SU_RSI + 1U); + wrongSource.setMessageNumber(MN + 1U); + std::vector wrongRSI = encodeKMM(wrongSource); + wrongSource.generateMAC(tek, wrongRSI.data()); + // DLD itself is handled, while the mismatched KMM source is discarded. + REQUIRE(FNETestHooks::processOTARDLD(harness.traffic, wrongRSI, SU_RSI, 9U)); + REQUIRE_FALSE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI + 1U, MN + 1U)); +} + +TEST_CASE("P25 OTAR DLI validates its Version-0 preamble and dispatches KMM", "[p25][kmm][otar][dli]") +{ + constexpr uint32_t SU_RSI = 0x654321U; + constexpr uint16_t MN = 0x2345U; + uint8_t tek[32U]; + for (uint32_t i = 0U; i < sizeof(tek); ++i) + tek[i] = (uint8_t)i; + + auto makeHarnessKey = [](KMMFNEHarness& harness) { + EKCKeyItem item; + item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U); + item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"); + FNETestHooks::addCryptoKey(harness.traffic, item); + }; + + KMMHello hello; + hello.setDstLLId(WUID_FNE); hello.setSrcLLId(SU_RSI); + hello.setResponseKind(KMM_ResponseKind::NONE); + hello.setHasMessageNumber(true); hello.setMessageNumber(MN); + hello.setFlag(KMM_HelloFlag::IDENT_ONLY); + hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256); + hello.setMACKId(0x1234U); hello.setMACFormat(KMM_MAC_FORMAT_CBC); + std::vector kmm = encodeKMM(hello); + hello.generateMAC(tek, kmm.data()); + + std::vector datagram(14U + kmm.size(), 0U); + datagram[0U] = 0U; // Version-0 format + datagram[1U] = MFG_STANDARD; + datagram[2U] = ALGO_UNENCRYPT; + ::memcpy(datagram.data() + 14U, kmm.data(), kmm.size()); + + SECTION("valid clear DLI reaches the authenticated KMM dispatcher") { + KMMFNEHarness harness; + makeHarnessKey(harness); + FNETestHooks::processOTARDLI(harness.traffic, datagram); + REQUIRE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN)); + } + + SECTION("invalid preamble MFID is rejected before KMM dispatch") { + KMMFNEHarness harness; + makeHarnessKey(harness); + datagram[1U] ^= 0x01U; + FNETestHooks::processOTARDLI(harness.traffic, datagram); + REQUIRE_FALSE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN)); + } + + SECTION("oversized DLI KMM is rejected before KMM dispatch") { + KMMFNEHarness harness; + makeHarnessKey(harness); + datagram.resize(14U + 466U, 0U); + FNETestHooks::processOTARDLI(harness.traffic, datagram); + REQUIRE_FALSE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN)); + } +}