You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1111 lines
46 KiB
1111 lines
46 KiB
// 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 "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"
|
|
#include "common/p25/kmm/KMMInventoryCommand.h"
|
|
#include "common/p25/kmm/KMMNoService.h"
|
|
#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"
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <cstring>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <arpa/inet.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
using namespace p25;
|
|
using namespace p25::defines;
|
|
using namespace p25::kmm;
|
|
|
|
/**
|
|
* @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),
|
|
rid("", 0U, false, false),
|
|
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)
|
|
{
|
|
traffic.setLookups(&rid, nullptr, nullptr, nullptr, &crypto, nullptr);
|
|
}
|
|
|
|
HostFNE host;
|
|
CryptoContainer crypto;
|
|
lookups::RadioIdLookup rid;
|
|
network::TrafficNetwork traffic;
|
|
};
|
|
|
|
namespace {
|
|
constexpr uint32_t KMF_RSI = P25DEF::KMF_DEFAULT_RSI;
|
|
|
|
/**
|
|
* @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<typename T>
|
|
std::vector<uint8_t> encodeKMM(T& frame)
|
|
{
|
|
std::vector<uint8_t> bytes(frame.fullLength(), 0U);
|
|
frame.encode(bytes.data());
|
|
return bytes;
|
|
}
|
|
}
|
|
|
|
TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm][factory]")
|
|
{
|
|
SECTION("HELLO frame round-trips with message number") {
|
|
KMMHello tx;
|
|
tx.setDstLLId(0x123456U);
|
|
tx.setSrcLLId(0x654321U);
|
|
tx.setMessageNumber(0x1122U);
|
|
tx.setFlag(KMM_HelloFlag::REKEY_REQUEST_NO_UKEK);
|
|
|
|
UInt8Array buffer = std::make_unique<uint8_t[]>(tx.fullLength());
|
|
::memset(buffer.get(), 0x00U, tx.fullLength());
|
|
tx.encode(buffer.get());
|
|
|
|
std::unique_ptr<KMMFrame> base = KMMFactory::create(buffer.get());
|
|
REQUIRE(base != nullptr);
|
|
|
|
KMMHello* rx = dynamic_cast<KMMHello*>(base.get());
|
|
REQUIRE(rx != nullptr);
|
|
REQUIRE(rx->getMessageId() == KMM_MessageType::HELLO);
|
|
REQUIRE(rx->getFlag() == KMM_HelloFlag::REKEY_REQUEST_NO_UKEK);
|
|
REQUIRE(rx->getDstLLId() == 0x123456U);
|
|
REQUIRE(rx->getSrcLLId() == 0x654321U);
|
|
REQUIRE(rx->getHasMessageNumber() == true);
|
|
REQUIRE(rx->getMessageNumber() == 0x1122U);
|
|
}
|
|
|
|
SECTION("NO_SERVICE frame round-trips") {
|
|
KMMNoService tx;
|
|
tx.setDstLLId(0x010203U);
|
|
tx.setSrcLLId(0xA0B0C0U);
|
|
|
|
UInt8Array buffer = std::make_unique<uint8_t[]>(tx.fullLength());
|
|
::memset(buffer.get(), 0x00U, tx.fullLength());
|
|
tx.encode(buffer.get());
|
|
|
|
std::unique_ptr<KMMFrame> base = KMMFactory::create(buffer.get());
|
|
REQUIRE(base != nullptr);
|
|
|
|
KMMNoService* rx = dynamic_cast<KMMNoService*>(base.get());
|
|
REQUIRE(rx != nullptr);
|
|
REQUIRE(rx->getMessageId() == KMM_MessageType::NO_SERVICE);
|
|
REQUIRE(rx->getDstLLId() == 0x010203U);
|
|
REQUIRE(rx->getSrcLLId() == 0xA0B0C0U);
|
|
}
|
|
|
|
SECTION("Factory returns nullptr for unknown message ID") {
|
|
uint8_t buffer[16U] = { 0U };
|
|
buffer[0U] = 0xFFU;
|
|
|
|
std::unique_ptr<KMMFrame> frame = KMMFactory::create(buffer);
|
|
REQUIRE(frame == nullptr);
|
|
}
|
|
}
|
|
|
|
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(KMF_RSI);
|
|
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<uint8_t> encoded = encodeKMM(tx);
|
|
std::unique_ptr<KMMFrame> base = KMMFactory::create(encoded.data(), encoded.size());
|
|
REQUIRE(base != nullptr);
|
|
KMMWarmStartCommand* rx = dynamic_cast<KMMWarmStartCommand*>(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(KMF_RSI); hello.setSrcLLId(SU_RSI);
|
|
hello.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
hello.setFlag(KMM_HelloFlag::REKEY_REQUEST_UKEK);
|
|
std::vector<uint8_t> helloBytes = encodeKMM(hello);
|
|
uint32_t commandLength = 0U;
|
|
UInt8Array commandResponse = FNETestHooks::processOTARKMM(harness.traffic,
|
|
helloBytes, SU_RSI, commandLength);
|
|
REQUIRE(commandResponse != nullptr);
|
|
std::vector<uint8_t> commandBytes(commandResponse.get(), commandResponse.get() + commandLength);
|
|
REQUIRE_FALSE(commandBytes.empty());
|
|
std::unique_ptr<KMMFrame> commandBase = KMMFactory::create(commandBytes.data(), commandBytes.size());
|
|
REQUIRE(commandBase != nullptr);
|
|
KMMWarmStartCommand* command = dynamic_cast<KMMWarmStartCommand*>(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<uint8_t> 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(KMF_RSI); 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<uint8_t> 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<KMMFrame> rekeyBase = KMMFactory::create(rekeyBytes.get());
|
|
KMMRekeyCommand* rekey = dynamic_cast<KMMRekeyCommand*>(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(KMF_RSI); 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<uint8_t> finalAckBytes = encodeKMM(finalAck);
|
|
finalAck.generateMAC(finalKey, finalAckBytes.data());
|
|
uint32_t finalResponseLength = 0U;
|
|
UInt8Array finalResponse = FNETestHooks::processOTARKMM(harness.traffic,
|
|
finalAckBytes, SU_RSI, finalResponseLength, ALGO_AES_256, FINAL_KID);
|
|
REQUIRE(finalResponse != nullptr);
|
|
|
|
std::unique_ptr<KMMFrame> changeoverBase = KMMFactory::create(finalResponse.get(),
|
|
finalResponseLength);
|
|
KMMChangeover* changeover = dynamic_cast<KMMChangeover*>(changeoverBase.get());
|
|
REQUIRE(changeover != nullptr);
|
|
CHECK(changeover->getMessageId() == KMM_MessageType::CHANGEOVER_CMD);
|
|
CHECK(changeover->getSupersededKeysetId() == 1U);
|
|
CHECK(changeover->getActiveKeysetId() == 1U);
|
|
CHECK(changeover->getMessageNumber() == rekey->getMessageNumber() + 1U);
|
|
CHECK(changeover->getMACKId() == FINAL_KID);
|
|
CHECK(changeover->verifyMAC(finalKey, finalResponse.get(), finalResponseLength));
|
|
CHECK_FALSE(FNETestHooks::hasOTARWarmStart(harness.traffic, SU_RSI));
|
|
}
|
|
|
|
TEST_CASE("KMM air-facing factory rejects malformed lengths", "[p25][kmm][security]")
|
|
{
|
|
KMMHello hello;
|
|
hello.setDstLLId(KMF_RSI);
|
|
hello.setSrcLLId(0x654321U);
|
|
std::vector<uint8_t> bytes = encodeKMM(hello);
|
|
|
|
REQUIRE(KMMFactory::create(bytes.data(), (uint32_t)bytes.size()) != nullptr);
|
|
REQUIRE(KMMFactory::create(bytes.data(), 9U) == nullptr);
|
|
|
|
std::vector<uint8_t> 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("P25 OTAR direct Rekey issues Changeover after its final acknowledgment",
|
|
"[p25][kmm][otar][rekey][changeover]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
constexpr uint16_t FINAL_KID = 0x2001U;
|
|
const char* finalHex = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F";
|
|
|
|
EKCKeyItem finalTEK;
|
|
finalTEK.id(1U);
|
|
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, "direct-rekey-test", "", true, true,
|
|
{ FINAL_KID });
|
|
|
|
const std::vector<std::vector<uint8_t>> rekeyFrames =
|
|
FNETestHooks::buildOTARRekey(harness.traffic, SU_RSI, SU_RSI);
|
|
REQUIRE(rekeyFrames.size() == 1U);
|
|
|
|
std::unique_ptr<KMMFrame> rekeyBase = KMMFactory::create(rekeyFrames[0U].data());
|
|
KMMRekeyCommand* rekey = dynamic_cast<KMMRekeyCommand*>(rekeyBase.get());
|
|
REQUIRE(rekey != nullptr);
|
|
|
|
uint8_t finalKey[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U };
|
|
finalTEK.getKey(finalKey);
|
|
KeyStatus status;
|
|
status.algId(ALGO_AES_256);
|
|
status.kId(FINAL_KID);
|
|
status.status(KMM_Status::CMD_PERFORMED);
|
|
|
|
KMMRekeyAck ack;
|
|
ack.setDstLLId(KMF_RSI);
|
|
ack.setSrcLLId(SU_RSI);
|
|
ack.setHasMessageNumber(true);
|
|
ack.setMessageNumber(rekey->getMessageNumber());
|
|
ack.setMACType(KMM_MAC::ENH_MAC);
|
|
ack.setMACAlgId(ALGO_AES_256);
|
|
ack.setMACKId(FINAL_KID);
|
|
ack.setMACFormat(KMM_MAC_FORMAT_CMAC);
|
|
ack.setAckMessageId(KMM_MessageType::REKEY_CMD);
|
|
ack.setNumberOfKeyStatus(1U);
|
|
ack.setKeyStatus({ status });
|
|
std::vector<uint8_t> ackBytes = encodeKMM(ack);
|
|
ack.generateMAC(finalKey, ackBytes.data());
|
|
|
|
uint32_t responseLength = 0U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic, ackBytes,
|
|
SU_RSI, responseLength, ALGO_AES_256, FINAL_KID);
|
|
REQUIRE(response != nullptr);
|
|
|
|
std::unique_ptr<KMMFrame> changeoverBase = KMMFactory::create(response.get(),
|
|
responseLength);
|
|
KMMChangeover* changeover = dynamic_cast<KMMChangeover*>(changeoverBase.get());
|
|
REQUIRE(changeover != nullptr);
|
|
CHECK(changeover->getSupersededKeysetId() == 1U);
|
|
CHECK(changeover->getActiveKeysetId() == 1U);
|
|
CHECK(changeover->getMessageNumber() == rekey->getMessageNumber() + 1U);
|
|
CHECK(changeover->getMACKId() == FINAL_KID);
|
|
CHECK(changeover->verifyMAC(finalKey, response.get(), responseLength));
|
|
}
|
|
|
|
TEST_CASE("P25 OTAR resynchronizes Rekey message numbers with same-RSI Change-RSI",
|
|
"[p25][kmm][otar][rekey][change-rsi]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
constexpr uint16_t TEK_KID = 0x2001U;
|
|
|
|
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);
|
|
EKCKeyItem ukek;
|
|
ukek.id(2U); ukek.rsiId(SU_RSI); ukek.algId(ALGO_AES_256);
|
|
ukek.kId(0x1001U); ukek.sln(0U);
|
|
ukek.keyMaterial("101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F");
|
|
FNETestHooks::addCryptoUKEK(harness.traffic, ukek);
|
|
harness.rid.addEntry(SU_RSI, true, "change-rsi-test", "", true, true,
|
|
{ TEK_KID });
|
|
|
|
const std::vector<std::vector<uint8_t>> frames =
|
|
FNETestHooks::buildOTARRekey(harness.traffic, SU_RSI, SU_RSI);
|
|
REQUIRE(frames.size() == 1U);
|
|
std::unique_ptr<KMMFrame> rekeyBase = KMMFactory::create(frames[0U].data());
|
|
KMMRekeyCommand* rekey = dynamic_cast<KMMRekeyCommand*>(rekeyBase.get());
|
|
REQUIRE(rekey != nullptr);
|
|
|
|
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES] = { 0U };
|
|
tek.getKey(key);
|
|
KMMNegativeAck nack;
|
|
nack.setDstLLId(KMF_RSI); nack.setSrcLLId(SU_RSI);
|
|
nack.setHasMessageNumber(true); nack.KMMFrame::setMessageNumber(10U);
|
|
nack.setMACType(KMM_MAC::ENH_MAC); nack.setMACAlgId(ALGO_AES_256);
|
|
nack.setMACKId(TEK_KID); nack.setMACFormat(KMM_MAC_FORMAT_CMAC);
|
|
nack.setNakMessageId(KMM_MessageType::REKEY_CMD);
|
|
nack.setMessageNumber(rekey->getMessageNumber());
|
|
nack.setStatus(KMM_Status::INVALID_MSG_NUMBER);
|
|
std::vector<uint8_t> nackBytes = encodeKMM(nack);
|
|
nack.generateMAC(key, nackBytes.data());
|
|
|
|
uint32_t commandLength = 0U;
|
|
UInt8Array commandBytes = FNETestHooks::processOTARKMM(harness.traffic,
|
|
nackBytes, SU_RSI, commandLength, ALGO_AES_256, TEK_KID);
|
|
REQUIRE(commandBytes != nullptr);
|
|
std::unique_ptr<KMMFrame> commandBase = KMMFactory::create(commandBytes.get(),
|
|
commandLength);
|
|
KMMChangeRSI* command = dynamic_cast<KMMChangeRSI*>(commandBase.get());
|
|
REQUIRE(command != nullptr);
|
|
CHECK(command->getMessageId() == KMM_MessageType::CHANGE_RSI_CMD);
|
|
CHECK(command->getOldRSI() == SU_RSI);
|
|
CHECK(command->getNewRSI() == SU_RSI);
|
|
CHECK(command->KMMFrame::getMessageNumber() == 10U);
|
|
CHECK(command->getInitialMessageNumber() == 11U);
|
|
CHECK(command->verifyMAC(key, commandBytes.get(), commandLength));
|
|
|
|
KMMChangeRSI response;
|
|
response.setMessageId(KMM_MessageType::CHANGE_RSI_RSP);
|
|
response.setDstLLId(KMF_RSI); response.setSrcLLId(SU_RSI);
|
|
response.setHasMessageNumber(true); response.setMessageNumber(11U);
|
|
response.setMACType(KMM_MAC::ENH_MAC); response.setMACAlgId(ALGO_AES_256);
|
|
response.setMACKId(TEK_KID); response.setMACFormat(KMM_MAC_FORMAT_CMAC);
|
|
response.setOldRSI(SU_RSI); response.setNewRSI(SU_RSI);
|
|
response.setStatus(KMM_Status::CMD_PERFORMED);
|
|
std::vector<uint8_t> responseBytes = encodeKMM(response);
|
|
response.generateMAC(key, responseBytes.data());
|
|
|
|
uint32_t retryLength = 0U;
|
|
UInt8Array retryBytes = FNETestHooks::processOTARKMM(harness.traffic,
|
|
responseBytes, SU_RSI, retryLength, ALGO_AES_256, TEK_KID);
|
|
REQUIRE(retryBytes != nullptr);
|
|
std::unique_ptr<KMMFrame> retryBase = KMMFactory::create(retryBytes.get());
|
|
KMMRekeyCommand* retry = dynamic_cast<KMMRekeyCommand*>(retryBase.get());
|
|
REQUIRE(retry != nullptr);
|
|
CHECK(retry->getMessageNumber() == 12U);
|
|
}
|
|
|
|
TEST_CASE("FNE OTAR dispatcher follows AACA response-kind procedures", "[p25][kmm][otar]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
SECTION("Response Kind 1 Hello produces no OTAR response") {
|
|
KMMHello request;
|
|
request.setDstLLId(KMF_RSI);
|
|
request.setSrcLLId(SU_RSI);
|
|
request.setResponseKind(KMM_ResponseKind::NONE);
|
|
|
|
uint32_t payloadSize = 99U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(request), SU_RSI, payloadSize);
|
|
|
|
REQUIRE(response == nullptr);
|
|
REQUIRE(payloadSize == 0U);
|
|
}
|
|
|
|
SECTION("SU-originated Response Kind 2 Hello is discarded") {
|
|
KMMHello request;
|
|
request.setDstLLId(KMF_RSI);
|
|
request.setSrcLLId(SU_RSI);
|
|
request.setResponseKind(KMM_ResponseKind::DELAYED);
|
|
|
|
uint32_t payloadSize = 99U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(request), SU_RSI, payloadSize);
|
|
|
|
REQUIRE(response == nullptr);
|
|
REQUIRE(payloadSize == 0U);
|
|
}
|
|
|
|
SECTION("Response Kind 3 Hello receives No-Service when KMF service is disabled") {
|
|
KMMHello request;
|
|
request.setDstLLId(KMF_RSI);
|
|
request.setSrcLLId(SU_RSI);
|
|
request.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
request.setFlag(KMM_HelloFlag::REKEY_REQUEST_UKEK);
|
|
|
|
uint32_t payloadSize = 0U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(request), SU_RSI, payloadSize);
|
|
|
|
REQUIRE(response != nullptr);
|
|
REQUIRE(payloadSize == KMMNoService().fullLength());
|
|
std::unique_ptr<KMMFrame> decoded = KMMFactory::create(response.get());
|
|
REQUIRE(decoded != nullptr);
|
|
REQUIRE(decoded->getMessageId() == KMM_MessageType::NO_SERVICE);
|
|
REQUIRE(decoded->getResponseKind() == KMM_ResponseKind::NONE);
|
|
REQUIRE(decoded->getDstLLId() == SU_RSI);
|
|
REQUIRE(decoded->getSrcLLId() == KMF_RSI);
|
|
}
|
|
|
|
SECTION("Response Kind 3 Deregistration receives Deregistration-Response") {
|
|
FNETestHooks::setKMFServicesEnabled(harness.traffic, true);
|
|
|
|
KMMDeregistrationCommand request;
|
|
request.setDstLLId(KMF_RSI);
|
|
request.setSrcLLId(SU_RSI);
|
|
request.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
request.setKMFRSI(KMF_RSI);
|
|
|
|
uint32_t payloadSize = 0U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(request), SU_RSI, payloadSize);
|
|
|
|
REQUIRE(response != nullptr);
|
|
KMMDeregistrationResponse expected;
|
|
REQUIRE(payloadSize == expected.fullLength());
|
|
std::unique_ptr<KMMFrame> decoded = KMMFactory::create(response.get());
|
|
REQUIRE(decoded != nullptr);
|
|
REQUIRE(decoded->getMessageId() == KMM_MessageType::DEREG_RSP);
|
|
REQUIRE(decoded->getResponseKind() == KMM_ResponseKind::NONE);
|
|
REQUIRE(decoded->getDstLLId() == SU_RSI);
|
|
REQUIRE(decoded->getSrcLLId() == KMF_RSI);
|
|
}
|
|
|
|
SECTION("security-sensitive KMM received without outer encryption is discarded") {
|
|
KMMRekeyAck ack;
|
|
ack.setDstLLId(KMF_RSI);
|
|
ack.setSrcLLId(SU_RSI);
|
|
ack.setResponseKind(KMM_ResponseKind::NONE);
|
|
ack.setMessageId(KMM_MessageType::REKEY_CMD);
|
|
ack.setNumberOfKeyStatus(0U);
|
|
|
|
uint32_t payloadSize = 99U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(ack), SU_RSI, payloadSize);
|
|
REQUIRE(response == nullptr);
|
|
REQUIRE(payloadSize == 0U);
|
|
}
|
|
}
|
|
|
|
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(KMF_RSI); 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<uint8_t> bytes = encodeKMM(hello);
|
|
hello.generateMAC(tek, bytes.data());
|
|
return bytes;
|
|
};
|
|
|
|
uint32_t size = 0U;
|
|
std::vector<uint8_t> valid = makeSigned(100U, KMM_HelloFlag::REKEY_REQUEST_UKEK);
|
|
UInt8Array cbcResponse = FNETestHooks::processOTARKMM(harness.traffic, valid, SU_RSI, size);
|
|
REQUIRE(cbcResponse != nullptr);
|
|
std::unique_ptr<KMMFrame> 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<uint8_t> changedSameMN = makeSigned(100U, KMM_HelloFlag::IDENT_ONLY);
|
|
REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, changedSameMN, SU_RSI, size) == nullptr);
|
|
|
|
std::vector<uint8_t> badMac = makeSigned(101U, KMM_HelloFlag::REKEY_REQUEST_UKEK);
|
|
badMac[10U] ^= 0x01U;
|
|
REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, badMac, SU_RSI, size) == nullptr);
|
|
|
|
std::vector<uint8_t> 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<KMMFrame> 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<uint8_t> next = makeSigned(102U, KMM_HelloFlag::REKEY_REQUEST_UKEK);
|
|
REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, next, SU_RSI, size) != nullptr);
|
|
}
|
|
|
|
TEST_CASE("FNE OTAR returns secured RK3 KMM NACKs for validation failures", "[p25][kmm][otar][nack]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
constexpr uint16_t TEK_KID = 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(TEK_KID); item.sln(1U);
|
|
item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
|
|
FNETestHooks::addCryptoKey(harness.traffic, item);
|
|
|
|
auto signedHello = [&](uint16_t mn) {
|
|
KMMHello hello;
|
|
hello.setDstLLId(KMF_RSI); hello.setSrcLLId(SU_RSI);
|
|
hello.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
hello.setHasMessageNumber(true); hello.setMessageNumber(mn);
|
|
hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256);
|
|
hello.setMACKId(TEK_KID); hello.setMACFormat(KMM_MAC_FORMAT_CBC);
|
|
std::vector<uint8_t> bytes = encodeKMM(hello);
|
|
hello.generateMAC(tek, bytes.data());
|
|
return bytes;
|
|
};
|
|
|
|
auto requireNack = [&](const std::vector<uint8_t>& request, uint8_t status, uint16_t mn,
|
|
uint8_t rejectedMessageId = KMM_MessageType::HELLO) {
|
|
uint32_t size = 0U;
|
|
UInt8Array response = FNETestHooks::processOTARKMM(harness.traffic, request, SU_RSI,
|
|
size, ALGO_AES_256, TEK_KID);
|
|
REQUIRE(response != nullptr);
|
|
std::unique_ptr<KMMFrame> decoded = KMMFactory::create(response.get(), size);
|
|
REQUIRE(decoded != nullptr);
|
|
KMMNegativeAck* nack = dynamic_cast<KMMNegativeAck*>(decoded.get());
|
|
REQUIRE(nack != nullptr);
|
|
REQUIRE(nack->getResponseKind() == KMM_ResponseKind::NONE);
|
|
REQUIRE(nack->getNakMessageId() == rejectedMessageId);
|
|
REQUIRE(nack->getMessageNumber() == mn);
|
|
REQUIRE(nack->getStatus() == status);
|
|
REQUIRE(nack->getMACType() == KMM_MAC::ENH_MAC);
|
|
REQUIRE(nack->getMACKId() == TEK_KID);
|
|
REQUIRE(nack->verifyMAC(tek, response.get(), size));
|
|
};
|
|
|
|
std::vector<uint8_t> accepted = signedHello(100U);
|
|
uint32_t ignoredSize = 0U;
|
|
REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, accepted, SU_RSI, ignoredSize,
|
|
ALGO_AES_256, TEK_KID) != nullptr);
|
|
|
|
std::vector<uint8_t> replay = signedHello(99U);
|
|
requireNack(replay, KMM_Status::INVALID_MSG_NUMBER, 99U);
|
|
|
|
std::vector<uint8_t> badMac = signedHello(101U);
|
|
badMac[12U] ^= 0x01U;
|
|
requireNack(badMac, KMM_Status::INVALID_MAC, 101U);
|
|
|
|
KMMHello noMac;
|
|
noMac.setDstLLId(KMF_RSI); noMac.setSrcLLId(SU_RSI);
|
|
noMac.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
noMac.setHasMessageNumber(true); noMac.setMessageNumber(102U);
|
|
requireNack(encodeKMM(noMac), KMM_Status::INVALID_MSG_NUMBER, 102U);
|
|
|
|
std::vector<uint8_t> unknownMessage = signedHello(103U);
|
|
unknownMessage[0U] = 0x7FU;
|
|
KMMHello signer;
|
|
signer.setHasMessageNumber(true); signer.setMessageNumber(103U);
|
|
signer.setMACType(KMM_MAC::ENH_MAC); signer.setMACAlgId(ALGO_AES_256);
|
|
signer.setMACKId(TEK_KID); signer.setMACFormat(KMM_MAC_FORMAT_CBC);
|
|
signer.fullLength();
|
|
signer.generateMAC(tek, unknownMessage.data());
|
|
requireNack(unknownMessage, KMM_Status::INVALID_MSG_ID, 103U, 0x7FU);
|
|
|
|
std::vector<uint8_t> missingMacKey = signedHello(104U);
|
|
SET_UINT16(0x4321U, missingMacKey.data(), missingMacKey.size() - 3U);
|
|
requireNack(missingMacKey, KMM_Status::ITEM_NOT_EXIST, 104U);
|
|
|
|
KMMInventoryCommand unsupportedProcedure;
|
|
unsupportedProcedure.setDstLLId(KMF_RSI); unsupportedProcedure.setSrcLLId(SU_RSI);
|
|
unsupportedProcedure.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
unsupportedProcedure.setHasMessageNumber(true); unsupportedProcedure.setMessageNumber(105U);
|
|
unsupportedProcedure.setMACType(KMM_MAC::ENH_MAC); unsupportedProcedure.setMACAlgId(ALGO_AES_256);
|
|
unsupportedProcedure.setMACKId(TEK_KID); unsupportedProcedure.setMACFormat(KMM_MAC_FORMAT_CBC);
|
|
unsupportedProcedure.setInventoryType(KMM_InventoryType::LIST_ACTIVE_KEYSET_IDS);
|
|
std::vector<uint8_t> unsupportedBytes = encodeKMM(unsupportedProcedure);
|
|
unsupportedProcedure.generateMAC(tek, unsupportedBytes.data());
|
|
requireNack(unsupportedBytes, KMM_Status::INVALID_MSG_ID, 105U, KMM_MessageType::INVENTORY_CMD);
|
|
}
|
|
|
|
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(KMF_RSI);
|
|
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<uint8_t> 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));
|
|
|
|
SECTION("encrypted DLD decrypts through the Auxiliary ES context") {
|
|
hello.setMessageNumber(MN + 1U);
|
|
std::vector<uint8_t> next = encodeKMM(hello);
|
|
hello.generateMAC(tek, next.data());
|
|
uint8_t mi[MI_LENGTH_BYTES] = { 0x10U, 0x21U, 0x32U, 0x43U, 0x54U,
|
|
0x65U, 0x76U, 0x87U, 0x98U };
|
|
std::vector<uint8_t> encrypted = FNETestHooks::cryptOTARKMM(harness.traffic,
|
|
ALGO_AES_256, 0x1234U, mi, next, true);
|
|
REQUIRE(encrypted.size() == next.size());
|
|
REQUIRE(encrypted != next);
|
|
REQUIRE(FNETestHooks::processOTARDLDPDU(harness.traffic, encrypted, SU_RSI,
|
|
true, ALGO_AES_256, 0x1234U, mi));
|
|
REQUIRE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN + 1U));
|
|
}
|
|
|
|
SECTION("encrypted DLD without Auxiliary ES metadata is rejected") {
|
|
REQUIRE_FALSE(FNETestHooks::processOTARDLD(harness.traffic, encoded, SU_RSI, 10U,
|
|
true, ALGO_AES_256, 0x1234U, nullptr));
|
|
}
|
|
|
|
std::vector<uint8_t> 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<uint8_t> 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 FNE accepts a KMM immediately after a PDU response", "[p25][kmm][otar][pdu-state]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
constexpr uint16_t MN = 1U;
|
|
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);
|
|
|
|
REQUIRE(FNETestHooks::processP25PDUResponse(harness.traffic, SU_RSI, 5U));
|
|
|
|
KMMHello hello;
|
|
hello.setDstLLId(KMF_RSI);
|
|
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<uint8_t> encoded = encodeKMM(hello);
|
|
hello.generateMAC(tek, encoded.data());
|
|
REQUIRE(FNETestHooks::processOTARDLDPDU(harness.traffic, encoded, SU_RSI));
|
|
REQUIRE(FNETestHooks::hasOTARInboundMessageNumber(harness.traffic, SU_RSI, MN));
|
|
}
|
|
|
|
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(KMF_RSI); 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<uint8_t> kmm = encodeKMM(hello);
|
|
hello.generateMAC(tek, kmm.data());
|
|
|
|
std::vector<uint8_t> 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::setKMFServicesEnabled(harness.traffic, true);
|
|
|
|
KMMRegistrationCommand registration;
|
|
registration.setDstLLId(KMF_RSI);
|
|
registration.setSrcLLId(SU_RSI);
|
|
registration.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
registration.setKMFRSI(KMF_RSI);
|
|
uint32_t registrationResponseSize = 0U;
|
|
UInt8Array registrationResponse = FNETestHooks::processOTARKMM(harness.traffic,
|
|
encodeKMM(registration), SU_RSI, registrationResponseSize,
|
|
ALGO_UNENCRYPT, 0U, true);
|
|
REQUIRE(registrationResponse != nullptr);
|
|
std::unique_ptr<KMMFrame> decodedRegistration = KMMFactory::create(
|
|
registrationResponse.get(), registrationResponseSize);
|
|
REQUIRE(decodedRegistration != nullptr);
|
|
REQUIRE(decodedRegistration->getMessageId() == KMM_MessageType::REG_RSP);
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("P25 OTAR Rekey Commands batch four keys per KMM", "[p25][kmm][otar][rekey]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
std::vector<uint16_t> allowed;
|
|
|
|
for (uint16_t i = 0U; i < 9U; i++) {
|
|
EKCKeyItem key;
|
|
key.id(i + 1U);
|
|
key.algId(ALGO_AES_256);
|
|
key.kId((uint16_t)(0x2000U + i));
|
|
key.sln((uint16_t)(0x0100U + i));
|
|
key.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
|
|
FNETestHooks::addCryptoKey(harness.traffic, key);
|
|
allowed.push_back((uint16_t)key.kId());
|
|
}
|
|
|
|
EKCKeyItem ukek;
|
|
ukek.id(100U);
|
|
ukek.rsiId(SU_RSI);
|
|
ukek.algId(ALGO_AES_256);
|
|
ukek.kId(0x1001U);
|
|
ukek.sln(0x0002U);
|
|
ukek.keyMaterial("101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F");
|
|
FNETestHooks::addCryptoKey(harness.traffic, ukek);
|
|
harness.rid.addEntry(SU_RSI, true, "batch-test", "", true, true, allowed);
|
|
|
|
std::vector<std::vector<uint8_t>> frames =
|
|
FNETestHooks::buildOTARRekey(harness.traffic, SU_RSI, SU_RSI);
|
|
REQUIRE(frames.size() == 3U);
|
|
|
|
bool outerEncrypted = false;
|
|
uint8_t outerAlgId = ALGO_UNENCRYPT;
|
|
uint16_t outerKId = 0U;
|
|
REQUIRE(FNETestHooks::resolveOTARResponseSecurity(harness.traffic, frames[0U],
|
|
outerEncrypted, outerAlgId, outerKId));
|
|
CHECK(outerEncrypted);
|
|
CHECK(outerAlgId == ALGO_AES_256);
|
|
CHECK(outerKId == allowed[0U]);
|
|
|
|
const uint32_t expectedCounts[] = { 4U, 4U, 1U };
|
|
for (size_t i = 0U; i < frames.size(); i++) {
|
|
REQUIRE(frames[i].size() >= 3U);
|
|
const uint32_t declaredLength = (((uint32_t)frames[i][1U] << 8U) | frames[i][2U]) + 3U;
|
|
REQUIRE(declaredLength == frames[i].size());
|
|
// The bounded air-facing factory deliberately rejects nested-count
|
|
// KMMs until it has a dedicated structural validator. This is a
|
|
// trusted, locally encoded RK3, so use the normal typed decoder here.
|
|
std::unique_ptr<KMMFrame> decoded = KMMFactory::create(frames[i].data());
|
|
REQUIRE(decoded != nullptr);
|
|
KMMRekeyCommand* rekey = dynamic_cast<KMMRekeyCommand*>(decoded.get());
|
|
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 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);
|
|
}
|
|
}
|
|
|
|
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<uint8_t> encoded = FNETestHooks::buildOTARChangeover(harness.traffic,
|
|
SU_RSI, SU_RSI, SUPERSEDED_KEYSET, ACTIVE_KEYSET);
|
|
REQUIRE_FALSE(encoded.empty());
|
|
|
|
std::unique_ptr<KMMFrame> base = KMMFactory::create(encoded.data(), (uint32_t)encoded.size());
|
|
REQUIRE(base != nullptr);
|
|
KMMChangeover* changeover = dynamic_cast<KMMChangeover*>(base.get());
|
|
REQUIRE(changeover != nullptr);
|
|
CHECK(changeover->getMessageId() == KMM_MessageType::CHANGEOVER_CMD);
|
|
CHECK(changeover->getResponseKind() == KMM_ResponseKind::IMMEDIATE);
|
|
CHECK(changeover->getSrcLLId() == KMF_RSI);
|
|
CHECK(changeover->getDstLLId() == SU_RSI);
|
|
CHECK(changeover->getSupersededKeysetId() == SUPERSEDED_KEYSET);
|
|
CHECK(changeover->getActiveKeysetId() == ACTIVE_KEYSET);
|
|
CHECK(changeover->getHasMessageNumber());
|
|
CHECK(changeover->getMessageNumber() == 3U);
|
|
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);
|
|
}
|
|
|
|
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]")
|
|
{
|
|
KMMFNEHarness harness;
|
|
constexpr uint32_t SU_RSI = 0x654321U;
|
|
FNETestHooks::setKMFServicesEnabled(harness.traffic, true);
|
|
|
|
int probe = ::socket(AF_INET, SOCK_DGRAM, 0);
|
|
REQUIRE(probe >= 0);
|
|
sockaddr_in probeAddress = {};
|
|
probeAddress.sin_family = AF_INET;
|
|
probeAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
probeAddress.sin_port = 0U;
|
|
REQUIRE(::bind(probe, reinterpret_cast<sockaddr*>(&probeAddress), sizeof(probeAddress)) == 0);
|
|
socklen_t probeLength = sizeof(probeAddress);
|
|
REQUIRE(::getsockname(probe, reinterpret_cast<sockaddr*>(&probeAddress), &probeLength) == 0);
|
|
const uint16_t serverPort = ntohs(probeAddress.sin_port);
|
|
::close(probe);
|
|
|
|
REQUIRE(FNETestHooks::openOTARDLI(harness.traffic, "127.0.0.1", serverPort));
|
|
|
|
int client = ::socket(AF_INET, SOCK_DGRAM, 0);
|
|
REQUIRE(client >= 0);
|
|
sockaddr_in serverAddress = {};
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
serverAddress.sin_port = htons(serverPort);
|
|
|
|
KMMRegistrationCommand registration;
|
|
registration.setDstLLId(KMF_RSI);
|
|
registration.setSrcLLId(SU_RSI);
|
|
registration.setResponseKind(KMM_ResponseKind::IMMEDIATE);
|
|
registration.setKMFRSI(KMF_RSI);
|
|
std::vector<uint8_t> registrationKMM = encodeKMM(registration);
|
|
std::vector<uint8_t> datagram(14U + registrationKMM.size(), 0U);
|
|
datagram[0U] = 0U;
|
|
datagram[1U] = MFG_STANDARD;
|
|
datagram[2U] = ALGO_UNENCRYPT;
|
|
::memcpy(datagram.data() + 14U, registrationKMM.data(), registrationKMM.size());
|
|
REQUIRE(::sendto(client, datagram.data(), datagram.size(), 0,
|
|
reinterpret_cast<sockaddr*>(&serverAddress), sizeof(serverAddress)) == (ssize_t)datagram.size());
|
|
|
|
std::vector<uint8_t> response(512U, 0U);
|
|
ssize_t responseLength = -1;
|
|
for (uint32_t attempt = 0U; attempt < 100U && responseLength < 0; ++attempt) {
|
|
FNETestHooks::clockOTARDLI(harness.traffic);
|
|
responseLength = ::recvfrom(client, response.data(), response.size(), MSG_DONTWAIT, nullptr, nullptr);
|
|
if (responseLength < 0)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
}
|
|
|
|
REQUIRE(responseLength >= 24);
|
|
response.resize((size_t)responseLength);
|
|
REQUIRE(response[0U] == 0U);
|
|
REQUIRE(response[1U] == MFG_STANDARD);
|
|
REQUIRE(response[2U] == ALGO_UNENCRYPT);
|
|
std::unique_ptr<KMMFrame> decoded = KMMFactory::create(response.data() + 14U,
|
|
(uint32_t)response.size() - 14U);
|
|
REQUIRE(decoded != nullptr);
|
|
REQUIRE(decoded->getMessageId() == KMM_MessageType::REG_RSP);
|
|
REQUIRE(decoded->getDstLLId() == SU_RSI);
|
|
|
|
::close(client);
|
|
FNETestHooks::closeOTARDLI(harness.traffic);
|
|
}
|
|
#endif
|
|
|
|
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<uint8_t> 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);
|
|
}
|