add AACA Warm-Start Command encoding, CBC-MAC authentication, and temporary TEK handling; begin rekey immediately after a valid Warm-Start acknowledgment or after the response timeout; add upstream UKEK resolution through replica masters; rename LLA key request/response packets to generalized KEK exchanges; include algorithm metadata in KEK responses and network callbacks; improve KMM validation, MAC handling, and encrypted DLD/DLI processing; advertise idle inbound-channel status symbols on outbound P25 PDUs; add Warm Start configuration, test hooks, and expanded KMM/OTAR coverage;

pull/133/head
Bryan Biedenkapp 1 month ago
parent 5d32e6fe70
commit 35a2716c07

@ -203,6 +203,8 @@ master:
# (This field *must* be 32 hex bytes in length or 64 characters
# 0 - 9, A - F.)
kmfPresharedKey: "000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F"
# Flag indicating whether or not P25 OTAR KMF services will allow a warm start rekey request.
kmfWarmStart: true
# Flag indicating whether or not verbose debug logging for P25 OTAR KMF services is enabled.
kmfDebug: false

@ -183,7 +183,39 @@ bool BaseNetwork::writeLLAKeyReq(const uint32_t srcId)
//Utils::dump("BaseNetwork::writeLLAKeyReq(), KMM Buffer", buffer, modifyKeyCmd.fullLength() + 11U);
return writeMaster({ NET_FUNC::KEY_LLA_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U);
}
/* Writes a UKEK enc. key request to the network. */
bool BaseNetwork::writeUKEKReq(const uint32_t rsi)
{
using namespace p25::defines;
using namespace p25::kmm;
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
uint8_t buffer[DATA_PACKET_LENGTH];
::memset(buffer, 0x00U, DATA_PACKET_LENGTH);
KMMModifyKey modifyKeyCmd;
modifyKeyCmd.setDstLLId(rsi);
modifyKeyCmd.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE);
modifyKeyCmd.setAlgId(ALGO_AES_256);
modifyKeyCmd.setKId(0U);
KeysetItem ks;
ks.keysetId(0U);
ks.algId(ALGO_AES_256);
ks.keyLength(P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
modifyKeyCmd.setKeysetItem(ks);
modifyKeyCmd.encode(buffer + 11U);
//Utils::dump("BaseNetwork::writeUKEKReq(), KMM Buffer", buffer, modifyKeyCmd.fullLength() + 11U);
return writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.fullLength() + 11U, RTP_END_OF_CALL_SEQ, 0U);
}
/* Writes the local activity log to the network. */

