diff --git a/src/common/p25/kmm/KMMChangeover.cpp b/src/common/p25/kmm/KMMChangeover.cpp index 7e7748b7..5bd6b9cc 100644 --- a/src/common/p25/kmm/KMMChangeover.cpp +++ b/src/common/p25/kmm/KMMChangeover.cpp @@ -82,6 +82,9 @@ void KMMChangeover::encode(uint8_t* data) std::string KMMChangeover::toString() { + if (m_messageId == KMM_MessageType::CHANGEOVER_RSP) + return std::string("KMM, CHANGEOVER_RSP (Changeover Response)"); + return std::string("KMM, CHANGEOVER_CMD (Changeover Command)"); } diff --git a/src/common/p25/kmm/KMMFactory.cpp b/src/common/p25/kmm/KMMFactory.cpp index 18cc90da..82e1dacf 100644 --- a/src/common/p25/kmm/KMMFactory.cpp +++ b/src/common/p25/kmm/KMMFactory.cpp @@ -135,6 +135,12 @@ std::unique_ptr KMMFactory::create(const uint8_t* data, uint32_t len) const uint32_t available = bodyEnd - body; switch (data[0U]) { + case KMM_MessageType::CHANGEOVER_CMD: + case KMM_MessageType::CHANGEOVER_RSP: + if (available < 1U || data[body] == 0U || + available != 1U + (uint32_t)data[body] * 2U) + return nullptr; + break; case KMM_MessageType::HELLO: case KMM_MessageType::INVENTORY_CMD: case KMM_MessageType::DEREG_RSP: diff --git a/src/fne/network/P25OTARService.cpp b/src/fne/network/P25OTARService.cpp index ffaae43f..c0ca6c76 100644 --- a/src/fne/network/P25OTARService.cpp +++ b/src/fne/network/P25OTARService.cpp @@ -58,6 +58,8 @@ using namespace p25::kmm; bool isSupportedKMM(uint8_t messageId) { switch (messageId) { + case KMM_MessageType::CHANGEOVER_CMD: + case KMM_MessageType::CHANGEOVER_RSP: case KMM_MessageType::HELLO: case KMM_MessageType::INVENTORY_CMD: case KMM_MessageType::INVENTORY_RSP: @@ -1264,6 +1266,73 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS return firstFrame; } +/* Helper used to build a Changeover-Command KMM for an SU. */ + +UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t kmmRSI, + uint8_t supersededKeysetId, uint8_t activeKeysetId, uint32_t* payloadSize) +{ + if (payloadSize != nullptr) + *payloadSize = 0U; + + lookups::RadioId ridEntry = m_network->m_ridLookup->find(kmmRSI); + if (ridEntry.radioDefault() || !ridEntry.radioEnabled() || !ridEntry.canRekey()) { + LogWarning(LOG_P25, P25_KMM_STR ", cannot build Changeover Command; RID %u has no eligible key policy", kmmRSI); + return nullptr; + } + + const std::vector allowedKIds = ridEntry.allowedKIds(); + EKCKeyItem macTekItem; + 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 ", 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]; + + KMMChangeover outKmm; + outKmm.setSrcLLId(WUID_FNE); + outKmm.setDstLLId(kmmRSI); + outKmm.setSupersededKeysetId(supersededKeysetId); + outKmm.setActiveKeysetId(activeKeysetId); + outKmm.setHasMessageNumber(true); + outKmm.setMessageNumber(mn); + outKmm.setMACType(KMM_MAC::ENH_MAC); + outKmm.setMACAlgId(macTekItem.algId()); + outKmm.setMACKId((uint16_t)macTekItem.kId()); + outKmm.setMACFormat(KMM_MAC_FORMAT_CBC); + + const uint32_t frameLength = outKmm.fullLength(); + UInt8Array encoded = std::make_unique(frameLength); + ::memset(encoded.get(), 0x00U, frameLength); + outKmm.encode(encoded.get()); + + uint8_t macTek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U }; + macTekItem.getKey(macTek); + outKmm.generateMAC(macTek, encoded.get()); + + 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); + } + + return encoded; +} + /* Helper used to return a Registration-Command KMM to the calling SU. */ UInt8Array P25OTARService::write_KMM_Reg_Command(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize) diff --git a/src/fne/network/P25OTARService.h b/src/fne/network/P25OTARService.h index b68ef006..f2c94975 100644 --- a/src/fne/network/P25OTARService.h +++ b/src/fne/network/P25OTARService.h @@ -197,6 +197,18 @@ namespace network UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize, const KMMAuthContext& auth, std::vector>* additionalResponses = nullptr); + /** + * @brief Builds an authenticated Changeover-Command KMM for an SU. + * @param llId Logical Link Address used to track the outbound message number. + * @param kmmRSI Destination KMM Radio Set Identifier. + * @param supersededKeysetId Keyset ID being superseded. + * @param activeKeysetId Keyset ID becoming active. + * @param[out] payloadSize Size of the returned KMM payload. + * @returns Encoded KMM Changeover Command, or nullptr if no authorized MAC TEK is available. + */ + UInt8Array write_KMM_Changeover_Command(uint32_t llId, uint32_t kmmRSI, + uint8_t supersededKeysetId, uint8_t activeKeysetId, uint32_t* payloadSize); + /** * @brief Helper used to return a Registration-Command KMM to the calling SU. * @param llId Logical Link Address. diff --git a/tests/fne/FNETestHooks.cpp b/tests/fne/FNETestHooks.cpp index b3af837b..b09ad206 100644 --- a/tests/fne/FNETestHooks.cpp +++ b/tests/fne/FNETestHooks.cpp @@ -125,6 +125,23 @@ std::vector> FNETestHooks::buildOTARRekey(TrafficNetwork& n return frames; } +/** Builds an OTAR Changeover Command without dispatching it. */ + +std::vector FNETestHooks::buildOTARChangeover(TrafficNetwork& network, + uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId) +{ + if (network.m_p25OTARService == nullptr) + return {}; + + uint32_t length = 0U; + UInt8Array encoded = network.m_p25OTARService->write_KMM_Changeover_Command(llId, + kmmRSI, supersededKeysetId, activeKeysetId, &length); + if (encoded == nullptr || length == 0U) + return {}; + + return std::vector(encoded.get(), encoded.get() + length); +} + /** * @brief Resolves the security parameters of an OTAR response packet. * @param network The TrafficNetwork instance. diff --git a/tests/fne/FNETestHooks.h b/tests/fne/FNETestHooks.h index 1a21f26a..14b1059d 100644 --- a/tests/fne/FNETestHooks.h +++ b/tests/fne/FNETestHooks.h @@ -107,6 +107,17 @@ public: */ static std::vector> buildOTARRekey(network::TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI); + /** + * @brief Builds an OTAR Changeover Command without dispatching it. + * @param network The TrafficNetwork instance. + * @param llId Logical Link ID used for message-number tracking. + * @param kmmRSI Destination KMM RSI. + * @param supersededKeysetId Keyset ID being superseded. + * @param activeKeysetId Keyset ID becoming active. + * @return Encoded command, or an empty vector when it cannot be built. + */ + static std::vector buildOTARChangeover(network::TrafficNetwork& network, + uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId); /** * @brief Resolves the outer security context selected for a generated response. * @param network The TrafficNetwork instance. diff --git a/tests/p25/P25_KMM_Tests.cpp b/tests/p25/P25_KMM_Tests.cpp index db1ab0a4..dd70d10c 100644 --- a/tests/p25/P25_KMM_Tests.cpp +++ b/tests/p25/P25_KMM_Tests.cpp @@ -10,6 +10,7 @@ #include "host/Defines.h" #include "common/p25/P25Defines.h" #include "common/p25/kmm/KMMFactory.h" +#include "common/p25/kmm/KMMChangeover.h" #include "common/p25/kmm/KMMDeregistrationCommand.h" #include "common/p25/kmm/KMMDeregistrationResponse.h" #include "common/p25/kmm/KMMHello.h" @@ -620,6 +621,58 @@ TEST_CASE("P25 OTAR Rekey Commands batch four keys per KMM", "[p25][kmm][otar][r } } +TEST_CASE("P25 OTAR builds authenticated Changeover Commands without dispatching them", + "[p25][kmm][otar][changeover]") +{ + KMMFNEHarness harness; + constexpr uint32_t SU_RSI = 0x654321U; + constexpr uint16_t TEK_KID = 0x2345U; + constexpr uint8_t SUPERSEDED_KEYSET = 0x01U; + constexpr uint8_t ACTIVE_KEYSET = 0x02U; + + EKCKeyItem tek; + tek.id(1U); + tek.algId(ALGO_AES_256); + tek.kId(TEK_KID); + tek.sln(1U); + tek.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"); + FNETestHooks::addCryptoKey(harness.traffic, tek); + harness.rid.addEntry(SU_RSI, true, "changeover-test", "", true, true, { TEK_KID }); + + const std::vector encoded = FNETestHooks::buildOTARChangeover(harness.traffic, + SU_RSI, SU_RSI, SUPERSEDED_KEYSET, ACTIVE_KEYSET); + REQUIRE_FALSE(encoded.empty()); + + std::unique_ptr base = KMMFactory::create(encoded.data(), (uint32_t)encoded.size()); + REQUIRE(base != nullptr); + KMMChangeover* changeover = dynamic_cast(base.get()); + REQUIRE(changeover != nullptr); + CHECK(changeover->getMessageId() == KMM_MessageType::CHANGEOVER_CMD); + CHECK(changeover->getResponseKind() == KMM_ResponseKind::IMMEDIATE); + CHECK(changeover->getSrcLLId() == WUID_FNE); + CHECK(changeover->getDstLLId() == SU_RSI); + CHECK(changeover->getSupersededKeysetId() == SUPERSEDED_KEYSET); + CHECK(changeover->getActiveKeysetId() == ACTIVE_KEYSET); + CHECK(changeover->getHasMessageNumber()); + CHECK(changeover->getMessageNumber() == 1U); + CHECK(changeover->getMACType() == KMM_MAC::ENH_MAC); + CHECK(changeover->getMACAlgId() == ALGO_AES_256); + CHECK(changeover->getMACKId() == TEK_KID); + + uint8_t material[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U }; + tek.getKey(material); + CHECK(changeover->verifyMAC(material, encoded.data(), (uint32_t)encoded.size())); + + bool encrypted = false; + uint8_t algoId = ALGO_UNENCRYPT; + uint16_t kid = 0U; + REQUIRE(FNETestHooks::resolveOTARResponseSecurity(harness.traffic, encoded, + encrypted, algoId, kid)); + CHECK(encrypted); + CHECK(algoId == ALGO_AES_256); + CHECK(kid == TEK_KID); +} + #if !defined(_WIN32) TEST_CASE("P25 OTAR DLI traverses the real UDP endpoint", "[p25][kmm][otar][dli][integration]") {