@ -477,6 +477,13 @@ namespace network
*/
bool writeLLAKeyReq(const uint32_t srcId);
/**
* @brief Writes a UKEK enc. key request to the network.
* @param rsi Source Radio ID.
* @returns bool True, if request was sent, otherwise false.
*/
bool writeUKEKReq(const uint32_t rsi);
/**
* @brief Writes the local activity log to the network.
* \code{.unparsed}

@ -74,7 +74,7 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort,
m_nxdnInCallCallback(nullptr),
m_analogInCallCallback(nullptr),
m_keyRespCallback(nullptr),
m_llaKeyRespCallback(nullptr)
m_kekKeyRespCallback(nullptr)
{
assert(!address.empty());
assert(port > 0U);
@ -350,7 +350,7 @@ void Network::clock(uint32_t ms)
{ NET_FUNC::ACK, &Network::PacketHandler::ack },
{ NET_FUNC::KEY_RSP, &Network::PacketHandler::keyResponse },
{ NET_FUNC::KEY_LLA_RSP, &Network::PacketHandler::llaKeyResponse },
{ NET_FUNC::KEY_KEK_RSP, &Network::PacketHandler::kekKeyResponse },
{ NET_FUNC::MST_DISC, &Network::PacketHandler::masterDisconnect },

@ -313,10 +313,10 @@ namespace network
*/
void setKeyResponseCallback(std::function<void(p25::kmm::KeyItem, uint8_t, uint8_t)>&& callback) { m_keyRespCallback = callback; }
/**
* @brief Helper to set the LLA encryption key response callback.
* @brief Helper to set the UKEK/LLA encryption key response callback.
* @param callback
*/
void setLLAKeyResponseCallback(std::function<void(uint32_t, p25::kmm::KeyItem, uint8_t)>&& callback) { m_llaKeyRespCallback = callback; }
void setKEKKeyResponseCallback(std::function<void(uint32_t, p25::kmm::KeyItem, uint8_t, uint8_t)>&& callback) { m_kekKeyRespCallback = callback; }
public:
/**
@ -441,10 +441,10 @@ namespace network
*/
std::function<void(p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength)> m_keyRespCallback;
/**
* @brief LLA Encryption Key Response Function Callback.
* (This is called once the master responds to a key LLA request.)
* @brief LLA/UKEK Encryption Key Response Function Callback.
* (This is called once the master responds to a key KEK request.)
*/
std::function<void(uint32_t rsi, p25::kmm::KeyItem ki, uint8_t keyLength)> m_llaKeyRespCallback;
std::function<void(uint32_t rsi, p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength)> m_kekKeyRespCallback;
using PacketHandlerFunc = bool (*)(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length);
@ -543,7 +543,7 @@ namespace network
static bool keyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length);
/**
* @brief Handles NET_FUNC::KEY_LLA_RSP packets.
* @brief Handles NET_FUNC::KEY_KEK_RSP packets.
* @param network Instance of the TrafficNetwork class.
* @param peerId Peer ID.
* @param streamId Stream ID.
@ -554,7 +554,7 @@ namespace network
* @param length Length of buffer.
* @returns bool True, if the packet was handled, otherwise false.
*/
static bool llaKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
static bool kekKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length);
/**

@ -68,8 +68,8 @@ namespace network
ACK = 0x7EU, //!< Packet Acknowledge
NAK = 0x7FU, //!< Packet Negative Acknowledge
KEY_LLA_REQ = 0x80U, //!< Encryption Key LLA Request
KEY_LLA_RSP = 0x81U, //!< Encryption Key LLA Response
KEY_KEK_REQ = 0x80U, //!< Encryption Key LLA/UKEK Request
KEY_KEK_RSP = 0x81U, //!< Encryption Key LLA/UKEK Response
RADIO_ALIAS_SYNC = 0x85U, //!< Radio Alias List Sync

@ -18,9 +18,9 @@ using namespace network;
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEY_LLA_RSP packets. */
/* Handles NET_FUNC::KEY_KEK_RSP packets. */
bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
bool Network::PacketHandler::kekKeyResponse(Network* network, uint32_t peerId, uint32_t streamId, uint64_t now,
const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader, const uint8_t* buffer, int length)
{
(void)peerId;
@ -69,6 +69,8 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u
break;
case P25DEF::ALGO_AES_256:
keyLength = P25DEF::MAX_ENC_KEY_LENGTH_BYTES;
break;
default:
LogWarning(LOG_NET, "PEER %u, unknown algorithm ID $%02X, unable to determine key length", network->m_peerId, ks.algId());
break;
@ -85,7 +87,7 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u
}
else {
if (modifyKey->getDecryptInfoFmt() == KMM_DECRYPT_PEER_ENC) {
LogInfoEx(LOG_NET, "PEER %u, received encrypted LLA enc. key, but no preshared key available, algId = $%02X, kID = $%04X", network->m_peerId,
LogInfoEx(LOG_NET, "PEER %u, received encrypted UKEK/LLA enc. key, but no preshared key available, algId = $%02X, kID = $%04X", network->m_peerId,
ks.algId(), ki.kId());
break;
}
@ -93,8 +95,8 @@ bool Network::PacketHandler::llaKeyResponse(Network* network, uint32_t peerId, u
}
// fire off key response callback if we have one
if (network->m_llaKeyRespCallback != nullptr) {
network->m_llaKeyRespCallback(modifyKey->getDstLLId(), ki, ks.keyLength());
if (network->m_kekKeyRespCallback != nullptr) {
network->m_kekKeyRespCallback(modifyKey->getDstLLId(), ki, ks.algId(), ks.keyLength());
}
}
}

@ -517,6 +517,7 @@ namespace p25
REKEY_ACK = 0x1DU, //!< Rekey Ack
REKEY_CMD = 0x1EU, //!< Rekey Command
WARM_START_CMD = 0x20U, //!< Warm-Start Command
ZEROIZE_CMD = 0x21U, //!< Zeroize Command
ZEROIZE_RSP = 0x22U, //!< Zeroize Response

@ -94,6 +94,8 @@ std::unique_ptr<KMMFrame> KMMFactory::create(const uint8_t* data)
return decode(new KMMRekeyAck(), data);
case KMM_MessageType::REKEY_CMD:
return decode(new KMMRekeyCommand(), data);
case KMM_MessageType::WARM_START_CMD:
return decode(new KMMWarmStartCommand(), data);
case KMM_MessageType::UNABLE_TO_DECRYPT:
return decode(new KMMUnableToDecrypt(), data);
default:
@ -161,6 +163,32 @@ std::unique_ptr<KMMFrame> KMMFactory::create(const uint8_t* data, uint32_t len)
if (available < 2U || available < 2U + (uint32_t)data[body + 1U] * 4U)
return nullptr;
break;
case KMM_MessageType::WARM_START_CMD:
{
if (available < 11U || mnCode != 2U || macType != KMM_MAC::ENH_MAC ||
((data[3U] >> 6U) & 0x03U) != KMM_ResponseKind::IMMEDIATE)
return nullptr;
const uint8_t decryptFormat = data[body];
if (decryptFormat != KMM_DECRYPT_INSTRUCT_NONE && decryptFormat != KMM_DECRYPT_INSTRUCT_MI)
return nullptr;
const uint32_t miLength = decryptFormat == KMM_DECRYPT_INSTRUCT_MI ? MI_LENGTH_BYTES : 0U;
if (available < 11U + miLength)
return nullptr;
const uint32_t keyLengthOffset = body + 4U + miLength;
const uint8_t keyLength = data[keyLengthOffset];
const uint32_t keyFormatOffset = keyLengthOffset + 2U;
const uint16_t sln = (uint16_t)(((uint16_t)data[keyFormatOffset + 1U] << 8U) |
data[keyFormatOffset + 2U]);
const uint16_t temporaryKId = (uint16_t)(((uint16_t)data[keyFormatOffset + 3U] << 8U) |
data[keyFormatOffset + 4U]);
if (data[body + 1U] != ALGO_AES_256 || keyLength != P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES ||
data[keyLengthOffset + 1U] != ALGO_AES_256 || temporaryKId == 0U ||
available != 11U + miLength + keyLength ||
(data[keyFormatOffset] & 0xE0U) != KMM_KEY_FORMAT_TEK ||
(data[keyFormatOffset] & 0x1FU) != 0U || sln != 0U)
return nullptr;
}
break;
case KMM_MessageType::NO_SERVICE:
case KMM_MessageType::ZEROIZE_CMD:
case KMM_MessageType::ZEROIZE_RSP:

@ -37,6 +37,7 @@
#include "common/p25/kmm/KMMRekeyAck.h"
#include "common/p25/kmm/KMMRekeyCommand.h"
#include "common/p25/kmm/KMMUnableToDecrypt.h"
#include "common/p25/kmm/KMMWarmStartCommand.h"
#include "common/p25/kmm/KMMZeroize.h"
namespace p25

@ -78,7 +78,7 @@ void KMMFrame::generateMAC(uint8_t* kek, uint8_t* data)
return;
}
if (m_macKId == 0U) {
if (m_macKId == 0U && m_messageId != KMM_MessageType::WARM_START_CMD) {
::LogError(LOG_P25, "KMMFrame::generateMAC(), MAC key ID is not set, aborting MAC signing");
return;
}

@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
#include "p25/P25Defines.h"
#include "p25/kmm/KMMWarmStartCommand.h"
using namespace p25::defines;
using namespace p25::kmm;
#include <cassert>
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the KMMWarmStartCommand class. */
KMMWarmStartCommand::KMMWarmStartCommand() : KMMFrame(),
m_decryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE),
m_kekAlgId(ALGO_UNENCRYPT),
m_kekKId(0U),
m_keyLength(0U),
m_tekAlgId(ALGO_UNENCRYPT),
m_key(),
m_mi()
{
m_messageId = KMM_MessageType::WARM_START_CMD;
m_respKind = KMM_ResponseKind::IMMEDIATE;
::memset(m_mi, 0x00U, sizeof(m_mi));
}
/* Gets the byte length of this KMMWarmStartCommand. */
uint32_t KMMWarmStartCommand::length() const
{
return KMMFrame::length() + 11U + (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI ? P25DEF::MI_LENGTH_BYTES : 0U) +
m_keyLength;
}
/* Decodes a KMM Warm-Start Command from the given data buffer. */
bool KMMWarmStartCommand::decode(const uint8_t* data)
{
assert(data != nullptr);
KMMFrame::decodeHeader(data);
uint32_t offset = 10U + m_bodyOffset;
m_decryptInfoFmt = data[offset++]; // Decryption information format
m_kekAlgId = data[offset++]; // KEK algorithm identifier
m_kekKId = GET_UINT16(data, offset); offset += 2U; // KEK key identifier
if (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI) {
::memcpy(m_mi, data + offset, sizeof(m_mi));
offset += sizeof(m_mi);
}
m_keyLength = data[offset++];
m_tekAlgId = data[offset++];
m_key.keyFormat(data[offset++] & 0xE0U);
uint16_t sln = GET_UINT16(data, offset); offset += 2U;
m_key.sln(sln);
uint16_t kid = GET_UINT16(data, offset); offset += 2U;
m_key.kId(kid);
m_key.setKey(data + offset, m_keyLength);
return true;
}
/* Encodes this KMM Warm-Start Command into the given data buffer. */
void KMMWarmStartCommand::encode(uint8_t* data)
{
assert(data != nullptr);
m_messageLength = length();
KMMFrame::encodeHeader(data);
uint32_t offset = 10U + m_bodyOffset;
data[offset++] = m_decryptInfoFmt; // Decryption information format
data[offset++] = m_kekAlgId; // KEK algorithm identifier
SET_UINT16(m_kekKId, data, offset); offset += 2U; // KEK key identifier
if (m_decryptInfoFmt == KMM_DECRYPT_INSTRUCT_MI) {
::memcpy(data + offset, m_mi, sizeof(m_mi));
offset += sizeof(m_mi);
}
data[offset++] = m_keyLength; // TEK key length in bytes
data[offset++] = m_tekAlgId; // TEK algorithm identifier
data[offset++] = m_key.keyFormat(); // TEK key format
uint16_t sln = m_key.sln();
SET_UINT16(sln, data, offset);
offset += 2U;
uint16_t kid = m_key.kId();
SET_UINT16(kid, data, offset);
offset += 2U;
m_key.getKey(data + offset);
}
/* Returns a string representation of this KMM Warm-Start Command. */
std::string KMMWarmStartCommand::toString()
{
return std::string("KMM, WARM_START_CMD (Warm-Start Command)");
}
/*
** Encryption data
*/
/* Sets the encryption message indicator. */
void KMMWarmStartCommand::setMI(const uint8_t* mi)
{
assert(mi != nullptr);
::memcpy(m_mi, mi, sizeof(m_mi));
}
/* Gets the encryption message indicator. */
void KMMWarmStartCommand::getMI(uint8_t* mi) const
{
assert(mi != nullptr);
::memcpy(mi, m_mi, sizeof(m_mi));
}
// ---------------------------------------------------------------------------
// Protected Class Members
// ---------------------------------------------------------------------------
/* Internal helper to copy the the class. */
void KMMWarmStartCommand::copy(const KMMWarmStartCommand& data)
{
KMMFrame::copy(data);
m_decryptInfoFmt = data.m_decryptInfoFmt;
m_kekAlgId = data.m_kekAlgId;
m_kekKId = data.m_kekKId;
m_keyLength = data.m_keyLength;
m_tekAlgId = data.m_tekAlgId;
m_key = data.m_key;
if (data.m_mi != nullptr) {
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
::memcpy(m_mi, data.m_mi, MI_LENGTH_BYTES);
}
}

@ -0,0 +1,119 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file KMMWarmStartCommand.h
* @ingroup p25_kmm
* @file KMMWarmStartCommand.cpp
* @ingroup p25_kmm
*/
#if !defined(__P25_KMM__KMM_WARM_START_COMMAND_H__)
#define __P25_KMM__KMM_WARM_START_COMMAND_H__
#include "common/Defines.h"
#include "common/p25/kmm/KMMFrame.h"
#include "common/p25/kmm/KeysetItem.h"
namespace p25
{
namespace kmm
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents a KMM Warm Start Command frame.
* @ingroup p25_kmm
*/
class DVM_COMMON_API KMMWarmStartCommand : public KMMFrame {
public:
/**
* @brief Initializes a new instance of the KMMWarmStartCommand class.
*/
KMMWarmStartCommand();
/**
* @brief Finalizes a instance of the KMMWarmStartCommand class.
*/
~KMMWarmStartCommand() = default;
/**
* @brief Gets the byte length of this KMMFrame.
* @return uint32_t Length of KMMFrame.
*/
uint32_t length() const override;
/**
* @brief Decode a KMM Warm-Start.
* @param[in] data Buffer containing KMM frame data to decode.
* @returns bool True, if decoded, otherwise false.
*/
bool decode(const uint8_t* data) override;
/**
* @brief Encode a KMM Warm-Start.
* @param[out] data Buffer to encode KMM frame data to.
*/
void encode(uint8_t* data) override;
/**
* @brief Returns a string that represents the current KMM frame.
* @returns std::string String representation of the KMM frame.
*/
std::string toString() override;
/** @name Encryption data */
/**
* @brief Sets the encryption message indicator.
* @param[in] mi Buffer containing the 9-byte Message Indicator.
*/
void setMI(const uint8_t* mi);
/**
* @brief Gets the encryption message indicator.
* @param[out] mi Buffer containing the 9-byte Message Indicator.
*/
void getMI(uint8_t* mi) const;
/** @} */
public:
/**
* @brief Gets or sets the decryption information format.
*/
DECLARE_PROPERTY(uint8_t, decryptInfoFmt, DecryptInfoFmt);
/**
* @brief Gets or sets the KEK algorithm identifier.
*/
DECLARE_PROPERTY(uint8_t, kekAlgId, KEKAlgId);
/**
* @brief Gets or sets the KEK key identifier.
*/
DECLARE_PROPERTY(uint16_t, kekKId, KEKKId);
/**
* @brief Gets or sets the key length.
*/
DECLARE_PROPERTY(uint8_t, keyLength, KeyLength);
/**
* @brief Gets or sets the TEK algorithm identifier.
*/
DECLARE_PROPERTY(uint8_t, tekAlgId, TEKAlgId);
/**
* @brief Gets or sets the key item.
*/
DECLARE_PROPERTY(KeyItem, key, Key);
DECLARE_COPY(KMMWarmStartCommand);
private:
uint8_t m_mi[P25DEF::MI_LENGTH_BYTES];
};
} // namespace kmm
} // namespace p25
#endif // __P25_KMM__KMM_WARM_START_COMMAND_H__

@ -258,6 +258,26 @@ void CryptoContainer::addEntry(EKCKeyItem key)
}
}
/* Adds or replaces a User Key Encryption Key by RSI and KID. */
void CryptoContainer::addUKEK(EKCKeyItem key)
{
if (key.isInvalid())
return;
std::lock_guard<std::mutex> lock(s_mutex);
auto it = std::find_if(m_ukeks.begin(), m_ukeks.end(),
[&](const EKCKeyItem& item)
{
return item.rsiId() == key.rsiId() && item.kId() == key.kId();
});
if (it != m_ukeks.end()) {
*it = key;
} else {
m_ukeks.push_back(key);
}
}
/* Erases an existing entry from the lookup table by the specified unique ID. */
void CryptoContainer::eraseEntry(uint32_t id)

@ -215,6 +215,11 @@ public:
* @param key Key Item.
*/
void addEntry(EKCKeyItem key);
/**
* @brief Adds or replaces a User Key Encryption Key.
* @param key Key Item representing the UKEK.
*/
void addUKEK(EKCKeyItem key);
/**
* @brief Erases an existing entry from the lookup table by the specified unique ID.
* @param id Unique ID to erase.

File diff suppressed because it is too large Load Diff

@ -25,11 +25,17 @@ class FNETestHooks;
#include "common/p25/P25Defines.h"
#include "common/p25/Crypto.h"
#include "common/p25/kmm/KMMFrame.h"
#include "common/p25/kmm/KeysetItem.h"
#include "common/network/udp/Socket.h"
#include "common/network/RawFrameQueue.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/packetdata/P25PacketData.h"
#include <condition_variable>
#include <array>
#include <mutex>
#include <unordered_map>
namespace network
{
// ---------------------------------------------------------------------------
@ -105,6 +111,23 @@ namespace network
*/
void close();
/**
* @brief Supplies a TEK returned asynchronously by an upstream master.
* @param key The TEK key item.
* @param algId The algorithm identifier for the TEK.
* @param keyLength The length of the TEK key in bytes.
*
*/
void cacheUpstreamTEK(const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength);
/**
* @brief Supplies a UKEK returned asynchronously by an upstream master.
* @param rsi The Radio Subscriber Identifier associated with the UKEK.
* @param key The UKEK key item.
* @param algId The algorithm identifier for the UKEK.
* @param keyLength The length of the UKEK key in bytes.
*/
void cacheUpstreamUKEK(uint32_t rsi, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength);
private:
/**
* @brief Context for KMM message authentication.
@ -118,6 +141,50 @@ namespace network
uint16_t format = P25DEF::KMM_MAC_FORMAT_CBC; //!< MAC format used for the KMM message.
};
/**
* @brief Represents an upstream key received from the master.
*/
struct UpstreamKey {
p25::kmm::KeyItem key; //!< The key item for the upstream key.
uint8_t algorithmId = P25DEF::ALGO_UNENCRYPT; //!< Algorithm ID for the upstream key.
uint8_t keyLength = 0U; //!< Length of the upstream key in bytes.
uint64_t receivedAt = 0U; //!< Timestamp when the upstream key was received.
};
/**
* @brief Represents the state of a warm start transaction.
*/
enum class WarmStartState : uint8_t
{
/**
* @brief Waiting for the warm start acknowledgment from the master.
*/
WAIT_WARM_ACK,
/**
* @brief Waiting for the rekey acknowledgment from the master.
*/
WAIT_REKEY_ACK
};
/**
* @brief Represents a warm start transaction.
*/
struct WarmStartTransaction {
WarmStartState state = WarmStartState::WAIT_WARM_ACK; //!< Current state of the warm start transaction.
uint32_t llId = 0U; //!< Logical link ID associated with the warm start transaction.
uint32_t rsi = 0U; //!< Subscriber RSI associated with the warm start transaction.
uint16_t temporaryKId = 0U; //!< Temporary key ID used in the warm start transaction.
uint16_t warmStartMN = 0U; //!< Message number for the warm start transaction.
uint64_t deadline = 0U; //!< Deadline for the warm start transaction.
bool dataLinkIndependent = false; //!< Indicates if the warm start transaction is data link independent.
bool hasDLIEndpoint = false; //!< Indicates if the warm start transaction has a data link independent endpoint.
sockaddr_storage dliAddress{}; //!< Address of the data link independent endpoint.
uint32_t dliAddressLength = 0U; //!< Length of the data link independent endpoint address.
std::array<uint8_t, P25DEF::MAX_ENC_KEY_LENGTH_BYTES> temporaryTEK{}; //!< Temporary TEK used in the warm start transaction.
std::vector<uint16_t> permittedFinalKIds; //!< Permitted final key IDs for the warm start transaction.
std::vector<uint16_t> pendingRekeyMNs; //!< Pending rekey message numbers for the warm start transaction.
};
network::udp::Socket* m_socket;
network::RawFrameQueue* m_frameQueue;
@ -131,17 +198,53 @@ namespace network
concurrent::unordered_map<uint32_t, uint64_t> m_rsiInboundFingerprint;
concurrent::unordered_map<uint32_t, bool> m_dliRegistered;
mutable std::mutex m_outboundMessageNumberMutex;
bool m_allowNoUKEKRekey;
bool m_debug;
bool m_verbose;
mutable std::mutex m_upstreamKeyMutex;
mutable std::condition_variable m_upstreamKeyReady;
mutable std::unordered_map<uint32_t, UpstreamKey> m_upstreamTEKs;
mutable std::unordered_map<uint32_t, UpstreamKey> m_upstreamUKEKs;
mutable std::mutex m_warmStartMutex;
mutable std::unordered_map<uint32_t, WarmStartTransaction> m_warmStartTransactions;
/**
* @brief Atomically reserves one or more outbound KMM message numbers.
* @param[in] rsi Radio Subscriber Identifier (RSI) for which to reserve message numbers.
* @param[in] count Number of outbound message numbers to reserve.
* @returns The first reserved outbound message number.
*
*/
uint16_t reserveOutboundMessageNumbers(uint32_t rsi, uint16_t count = 1U);
/**
* @brief Entry point to process a given network packet.
* @param arg Instance of the OTARPacketRequest structure.
*/
static void taskNetworkRx(OTARPacketRequest* req);
/**
* @brief Helper used to process KMM frames.
* @param[in] data Network data buffer.
* @param len Length of data.
* @param encrypted Flag indicating whether or not the KMM frame is encrypted.
* @param llId Logical Link ID.
* @param[out] payloadSize Size of the returned KMM payload.
* @param algoId Algorithm ID.
* @param kid Key ID.
* @param mi Message Indicator.
* @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent.
* @param additionalResponses Optional container for additional KMM responses.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array processKMM(const uint8_t* data, uint32_t len, uint32_t llId, bool encrypted, uint32_t* payloadSize,
uint8_t algoId = P25DEF::ALGO_UNENCRYPT, uint16_t kid = 0U, const uint8_t* mi = nullptr,
bool dataLinkIndependent = false, std::vector<std::vector<uint8_t>>* additionalResponses = nullptr);
/**
* @brief Encrypt/decrypt KMM frame.
* @param[in] algoId Algorithm ID.
@ -154,6 +257,35 @@ namespace network
*/
UInt8Array cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, const uint8_t* buffer, uint32_t len, bool encrypt = false);
/**
* @brief Resolves a TEK locally, then from an upstream replica master.
* @param[in] kid Key ID.
* @param[in] algorithmId Algorithm ID.
* @param[in] requestingRSI Requesting RSI (default is 0U).
* @return Resolved TEK as an EKCKeyItem.
*
*/
EKCKeyItem resolveTEK(uint16_t kid, uint8_t algorithmId, uint32_t requestingRSI = 0U) const;
/**
* @brief Resolves a UKEK locally, then from an upstream replica master.
* @param[in] rsi Requesting RSI (default is 0U).
* @return Resolved UKEK as an EKCKeyItem.
*/
EKCKeyItem resolveUKEK(uint32_t rsi) const;
/**
* @brief Resolves a warm start TEK locally, then from an upstream replica master.
* @param[in] kid Key ID.
* @param[in] rsi Requesting RSI (default is 0U).
* @return Resolved warm start TEK as an EKCKeyItem.
*/
EKCKeyItem resolveWarmStartTEK(uint16_t kid, uint32_t rsi = 0U) const;
/**
* @brief Erases a warm start transaction for the given requesting RSI.
* @param[in] rsi Requesting RSI.
*/
void eraseWarmStart(uint32_t rsi);
/**
* @brief Resolves the required outer-encryption context for a locally generated KMM response.
* @param[in] data Encoded plaintext KMM response.
@ -167,35 +299,29 @@ namespace network
uint8_t& algoId, uint16_t& kid) const;
/**
* @brief Helper used to process KMM frames.
* @param[in] data Network data buffer.
* @param len Length of data.
* @param encrypted Flag indicating whether or not the KMM frame is encrypted.
* @param llId Logical Link ID.
* @brief Helper used to return a Rekey-Command KMM to the calling SU.
* @param llId Logical Link Address.
* @param kmmRSI KMM Radio Set Identifier.
* @param flags Hello KMM flags.
* @param[out] payloadSize Size of the returned KMM payload.
* @param algoId Algorithm ID.
* @param kid Key ID.
* @param mi Message Indicator.
* @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent.
* @param auth Authentication context containing MAC/MN fields to mirror.
* @param additionalResponses Optional container for additional KMM responses.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array processKMM(const uint8_t* data, uint32_t len, uint32_t llId, bool encrypted, uint32_t* payloadSize,
uint8_t algoId = P25DEF::ALGO_UNENCRYPT, uint16_t kid = 0U, const uint8_t* mi = nullptr,
bool dataLinkIndependent = false, std::vector<std::vector<uint8_t>>* additionalResponses = nullptr);
UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize,
const KMMAuthContext& auth, std::vector<std::vector<uint8_t>>* additionalResponses = nullptr,
bool useWarmStartTEK = false);
/**
* @brief Helper used to return a Rekey-Command KMM to the calling SU.
* @brief Builds and records an RK3 Warm-Start transaction.
* @param llId Logical Link Address.
* @param kmmRSI KMM Radio Set Identifier.
* @param flags Hello KMM flags.
* @param[out] payloadSize Size of the returned KMM payload.
* @param auth Authentication context containing MAC/MN fields to mirror.
* @param additionalResponses Optional container for additional KMM responses.
* @param dataLinkIndependent Flag indicating whether the KMM frame is data-link independent.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize,
const KMMAuthContext& auth, std::vector<std::vector<uint8_t>>* additionalResponses = nullptr);
UInt8Array write_KMM_WarmStart_Command(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize,
bool dataLinkIndependent = false);
/**
* @brief Builds an authenticated Changeover-Command KMM for an SU.

@ -50,7 +50,7 @@ const uint32_t FIXED_HA_UPDATE_INTERVAL = 30U; // 30s
// ---------------------------------------------------------------------------
std::timed_mutex TrafficNetwork::s_keyQueueMutex;
std::timed_mutex TrafficNetwork::s_llaKeyQueueMutex;
std::timed_mutex TrafficNetwork::s_kekKeyQueueMutex;
std::array<std::mutex, PEER_STATE_LOCK_STRIPES> TrafficNetwork::s_peerStateLocks;
@ -92,6 +92,7 @@ TrafficNetwork::TrafficNetwork(HostFNE* host, const std::string& address, uint16
m_kmfAllowRID0(false),
m_kmfEncKeyRequest(false),
m_kmfPresharedKey(nullptr),
m_kmfWarmStart(true),
m_ridLookup(nullptr),
m_ridAliasLookup(nullptr),
m_tidLookup(nullptr),
@ -104,7 +105,7 @@ TrafficNetwork::TrafficNetwork(HostFNE* host, const std::string& address, uint16
m_peerAffiliations(),
m_ccPeerMap(),
m_peerReplicaKeyQueue(),
m_peerReplicaLLAKeyQueue(),
m_peerReplicaKEKKeyQueue(),
m_globalAff(nullptr),
m_treeRoot(nullptr),
m_treeLock(),
@ -336,6 +337,7 @@ void TrafficNetwork::setOptions(yaml::Node& conf, bool printOptions)
LogWarning(LOG_MASTER, "FNE is compiled without OpenSSL support, KMF services are unavailable.");
#endif // ENABLE_SSL
m_kmfAllowRID0 = conf["kmfAllowRID0"].as<bool>(false);
m_kmfWarmStart = conf["kmfWarmStart"].as<bool>(true);
// scope is intentional
{
@ -529,6 +531,7 @@ void TrafficNetwork::setOptions(yaml::Node& conf, bool printOptions)
LogInfo(" P25 OTAR KMF Listening Address: %s", m_address.c_str());
LogInfo(" P25 OTAR KMF Listening Port: %u", kmfOtarPort);
LogInfo(" P25 KMF Allow RID 0 Requests: %s", m_kmfAllowRID0 ? "yes" : "no");
LogInfo(" P25 KMF Warm Start Requests Allowed: %s", m_kmfWarmStart ? "yes" : "no");
LogInfo(" P25 KMF Peer Request Encrypted: %s", m_kmfEncKeyRequest ? "yes" : "no");
if (!m_encryptedTrafficConn && !m_kmfEncKeyRequest) {
LogWarning(LOG_MASTER, "Peers can make key requests, but the encrypted traffic connection is not enabled and KMF requests are not encrypted! Key requests will be sent in the clear.");
@ -800,13 +803,15 @@ void TrafficNetwork::clock(uint32_t ms)
// perform peer replica maintainence tasks
if (peer.second->isEnabled() && peer.second->getRemotePeerId() > 0U &&
peer.second->isReplica()) {
// attach key response handlers if not already attached
if (!peer.second->getAttachedKeyRSPHandler()) {
peer.second->setAttachedKeyRSPHandler(true); // this is the only place this should happen
peer.second->setKeyResponseCallback([=](p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) {
processTEKResponse(&ki, algId, keyLength);
});
peer.second->setLLAKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t keyLength) {
processLLAResponse(srcId, &ki, keyLength);
peer.second->setKEKKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) {
processKEKResponse(srcId, &ki, algId, keyLength);
});
}
@ -1216,7 +1221,7 @@ void TrafficNetwork::taskNetworkRx(NetPacketRequest* req)
{ NET_FUNC::INCALL_CTRL, &TrafficNetwork::PacketHandler::inCallControl },
{ NET_FUNC::KEY_REQ, &TrafficNetwork::PacketHandler::keyRequest },
{ NET_FUNC::KEY_LLA_REQ, &TrafficNetwork::PacketHandler::llaKeyRequest },
{ NET_FUNC::KEY_KEK_REQ, &TrafficNetwork::PacketHandler::kekKeyRequest },
};
// dispatch to the appropriate handler based on the function opcode
@ -2961,6 +2966,64 @@ bool TrafficNetwork::writePeerNAK(uint32_t peerId, const char* tag, NET_CONN_NAK
{ NET_FUNC::NAK, NET_SUBFUNC::NOP }, RTP_END_OF_CALL_SEQ, addr, addrLen);
}
/*
** Key Request
*/
/* Sends a TEK query to each enabled upstream replica master. */
bool TrafficNetwork::requestUpstreamTEK(uint16_t kid, uint8_t algId, uint32_t requestingRSI)
{
bool sent = false;
for (auto& peer : m_host->m_peerNetworks) {
PeerNetwork* upstream = peer.second;
if (upstream == nullptr || !upstream->isEnabled() || !upstream->isReplica())
continue;
// attach key response handlers if not already attached
if (!upstream->getAttachedKeyRSPHandler()) {
upstream->setAttachedKeyRSPHandler(true);
upstream->setKeyResponseCallback([this](p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) {
processTEKResponse(&ki, responseAlgId, keyLength);
});
upstream->setKEKKeyResponseCallback([this](uint32_t rsi, p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) {
processKEKResponse(rsi, &ki, responseAlgId, keyLength);
});
}
sent = upstream->writeKeyReq(kid, algId, requestingRSI) || sent;
}
return sent;
}
/* Sends a UKEK query to each enabled upstream replica master. */
bool TrafficNetwork::requestUpstreamUKEK(uint32_t rsi)
{
bool sent = false;
for (auto& peer : m_host->m_peerNetworks) {
PeerNetwork* upstream = peer.second;
if (upstream == nullptr || !upstream->isEnabled() || !upstream->isReplica())
continue;
// attach key response handlers if not already attached
if (!upstream->getAttachedKeyRSPHandler()) {
upstream->setAttachedKeyRSPHandler(true);
upstream->setKeyResponseCallback([this](p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) {
processTEKResponse(&ki, responseAlgId, keyLength);
});
upstream->setKEKKeyResponseCallback([this](uint32_t responseRSI, p25::kmm::KeyItem ki, uint8_t responseAlgId, uint8_t keyLength) {
processKEKResponse(responseRSI, &ki, responseAlgId, keyLength);
});
}
sent = upstream->writeUKEKReq(rsi) || sent;
}
return sent;
}
/*
** Internal KMM Callback.
*/
@ -2977,6 +3040,9 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId,
LogInfoEx(LOG_PEER, "upstream master enc. key, algId = $%02X, kID = $%04X", algId, rspKi->kId());
if (m_p25OTARService != nullptr)
m_p25OTARService->cacheUpstreamTEK(*rspKi, algId, keyLength);
s_keyQueueMutex.lock();
std::vector<uint32_t> peersToRemove;
@ -3033,9 +3099,9 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId,
s_keyQueueMutex.unlock();
}
/* Helper to process a FNE KMM LLA response. */
/* Helper to process a FNE KMM UKEK/LLA response. */
void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi, uint8_t keyLength)
void TrafficNetwork::processKEKResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi, uint8_t algId, uint8_t keyLength)
{
using namespace p25::defines;
using namespace p25::kmm;
@ -3045,12 +3111,16 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi
LogInfoEx(LOG_PEER, "upstream master LLA enc. key, rsi = %u", srcId);
s_llaKeyQueueMutex.lock();
if (algId == ALGO_AES_256 && m_p25OTARService != nullptr)
m_p25OTARService->cacheUpstreamUKEK(srcId, *rspKi, algId, keyLength);
s_kekKeyQueueMutex.lock();
std::vector<uint32_t> peersToRemove;
for (auto entry : m_peerReplicaLLAKeyQueue) {
uint32_t requestingRid = entry.second;
if (requestingRid == srcId) {
for (auto entry : m_peerReplicaKEKKeyQueue) {
uint8_t requestedAlgId = (uint8_t)(entry.second >> 24U);
uint32_t requestingRid = entry.second & 0xFFFFFFU;
if (requestingRid == srcId && requestedAlgId == algId) {
uint32_t peerId = entry.first;
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
@ -3058,8 +3128,8 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi
rspKi->getKey(key);
if (m_debug) {
LogDebugEx(LOG_HOST, "TrafficNetwork::processLLAResponse()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::processLLAResponse(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
LogDebugEx(LOG_HOST, "TrafficNetwork::processKEKResponse()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::processKEKResponse(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
// build response buffer
@ -3068,14 +3138,14 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi
KMMModifyKey modifyKeyRsp = KMMModifyKey();
modifyKeyRsp.setDecryptInfoFmt(m_kmfEncKeyRequest ? KMM_DECRYPT_PEER_ENC : KMM_DECRYPT_INSTRUCT_NONE);
modifyKeyRsp.setAlgId(ALGO_AES_128);
modifyKeyRsp.setAlgId(algId);
modifyKeyRsp.setKId(0U);
modifyKeyRsp.setSrcLLId(WUID_FNE);
modifyKeyRsp.setDstLLId(srcId);
KeysetItem ks = KeysetItem();
ks.keysetId(1U);
ks.algId(ALGO_AES_128);
ks.algId(algId);
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
@ -3089,18 +3159,18 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi
modifyKeyRsp.encode(buffer + 11U);
writePeer(peerId, m_peerId, { NET_FUNC::KEY_LLA_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U,
writePeer(peerId, m_peerId, { NET_FUNC::KEY_KEK_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U,
RTP_END_OF_CALL_SEQ, createStreamId());
peersToRemove.push_back(peerId);
} else {
LogError(LOG_PEER, "upstream master LLA enc. key, peerId = %u, requestingRSI = %u, rsi = %u -- mismatch!", entry.first, requestingRid, srcId);
LogError(LOG_PEER, "upstream master UKEK/LLA enc. key, peerId = %u, requestingRSI = %u, rsi = %u -- mismatch!", entry.first, requestingRid, srcId);
}
}
// remove peers who were sent keys
for (auto& peerId : peersToRemove)
m_peerReplicaLLAKeyQueue.erase(peerId);
m_peerReplicaKEKKeyQueue.erase(peerId);
s_llaKeyQueueMutex.unlock();
s_kekKeyQueueMutex.unlock();
}

@ -375,6 +375,7 @@ namespace network
bool m_kmfAllowRID0;
bool m_kmfEncKeyRequest;
uint8_t* m_kmfPresharedKey;
bool m_kmfWarmStart;
lookups::RadioIdLookup* m_ridLookup;
lookups::RadioAliasLookup* m_ridAliasLookup;
@ -395,8 +396,8 @@ namespace network
concurrent::shared_unordered_map<uint32_t, std::vector<uint32_t>> m_ccPeerMap;
static std::timed_mutex s_keyQueueMutex;
std::unordered_map<uint32_t, uint16_t> m_peerReplicaKeyQueue;
static std::timed_mutex s_llaKeyQueueMutex;
std::unordered_map<uint32_t, uint32_t> m_peerReplicaLLAKeyQueue;
static std::timed_mutex s_kekKeyQueueMutex;
std::unordered_map<uint32_t, uint32_t> m_peerReplicaKEKKeyQueue; // value packs algId in high byte, and RSI in low 24 bits
fne_lookups::AffiliationLookup* m_globalAff;
@ -623,7 +624,7 @@ namespace network
*/
static void keyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::KEY_LLA_REQ packets.
* @brief Handles NET_FUNC::KEY_KEK_REQ packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
@ -631,7 +632,7 @@ namespace network
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
static void kekKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
};
/**
@ -808,6 +809,21 @@ namespace network
*/
void replicatePatchStatus(json::object obj, uint32_t exceptPeerId = 0U);
/**
* @brief Serializes and queues a patch status transfer payload.
* @param connection Destination connection.
* @param obj Patch status JSON payload.
* @returns bool True, if message was queued, otherwise false.
*/
bool writePatchStatusPayload(FNEPeerConnection* connection, json::object obj);
/**
* @brief Serializes and queues a patch status replication payload.
* @param connection Destination neighbor connection.
* @param obj Patch status JSON payload.
* @returns bool True, if message was queued, otherwise false.
*/
bool writePatchStatusReplicationPayload(FNEPeerConnection* connection, json::object obj);
/*
** ACL Message Writing
*/
@ -1072,20 +1088,22 @@ namespace network
*/
bool writePeerNAK(uint32_t peerId, const char* tag, NET_CONN_NAK_REASON reason, sockaddr_storage& addr, uint32_t addrLen);
/**
* @brief Serializes and queues a patch status transfer payload.
* @param connection Destination connection.
* @param obj Patch status JSON payload.
* @returns bool True, if message was queued, otherwise false.
/*
** Key Request
*/
/**
* @brief Sends a TEK query to each enabled upstream replica master.
* @param kid Key ID for the TEK query.
* @param algId Algorithm ID for the TEK query.
* @param requestingRSI RSI of the requesting entity.
*/
bool writePatchStatusPayload(FNEPeerConnection* connection, json::object obj);
/**
* @brief Serializes and queues a patch status replication payload.
* @param connection Destination neighbor connection.
* @param obj Patch status JSON payload.
* @returns bool True, if message was queued, otherwise false.
bool requestUpstreamTEK(uint16_t kid, uint8_t algId, uint32_t requestingRSI);
/**
* @brief Sends a UKEK query to each enabled upstream replica master.
* @param rsi RSI of the requesting entity.
*/
bool writePatchStatusReplicationPayload(FNEPeerConnection* connection, json::object obj);
bool requestUpstreamUKEK(uint32_t rsi);
/*
** Internal KMM Callback.
@ -1095,17 +1113,18 @@ namespace network
* @brief Helper to process a FNE KMM TEK response.
* @param ki Key Item.
* @param algId Algorithm ID.
* @param algId Algorithm ID.
* @param keyLength Length of key in bytes.
*/
void processTEKResponse(p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength);
/**
* @brief Helper to process a FNE KMM LLA response.
* @param srcId Source Radio ID for the LLA response.
* @brief Helper to process a FNE KMM UKEK/LLA response.
* @param srcId Source Radio ID for the UKEK/LLA response.
* @param ki Key Item.
* @param keyLength Length of key in bytes.
*/
void processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* ki, uint8_t keyLength);
void processKEKResponse(uint32_t srcId, p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength);
/*
** Metrics Helpers

@ -21,9 +21,9 @@ using namespace network;
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEY_LLA_REQ packets. */
/* Handles NET_FUNC::KEY_KEK_REQ packets. */
void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
void TrafficNetwork::PacketHandler::kekKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
using namespace p25::defines;
using namespace p25::kmm;
@ -59,18 +59,27 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa
{
KMMModifyKey* modifyKey = static_cast<KMMModifyKey*>(frame.get());
if (modifyKey->getAlgId() == ALGO_AES_128 && modifyKey->getDstLLId() > 0U) {
if ((modifyKey->getAlgId() == ALGO_AES_128 || modifyKey->getAlgId() == ALGO_AES_256) &&
modifyKey->getDstLLId() > 0U) {
if (network->m_debug)
LogDebugEx(LOG_MASTER, "TrafficNetwork::taskNetworkRx()", "PEER %u (%s) LLA enc. key request received, dstLLId = %u, algId = %u, kId = %u", peerId, connection->identWithQualifier().c_str(),
LogDebugEx(LOG_MASTER, "TrafficNetwork::taskNetworkRx()", "PEER %u (%s) UKEK/LLA enc. key request received, dstLLId = %u, algId = %u, kId = %u", peerId, connection->identWithQualifier().c_str(),
modifyKey->getDstLLId(), modifyKey->getAlgId(), modifyKey->getKId());
uint32_t requestingRid = modifyKey->getDstLLId();
LogInfoEx(LOG_MASTER, "PEER %u (%s) requested LLA enc. key, rsi = %u", peerId, connection->identWithQualifier().c_str(),
requestingRid);
// determine if the request is for a UKEK or LLA encryption key
const bool ukekRequest = modifyKey->getAlgId() == ALGO_AES_256;
if (ukekRequest && connection->peerClass() != PEER_CONN_CLASS_NEIGHBOR) {
LogError(LOG_MASTER, "PEER %u (%s) requested UKEK but is not a neighbor FNE, no response",
peerId, connection->identWithQualifier().c_str());
return;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) requested %s key, rsi = %u", peerId, connection->identWithQualifier().c_str(), ukekRequest ? "UKEK" : "LLA enc.", requestingRid);
::EKCKeyItem keyItem = network->m_cryptoLookup->findLLA(requestingRid);
if (!keyItem.isInvalid()) {
::EKCKeyItem keyItem = ukekRequest ? network->m_cryptoLookup->findUKEK(requestingRid) :
network->m_cryptoLookup->findLLA(requestingRid);
if (!keyItem.isInvalid() && keyItem.algId() == modifyKey->getAlgId()) {
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);
@ -120,7 +129,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.keyFormat(ukekRequest ? KMM_KEY_FORMAT_KEK : KMM_KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(key, keyLength);
@ -130,7 +139,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa
modifyKeyRsp.encode(buffer + 11U);
network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_LLA_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U,
network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_KEK_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.fullLength() + 11U,
RTP_END_OF_CALL_SEQ, network->createStreamId());
} else {
// attempt to forward KMM key request to replica masters
@ -141,13 +150,13 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa
LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, rsi = %u", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), requestingRid);
bool locked = network->s_llaKeyQueueMutex.try_lock_for(std::chrono::milliseconds(60));
network->m_peerReplicaLLAKeyQueue[peerId] = modifyKey->getDstLLId();
if (locked)
network->s_llaKeyQueueMutex.unlock();
{
std::lock_guard<std::timed_mutex> lock(network->s_kekKeyQueueMutex);
network->m_peerReplicaKEKKeyQueue[peerId] =
((uint32_t)modifyKey->getAlgId() << 24U) | (modifyKey->getDstLLId() & 0xFFFFFFU);
}
peer.second->writeMaster({ NET_FUNC::KEY_LLA_REQ, NET_SUBFUNC::NOP },
peer.second->writeMaster({ NET_FUNC::KEY_KEK_REQ, NET_SUBFUNC::NOP },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false);
}
}

@ -101,7 +101,7 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke
LogInfoEx(LOG_MASTER, "PEER %u (%s) requested enc. key, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
::EKCKeyItem keyItem = network->m_cryptoLookup->find(modifyKey->getKId());
if (!keyItem.isInvalid()) {
if (!keyItem.isInvalid() && keyItem.algId() == modifyKey->getAlgId()) {
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);
@ -170,11 +170,10 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke
LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
bool locked = network->s_keyQueueMutex.try_lock_for(std::chrono::milliseconds(60));
network->m_peerReplicaKeyQueue[peerId] = modifyKey->getKId();
if (locked)
network->s_keyQueueMutex.unlock();
{
std::lock_guard<std::timed_mutex> lock(network->s_keyQueueMutex);
network->m_peerReplicaKeyQueue[peerId] = modifyKey->getKId();
}
peer.second->writeMaster({ NET_FUNC::KEY_REQ, NET_SUBFUNC::NOP },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false);

@ -257,7 +257,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
if (m_control->m_requireLLAForReg) {
if (m_network != nullptr) {
m_network->setLLAKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t keyLength) {
m_network->setKEKKeyResponseCallback([=](uint32_t srcId, p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) {
(void)algId;
processLLAResponse(srcId, &ki, keyLength);
});
}

@ -1102,8 +1102,10 @@ bool Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ac
// generate NID
m_p25->m_nid.encode(data + 2U, DUID::PDU);
// add status bits
P25Utils::addStatusBits(data + 2U, newBitLength, m_inbound, true);
// advertise the inbound channel as idle when no RF PDU is being received
// the remaining, alternating status-symbol positions stay "unknown" as
// required by P25Utils::addStatusBits()
P25Utils::addStatusBits(data + 2U, newBitLength, m_inbound, false);
P25Utils::setStatusBitsStartIdle(data + 2U);
//Utils::dump("P25, Data::writeRF_PDU(), Raw PDU OSP", data, newByteLength + 2U);

@ -125,8 +125,15 @@ std::vector<std::vector<uint8_t>> FNETestHooks::buildOTARRekey(TrafficNetwork& n
return frames;
}
/** Builds an OTAR Changeover Command without dispatching it. */
/**
* @brief Builds an OTAR Changeover Command without dispatching it.
* @param network The TrafficNetwork instance.
* @param llId The logical link ID.
* @param kmmRSI The KMM RSI value.
* @param supersededKeysetId The ID of the superseded keyset.
* @param activeKeysetId The ID of the active keyset.
* @return A vector containing the encoded OTAR Changeover Command.
*/
std::vector<uint8_t> FNETestHooks::buildOTARChangeover(TrafficNetwork& network,
uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId)
{
@ -142,6 +149,102 @@ std::vector<uint8_t> FNETestHooks::buildOTARChangeover(TrafficNetwork& network,
return std::vector<uint8_t>(encoded.get(), encoded.get() + length);
}
/**
* @brief Builds an OTAR Warm Start Command without dispatching it.
* @param network The TrafficNetwork instance.
* @param llId The logical link ID.
* @param kmmRSI The KMM RSI value.
* @return A vector containing the encoded OTAR Warm Start Command.
*/
std::vector<uint8_t> FNETestHooks::buildOTARWarmStart(TrafficNetwork& network,
uint32_t llId, uint32_t kmmRSI)
{
if (network.m_p25OTARService == nullptr)
return {};
uint32_t length = 0U;
UInt8Array encoded = network.m_p25OTARService->write_KMM_WarmStart_Command(llId, kmmRSI, &length);
if (encoded == nullptr || length == 0U)
return {};
return std::vector<uint8_t>(encoded.get(), encoded.get() + length);
}
/**
* @brief Retrieves the temporary TEK for an OTAR Warm Start transaction.
* @param network The TrafficNetwork instance.
* @param rsi The RSI associated with the Warm Start transaction.
* @param kid Output parameter for the key ID.
* @param key Output parameter for the temporary TEK.
* @return True if the temporary TEK was successfully retrieved, false otherwise.
*/
bool FNETestHooks::getOTARWarmStartTEK(TrafficNetwork& network, uint32_t rsi,
uint16_t& kid, std::vector<uint8_t>& key)
{
if (network.m_p25OTARService == nullptr)
return false;
std::lock_guard<std::mutex> lock(network.m_p25OTARService->m_warmStartMutex);
auto it = network.m_p25OTARService->m_warmStartTransactions.find(rsi);
if (it == network.m_p25OTARService->m_warmStartTransactions.end())
return false;
kid = it->second.temporaryKId;
key.assign(it->second.temporaryTEK.begin(), it->second.temporaryTEK.end());
return true;
}
/**
* @brief Checks if there is an active OTAR Warm Start transaction for the given RSI.
* @param network The TrafficNetwork instance.
* @param rsi The RSI to check.
* @return True if there is an active Warm Start transaction, false otherwise.
*/
bool FNETestHooks::hasOTARWarmStart(TrafficNetwork& network, uint32_t rsi)
{
if (network.m_p25OTARService == nullptr)
return false;
std::lock_guard<std::mutex> lock(network.m_p25OTARService->m_warmStartMutex);
return network.m_p25OTARService->m_warmStartTransactions.find(rsi) !=
network.m_p25OTARService->m_warmStartTransactions.end();
}
/**
* @brief Injects and resolves an upstream TEK response through the OTAR cache.
* @param network The TrafficNetwork instance.
* @param key The KMM key item representing the TEK.
* @param algId The algorithm ID used for the TEK.
* @param keyLength The length of the TEK.
* @return True if the TEK was successfully cached and resolved, false otherwise.
*/
bool FNETestHooks::cacheOTARTEK(TrafficNetwork& network,
const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength)
{
if (network.m_p25OTARService == nullptr)
return false;
network.m_p25OTARService->cacheUpstreamTEK(key, algId, keyLength);
const uint32_t id = ((uint32_t)algId << 16U) | key.kId();
auto it = network.m_p25OTARService->m_upstreamTEKs.find(id);
return it != network.m_p25OTARService->m_upstreamTEKs.end() &&
it->second.algorithmId == algId && it->second.keyLength == keyLength;
}
/**
* @brief Injects and resolves an upstream UKEK response through the OTAR cache.
* @param network The TrafficNetwork instance.
* @param rsi The RSI associated with the UKEK.
* @param key The KMM key item representing the UKEK.
* @param algId The algorithm ID used for the UKEK.
* @param keyLength The length of the UKEK.
* @return True if the UKEK was successfully cached and resolved, false otherwise.
*/
bool FNETestHooks::cacheOTARUKEK(TrafficNetwork& network, uint32_t rsi,
const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength)
{
if (network.m_p25OTARService == nullptr)
return false;
network.m_p25OTARService->cacheUpstreamUKEK(rsi, key, algId, keyLength);
auto it = network.m_p25OTARService->m_upstreamUKEKs.find(rsi);
return it != network.m_p25OTARService->m_upstreamUKEKs.end() &&
it->second.algorithmId == algId && it->second.keyLength == keyLength;
}
/**
* @brief Resolves the security parameters of an OTAR response packet.
* @param network The TrafficNetwork instance.
@ -383,6 +486,16 @@ void FNETestHooks::addCryptoKey(TrafficNetwork& network, const EKCKeyItem& key)
network.m_cryptoLookup->addEntry(key);
}
/**
* @brief Adds a UKEK to the test crypto container.
* @param network The TrafficNetwork instance.
* @param key The UKEK key item to add.
*/
void FNETestHooks::addCryptoUKEK(TrafficNetwork& network, const EKCKeyItem& key)
{
network.m_cryptoLookup->addUKEK(key);
}
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------

@ -118,6 +118,55 @@ public:
*/
static std::vector<uint8_t> buildOTARChangeover(network::TrafficNetwork& network,
uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId);
/**
* @brief Builds and records an OTAR Warm-Start Command.
* @param network The TrafficNetwork instance.
* @param llId Logical Link ID used for message-number tracking.
* @param kmmRSI Destination KMM RSI.
* @return Encoded command, or an empty vector when it cannot be built.
*
*/
static std::vector<uint8_t> buildOTARWarmStart(network::TrafficNetwork& network,
uint32_t llId, uint32_t kmmRSI);
/**
* @brief Retrieves an active Warm-Start temporary key for transaction tests.
* @param network The TrafficNetwork instance.
* @param rsi The RSI for which to retrieve the temporary key.
* @param kid Output parameter for the key ID of the temporary key.
* @param key Output parameter for the temporary key bytes.
* @return True if an active Warm-Start temporary key exists, false otherwise.
*
*/
static bool getOTARWarmStartTEK(network::TrafficNetwork& network, uint32_t rsi,
uint16_t& kid, std::vector<uint8_t>& key);
/**
* @brief Returns whether an active Warm-Start transaction exists.
* @param network The TrafficNetwork instance.
* @param rsi The RSI for which to check the Warm-Start transaction.
* @return True if an active Warm-Start transaction exists, false otherwise.
*/
static bool hasOTARWarmStart(network::TrafficNetwork& network, uint32_t rsi);
/**
* @brief Injects and verifies an upstream TEK response in the OTAR cache.
* @param network The TrafficNetwork instance.
* @param key The key item to cache.
* @param algId The algorithm ID of the key.
* @param keyLength The length of the key in bytes.
* @return True if the key was successfully cached, false otherwise.
*/
static bool cacheOTARTEK(network::TrafficNetwork& network,
const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength);
/**
* @brief Injects and verifies an upstream UKEK response in the OTAR cache.
* @param network The TrafficNetwork instance.
* @param rsi The RSI for which to cache the UKEK.
* @param key The key item to cache.
* @param algId The algorithm ID of the key.
* @param keyLength The length of the key in bytes.
* @return True if the key was successfully cached, false otherwise.
*/
static bool cacheOTARUKEK(network::TrafficNetwork& network, uint32_t rsi,
const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength);
/**
* @brief Resolves the outer security context selected for a generated response.
* @param network The TrafficNetwork instance.
@ -224,6 +273,12 @@ public:
* @param key The cryptographic key to add.
*/
static void addCryptoKey(network::TrafficNetwork& network, const EKCKeyItem& key);
/**
* @brief Adds a UKEK to the test crypto container.
* @param network The TrafficNetwork instance.
* @param key The UKEK key item to add.
*/
static void addCryptoUKEK(network::TrafficNetwork& network, const EKCKeyItem& key);
};
#endif // __FNE_TEST_HOOKS_H__

@ -9,6 +9,7 @@
*/
#include "host/Defines.h"
#include "common/p25/P25Defines.h"
#include "common/p25/Crypto.h"
#include "common/p25/kmm/KMMFactory.h"
#include "common/p25/kmm/KMMChangeover.h"
#include "common/p25/kmm/KMMDeregistrationCommand.h"
@ -19,6 +20,7 @@
#include "common/p25/kmm/KMMNegativeAck.h"
#include "common/p25/kmm/KMMRekeyAck.h"
#include "common/p25/kmm/KMMRekeyCommand.h"
#include "common/p25/kmm/KMMWarmStartCommand.h"
#include "fne/FNETestHooks.h"
#include "fne/HostFNE.h"
#include "common/lookups/RadioIdLookup.h"
@ -135,6 +137,160 @@ TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm]
}
}
TEST_CASE("KMM Warm-Start Command round-trips its wrapped temporary TEK", "[p25][kmm][warm-start]")
{
uint8_t wrapped[P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES];
for (uint8_t i = 0U; i < sizeof(wrapped); ++i)
wrapped[i] = i;
KeyItem key;
key.keyFormat(KMM_KEY_FORMAT_TEK);
key.sln(0U);
key.kId(0x4567U);
key.setKey(wrapped, sizeof(wrapped));
KMMWarmStartCommand tx;
tx.setDstLLId(0x123456U);
tx.setSrcLLId(WUID_FNE);
tx.setHasMessageNumber(true);
tx.setMessageNumber(7U);
tx.setMACType(KMM_MAC::ENH_MAC);
tx.setMACAlgId(ALGO_AES_256);
tx.setMACKId(0x4567U);
tx.setMACFormat(KMM_MAC_FORMAT_CMAC);
tx.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE);
tx.setKEKAlgId(ALGO_AES_256);
tx.setKEKKId(0x1122U);
tx.setKeyLength(sizeof(wrapped));
tx.setTEKAlgId(ALGO_AES_256);
tx.setKey(key);
std::vector<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(WUID_FNE); 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(WUID_FNE); warmAck.setSrcLLId(SU_RSI);
warmAck.setHasMessageNumber(true); warmAck.setMessageNumber(command->getMessageNumber());
warmAck.setMACType(KMM_MAC::ENH_MAC); warmAck.setMACAlgId(ALGO_AES_256);
warmAck.setMACKId(temporaryKId); warmAck.setMACFormat(KMM_MAC_FORMAT_CMAC);
warmAck.setAckMessageId(KMM_MessageType::WARM_START_CMD);
warmAck.setNumberOfKeyStatus(1U); warmAck.setKeyStatus({ warmStatus });
std::vector<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(WUID_FNE); finalAck.setSrcLLId(SU_RSI);
finalAck.setHasMessageNumber(true); finalAck.setMessageNumber(rekey->getMessageNumber());
finalAck.setMACType(KMM_MAC::ENH_MAC); finalAck.setMACAlgId(ALGO_AES_256);
finalAck.setMACKId(FINAL_KID); finalAck.setMACFormat(KMM_MAC_FORMAT_CMAC);
finalAck.setAckMessageId(KMM_MessageType::REKEY_CMD);
finalAck.setNumberOfKeyStatus(1U); finalAck.setKeyStatus({ rekeyStatus });
std::vector<uint8_t> finalAckBytes = encodeKMM(finalAck);
finalAck.generateMAC(finalKey, finalAckBytes.data());
uint32_t finalResponseLength = 0U;
REQUIRE(FNETestHooks::processOTARKMM(harness.traffic, finalAckBytes, SU_RSI,
finalResponseLength, ALGO_AES_256, FINAL_KID) == nullptr);
CHECK_FALSE(FNETestHooks::hasOTARWarmStart(harness.traffic, SU_RSI));
}
TEST_CASE("KMM air-facing factory rejects malformed lengths", "[p25][kmm][security]")
{
KMMHello hello;
@ -613,9 +769,9 @@ TEST_CASE("P25 OTAR Rekey Commands batch four keys per KMM", "[p25][kmm][otar][r
REQUIRE(rekey != nullptr);
REQUIRE(rekey->getKeysets().size() == 1U);
CHECK(rekey->getKeysets()[0U].keys().size() == expectedCounts[i]);
// A new RK3 uses Rule 1 message-number validation. The SU initializes
// MNL to zero, so the first valid outbound KMF message number is one.
CHECK(rekey->getMessageNumber() == i + 1U);
// A new RK3 uses Rule 1 message-number validation. The interoperating
// Bootstrap past the Harris subscriber's retained MNL values one and two.
CHECK(rekey->getMessageNumber() == i + 3U);
CHECK(rekey->getComplete() == (i + 1U == frames.size()));
CHECK(frames[i].size() <= 287U);
}
@ -654,7 +810,7 @@ TEST_CASE("P25 OTAR builds authenticated Changeover Commands without dispatching
CHECK(changeover->getSupersededKeysetId() == SUPERSEDED_KEYSET);
CHECK(changeover->getActiveKeysetId() == ACTIVE_KEYSET);
CHECK(changeover->getHasMessageNumber());
CHECK(changeover->getMessageNumber() == 1U);
CHECK(changeover->getMessageNumber() == 3U);
CHECK(changeover->getMACType() == KMM_MAC::ENH_MAC);
CHECK(changeover->getMACAlgId() == ALGO_AES_256);
CHECK(changeover->getMACKId() == TEK_KID);
@ -673,6 +829,30 @@ TEST_CASE("P25 OTAR builds authenticated Changeover Commands without dispatching
CHECK(kid == TEK_KID);
}
TEST_CASE("P25 OTAR accepts upstream TEK and UKEK responses", "[p25][kmm][otar][upstream-keys]")
{
KMMFNEHarness harness;
constexpr uint32_t SU_RSI = 0x654321U;
uint8_t material[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
for (uint32_t i = 0U; i < sizeof(material); ++i)
material[i] = (uint8_t)i;
p25::kmm::KeyItem tek;
tek.keyFormat(KMM_KEY_FORMAT_TEK);
tek.kId(0x1234U);
tek.sln(1U);
tek.setKey(material, sizeof(material));
CHECK(FNETestHooks::cacheOTARTEK(harness.traffic, tek, ALGO_AES_256, sizeof(material)));
p25::kmm::KeyItem ukek;
ukek.keyFormat(KMM_KEY_FORMAT_KEK);
ukek.kId(0x4321U);
ukek.sln(0U);
ukek.setKey(material, sizeof(material));
CHECK(FNETestHooks::cacheOTARUKEK(harness.traffic, SU_RSI, ukek,
ALGO_AES_256, sizeof(material)));
}
#if !defined(_WIN32)
TEST_CASE("P25 OTAR DLI traverses the real UDP endpoint", "[p25][kmm][otar][dli][integration]")
{

Loading…
Cancel
Save

Powered by TurnKey Linux.