correct bad KMM authentication and replay protection; correctly negotiate proper response authentication; correct key-format semantics (there was a serious mismatch in P25Defines causing incorrect handling of values); harden KMM parsing and framing; fix PDU AES OFB processing; expand the P25 OTAR and crypto tests;

pull/133/head
Bryan Biedenkapp 1 month ago
parent e7fdaed775
commit 808aa8b70f

@ -171,8 +171,8 @@ void P25Crypto::generateKeystream()
case ALGO_AES_256:
{
if (m_keystream == nullptr)
m_keystream = new uint8_t[240U];
::memset(m_keystream, 0x00U, 240U);
m_keystream = new uint8_t[528U];
::memset(m_keystream, 0x00U, 528U);
uint8_t* iv = expandMIToIV();
@ -182,7 +182,7 @@ void P25Crypto::generateKeystream()
::memset(input, 0x00U, 16U);
::memcpy(input, iv, 16U);
for (uint32_t i = 0U; i < (240U / 16U); i++) {
for (uint32_t i = 0U; i < (528U / 16U); i++) {
uint8_t* output = aes.encryptECB(input, 16U, m_tek.get());
::memcpy(m_keystream + (i * 16U), output, 16U);
::memcpy(input, output, 16U);
@ -671,16 +671,18 @@ UInt8Array P25Crypto::cryptAES_KMM_CMAC(const uint8_t* macKey, const uint8_t* ms
/* Helper to crypt a P25 PDU frame using AES-256. */
void P25Crypto::cryptAES_PDU(uint8_t* frame, uint8_t frameLen)
void P25Crypto::cryptAES_PDU(uint8_t* frame, uint32_t frameLen)
{
if (m_keystream == nullptr)
return;
// per AAAD-B the first OFB iteration is discarded -- generate enough
// contiguous stream for the complete AACA-D 512-octet logical message;
// repeating a shorter stream is not OFB and leaks plaintext relationships
uint32_t offset = 16U;
for (uint8_t i = 0U; i < frameLen; i++) {
if (offset > 240U) {
offset = 16U;
}
for (uint32_t i = 0U; i < frameLen; i++) {
if (offset >= 528U)
return;
frame[i] ^= m_keystream[offset];
offset++;

@ -137,7 +137,7 @@ namespace p25
* @param frame Data frame
* @param frameLen Data frame Length
*/
void cryptAES_PDU(uint8_t* frame, uint8_t frameLen);
void cryptAES_PDU(uint8_t* frame, uint32_t frameLen);
/**
* @brief Helper to crypt P25 IMBE audio using DES.

@ -605,15 +605,20 @@ namespace p25
/** @brief KMM MAC Format - CMAC */
const uint8_t KMM_MAC_FORMAT_CMAC = 0x41U;
/** @brief KMM Keyset Format - KEKs */
const uint8_t KEYSET_FORMAT_KEK = 0x80U;
/** @brief KMM Body/Key Format - TEK Included */
const uint8_t KEY_FORMAT_TEK = 0x80U;
/** @brief KMM Body/Key Format - KEK Exists */
const uint8_t KEY_FORMAT_KEK_EXISTS = 0x40U;
/** @brief KMM Key Format - Delete Key */
const uint8_t KEY_FORMAT_DELETE = 0x20U;
/** @brief AACA-D Table 64 Body Format - reverse-warm-start TEK segment included. */
const uint8_t KMM_BODY_FORMAT_TEK_INCLUDED = 0x80U;
/** @brief AACA-D Table 64 Body Format - KEK does not exist. */
const uint8_t KMM_BODY_FORMAT_KEK_MISSING = 0x40U;
/** @brief AACA-D Table 70 Key Format - item is a TEK (T bit clear). */
const uint8_t KMM_KEY_FORMAT_TEK = 0x00U;
/** @brief AACA-D Table 70 Key Format - item is a KEK (T bit set). */
const uint8_t KMM_KEY_FORMAT_KEK = 0x80U;
/** @brief AACA-D Table 70 Key Format - delete/revoke item. */
const uint8_t KMM_KEY_FORMAT_DELETE = 0x20U;
/** @brief AACA-D Table 71 Keyset Format - keyset contains KEKs. */
const uint8_t KMM_KEYSET_FORMAT_KEK = 0x80U;
/** @brief SNDCP version 1 */
const uint8_t SNDCP_VERSION_1 = 0x01U;

@ -101,6 +101,76 @@ std::unique_ptr<KMMFrame> KMMFactory::create(const uint8_t* data)
return nullptr;
}
/* Create an instance of a KMMFrame with a specified length. */
std::unique_ptr<KMMFrame> KMMFactory::create(const uint8_t* data, uint32_t len)
{
if (data == nullptr || len < 10U || len > 512U)
return nullptr;
const uint32_t fullLength = (((uint32_t)data[1U] << 8U) | data[2U]) + 3U;
if (fullLength < 10U || fullLength > len || fullLength > 512U)
return nullptr;
const uint8_t mnCode = (data[3U] >> 4U) & 0x03U;
const uint8_t macType = (data[3U] >> 2U) & 0x03U;
if ((mnCode != 0U && mnCode != 2U) || macType == 1U)
return nullptr;
const uint32_t body = 10U + ((mnCode == 2U) ? 2U : 0U);
uint32_t bodyEnd = fullLength;
if (macType == KMM_MAC::DES_MAC) {
if (bodyEnd < 7U) return nullptr;
bodyEnd -= 7U;
} else if (macType == KMM_MAC::ENH_MAC) {
if (bodyEnd < 13U || data[bodyEnd - 5U] != P25DEF::KMM_AES_MAC_LENGTH)
return nullptr;
bodyEnd -= 13U;
}
if (body > bodyEnd)
return nullptr;
const uint32_t available = bodyEnd - body;
switch (data[0U]) {
case KMM_MessageType::HELLO:
case KMM_MessageType::INVENTORY_CMD:
case KMM_MessageType::DEREG_RSP:
case KMM_MessageType::REG_RSP:
if (available < 1U)
return nullptr;
break;
case KMM_MessageType::DEREG_CMD:
case KMM_MessageType::REG_CMD:
if (available < 4U)
return nullptr;
break;
case KMM_MessageType::NAK:
if (available < 4U)
return nullptr;
break;
case KMM_MessageType::REKEY_ACK:
if (available < 2U || available < 2U + (uint32_t)data[body + 1U] * 4U)
return nullptr;
break;
case KMM_MessageType::NO_SERVICE:
case KMM_MessageType::ZEROIZE_CMD:
case KMM_MessageType::ZEROIZE_RSP:
break;
case KMM_MessageType::UNABLE_TO_DECRYPT:
// the optional reverse-warm-start body is not accepted until its full
// nested length validation is implemented
if (available < 7U || (data[body] & 0x80U) != 0U)
return nullptr;
break;
default:
// other decoders contain nested count fields; keep the air-facing path
// fail-closed until each has a dedicated structural validator
return nullptr;
}
return create(data);
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------

@ -67,6 +67,13 @@ namespace p25
* @returns KMMFrame* Instance of a KMMFrame representing the decoded data.
*/
static std::unique_ptr<KMMFrame> create(const uint8_t* data);
/**
* @brief Create an instance of a KMMFrame with a specified length.
* @param[in] data Buffer containing KMM frame packet data to decode.
* @param[in] len Length of the buffer.
* @returns KMMFrame* Instance of a KMMFrame representing the decoded data.
*/
static std::unique_ptr<KMMFrame> create(const uint8_t* data, uint32_t len);
private:
/**

@ -136,6 +136,30 @@ void KMMFrame::generateMAC(uint8_t* kek, uint8_t* data)
}
}
/* Verify the MAC code for the given KMM frame. */
bool KMMFrame::verifyMAC(const uint8_t* tek, const uint8_t* data, uint32_t len)
{
if (tek == nullptr || data == nullptr || m_macType != KMM_MAC::ENH_MAC ||
m_messageFullLength > len || m_messageFullLength < P25DEF::KMM_AES_MAC_LENGTH + 5U)
return false;
UInt8Array candidate = std::make_unique<uint8_t[]>(m_messageFullLength);
::memcpy(candidate.get(), data, m_messageFullLength);
uint8_t expected[P25DEF::KMM_AES_MAC_LENGTH];
const uint32_t macOffset = m_messageFullLength - (P25DEF::KMM_AES_MAC_LENGTH + 5U);
::memcpy(expected, candidate.get() + macOffset, sizeof(expected));
generateMAC(const_cast<uint8_t*>(tek), candidate.get());
uint8_t different = 0U;
for (uint32_t i = 0U; i < sizeof(expected); ++i)
different |= expected[i] ^ candidate[macOffset + i];
return different == 0U;
}
/* Returns a string that represents the current KMM frame. */
std::string KMMFrame::toString()

@ -39,7 +39,9 @@ namespace p25
* @{
*/
const uint32_t KMM_FRAME_LENGTH = 9U;
// message length counts Format + Dest RSI + Source RSI (7 octets),
// followed by optional MN/body/MAC; ID and Length are excluded
const uint32_t KMM_FRAME_LENGTH = 7U;
/** @} */
@ -111,6 +113,14 @@ namespace p25
* @param[out] data Buffer to encode KMM MAC to.
*/
void generateMAC(uint8_t* kek, uint8_t* data);
/**
* @brief Verify the MAC code for the given KMM frame.
* @param tek Traffic Encryption Key
* @param[in] data Buffer containing KMM frame data to verify.
* @param len Length of the buffer.
* @returns bool True, if the MAC is valid, otherwise false.
*/
bool verifyMAC(const uint8_t* tek, const uint8_t* data, uint32_t len);
/**
* @brief Returns a string that represents the current KMM frame.

@ -37,7 +37,7 @@ namespace p25
* @{
*/
const uint32_t KMM_BODY_MODIFY_KEY_LENGTH = 8U;
const uint32_t KMM_BODY_MODIFY_KEY_LENGTH = 4U; // decryption instruction block
/** @} */

@ -29,8 +29,8 @@ KMMNegativeAck::KMMNegativeAck() : KMMFrame(),
m_messageNo(0U),
m_status(KMM_Status::CMD_NOT_PERFORMED)
{
m_messageId = KMM_MessageType::NAK;
m_respKind = KMM_ResponseKind::IMMEDIATE;
KMMFrame::m_messageId = KMM_MessageType::NAK;
m_respKind = KMM_ResponseKind::NONE;
}
/* Finalizes a instance of the KMMNegativeAck class. */

@ -37,7 +37,7 @@ namespace p25
* @{
*/
const uint32_t KMM_BODY_REKEY_CMD_LENGTH = 4U;
const uint32_t KMM_BODY_REKEY_CMD_LENGTH = 5U; // DI block + keyset count
/** @} */

@ -58,7 +58,7 @@ KMMUnableToDecrypt::~KMMUnableToDecrypt()
uint32_t KMMUnableToDecrypt::length() const
{
uint32_t len = KMMFrame::length() + KMM_BODY_UNABLE_TO_DECRYPT_LENGTH;
if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) {
if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) {
len += 3U;
}
@ -85,7 +85,7 @@ bool KMMUnableToDecrypt::decode(const uint8_t* data)
m_status = data[15U + m_bodyOffset]; // Status
uint8_t offset = 0U;
if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) {
if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) {
m_decryptInfoFmt = data[16U + m_bodyOffset]; // Decrypt Info Format
m_decryptAlgId = data[17U + m_bodyOffset]; // Decrypt Algorithm ID
m_decryptKId = GET_UINT16(data, 18U + m_bodyOffset); // Decrypt Key ID
@ -134,7 +134,7 @@ void KMMUnableToDecrypt::encode(uint8_t* data)
data[15U + m_bodyOffset] = m_status; // Status
uint8_t offset = 0U;
if ((m_bodyFormat & KEY_FORMAT_TEK) == KEY_FORMAT_TEK) {
if ((m_bodyFormat & KMM_BODY_FORMAT_TEK_INCLUDED) != 0U) {
data[16U + m_bodyOffset] = m_decryptInfoFmt; // Decrypt Info Format
data[17U + m_bodyOffset] = m_decryptAlgId; // Decrypt Algorithm ID
SET_UINT16(m_decryptKId, data, 18U + m_bodyOffset); // Decrypt Key ID
@ -146,6 +146,8 @@ void KMMUnableToDecrypt::encode(uint8_t* data)
}
data[16U + (m_bodyOffset + offset)] = m_key.getLength();
data[17U + (m_bodyOffset + offset)] = m_algId;
data[18U + (m_bodyOffset + offset)] = m_key.keyFormat();
uint16_t sln = m_key.sln();
SET_UINT16(sln, data, 19U + (m_bodyOffset + offset));

@ -15,6 +15,7 @@
#define __P25_KMM__KEYSET_ITEM_H__
#include "common/Defines.h"
#include "common/p25/P25Defines.h"
#include "common/Utils.h"
#include <cassert>
@ -51,7 +52,7 @@ namespace p25
* @brief Initializes a new instance of the KeyItem class.
*/
KeyItem() :
m_keyFormat(0x80U/*P25DEF::KEY_FORMAT_TEK*/),
m_keyFormat(P25DEF::KMM_KEY_FORMAT_TEK),
m_sln(0U),
m_kId(0U),
m_keyLength(0U),
@ -182,7 +183,7 @@ namespace p25
*/
uint32_t length() const
{
uint32_t len = 4U;
uint32_t len = 5U; // format, keyset ID, ALGID, key length, key count
uint32_t keyItemLength = m_keys.size() * 5U;
uint32_t combinedKeyLength = m_keys.size() * m_keyLength;

@ -46,6 +46,24 @@ using namespace p25::kmm;
#define MAX_THREAD_CNT 4U
namespace {
bool allowsUnauthenticatedKMM(uint8_t messageId)
{
switch (messageId) {
case KMM_MessageType::HELLO:
case KMM_MessageType::NO_SERVICE:
case KMM_MessageType::DEREG_CMD:
case KMM_MessageType::DEREG_RSP:
case KMM_MessageType::REG_CMD:
case KMM_MessageType::REG_RSP:
case KMM_MessageType::UNABLE_TO_DECRYPT:
return true;
default:
return false;
}
}
}
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -59,6 +77,8 @@ P25OTARService::P25OTARService(TrafficNetwork* network, P25PacketData* packetDat
m_network(network),
m_packetData(packetData),
m_rsiMessageNumber(),
m_rsiInboundMessageNumber(),
m_rsiInboundFingerprint(),
m_allowNoUKEKRekey(false),
m_debug(debug),
m_verbose(verbose)
@ -127,7 +147,7 @@ bool P25OTARService::processDLD(const uint8_t* data, uint32_t len, uint32_t llId
}
}
std::unique_ptr<KMMFrame> frame = KMMFactory::create(kmmPayload.get());
std::unique_ptr<KMMFrame> frame = KMMFactory::create(kmmPayload.get(), len);
if (frame == nullptr) {
LogWarning(LOG_P25, P25_KMM_STR ", undecodable KMM packet");
sendNack(PDUAckType::NACK_ILLEGAL);
@ -247,25 +267,34 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req)
return;
}
if (req->length >= 13) {
// read crypto parameters from KMM header
// AACA-D 12.3: Version-0 DLI preamble is Format(1), MFID(1),
// ALGID(1), KID(2), MI(9) = 14 octets
if (req->length >= 24 && (req->buffer[0U] & 0xFFU) == 0U && req->buffer[1U] == MFG_STANDARD) {
uint8_t algoId = req->buffer[2U];
uint16_t kid = GET_UINT16(req->buffer, 3U);
uint8_t mi[MI_LENGTH_BYTES];
::memset(mi, 0x00U, MI_LENGTH_BYTES);
for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) {
mi[i] = req->buffer[4U + i];
mi[i] = req->buffer[5U + i];
}
// KMM frame
UInt8Array buffer = std::make_unique<uint8_t[]>(req->length - 13U);
::memset(buffer.get(), 0x00U, req->length - 13U);
::memcpy(buffer.get(), req->buffer + 13U, req->length - 13U);
const uint32_t kmmLength = req->length - 14U;
if (kmmLength > 465U) {
LogWarning(LOG_P25, P25_KMM_STR ", invalid DLI KMM length, len = %u", kmmLength);
if (req->buffer != nullptr) delete[] req->buffer;
delete req;
return;
}
UInt8Array buffer = std::make_unique<uint8_t[]>(kmmLength);
::memset(buffer.get(), 0x00U, kmmLength);
::memcpy(buffer.get(), req->buffer + 14U, kmmLength);
bool encrypted = (algoId != ALGO_UNENCRYPT);
if (encrypted) {
buffer = network->cryptKMM(algoId, kid, mi, buffer.get(), req->length - 13U, false);
buffer = network->cryptKMM(algoId, kid, mi, buffer.get(), kmmLength, false);
if (buffer == nullptr) {
LogError(LOG_P25, P25_KMM_STR ", unable to decrypt KMM, algoId = $%02X, kID = $%04X", algoId, kid);
if (req->buffer != nullptr)
@ -276,7 +305,7 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req)
}
uint32_t payloadSize = 0U;
UInt8Array pduUserData = network->processKMM(buffer.get(), req->length - 13U, 0U, false, &payloadSize);
UInt8Array pduUserData = network->processKMM(buffer.get(), kmmLength, 0U, false, &payloadSize);
if (pduUserData == nullptr || payloadSize == 0U) {
if (network->m_debug)
LogDebug(LOG_P25, P25_KMM_STR ", no KMM response generated for network request");
@ -299,7 +328,19 @@ void P25OTARService::taskNetworkRx(OTARPacketRequest* req)
}
}
network->m_frameQueue->write(pduUserData.get(), payloadSize, req->address, req->addrLen);
UInt8Array datagram = std::make_unique<uint8_t[]>(payloadSize + 14U);
::memset(datagram.get(), 0x00U, payloadSize + 14U);
datagram[0U] = 0U; // Version-0 preamble format
datagram[1U] = MFG_STANDARD;
datagram[2U] = encrypted ? algoId : ALGO_UNENCRYPT;
SET_UINT16(encrypted ? kid : 0U, datagram.get(), 3U);
if (encrypted)
::memcpy(datagram.get() + 5U, mi, MI_LENGTH_BYTES);
::memcpy(datagram.get() + 14U, pduUserData.get(), payloadSize);
network->m_frameQueue->write(datagram.get(), payloadSize + 14U, req->address, req->addrLen);
}
if (req->buffer != nullptr)
@ -314,6 +355,13 @@ UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, c
{
assert(buffer != nullptr);
// AACA-D 12.4: a DLD KMM is at most 512 octets including the three
// octets excluded from Message Length
if (len < 10U || len > 512U) {
LogError(LOG_P25, P25_KMM_STR ", invalid DLD KMM length, len = %u", len);
return nullptr;
}
P25Crypto crypto;
if (!encrypt)
crypto.setMI(mi);
@ -340,12 +388,11 @@ UInt8Array P25OTARService::cryptKMM(uint8_t algoId, uint16_t kid, uint8_t* mi, c
::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);
if (m_network->m_debug) {
if (m_network->m_debug)
LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength);
Utils::dump(1U, "P25OTARService::cryptKMM(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
LogInfoEx(LOG_P25, P25_KMM_STR ", algId = $%02X, kID = $%04X", algoId, kid);
crypto.setTEKAlgoId(algoId);
crypto.setKey(key, keyLength);
crypto.generateKeystream();
@ -413,26 +460,95 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
Utils::dump(1U, "P25OTARService::processKMM(), (Decrypted) KMM Network Message", buffer.get(), len);
}
std::unique_ptr<KMMFrame> frame = KMMFactory::create(buffer.get());
std::unique_ptr<KMMFrame> frame = KMMFactory::create(buffer.get(), len);
if (frame == nullptr) {
LogWarning(LOG_P25, P25_KMM_STR ", undecodable KMM packet");
return nullptr;
}
// AACA-D 7.4: validate addressing, MN/MAC coupling, authentication and
// anti-replay state before executing message-specific behavior
if (frame->getDstLLId() != WUID_FNE || frame->getSrcLLId() == 0U ||
(llId != 0U && frame->getSrcLLId() != llId)) {
LogWarning(LOG_P25, P25_KMM_STR ", invalid source/destination RSI");
return nullptr;
}
if (frame->getMACType() == KMM_MAC::NO_MAC &&
(frame->getHasMessageNumber() || !allowsUnauthenticatedKMM(frame->getMessageId()))) {
LogWarning(LOG_P25, P25_KMM_STR ", MAC required for message type/MN, RSI = %u", frame->getSrcLLId());
return nullptr;
}
if (frame->getMACType() != KMM_MAC::NO_MAC) {
if (frame->getMACType() != KMM_MAC::ENH_MAC || frame->getMACAlgId() != ALGO_AES_256 ||
(frame->getMACFormat() != KMM_MAC_FORMAT_CBC && frame->getMACFormat() != KMM_MAC_FORMAT_CMAC)) {
LogWarning(LOG_P25, P25_KMM_STR ", unsupported MAC type/algorithm, RSI = %u", frame->getSrcLLId());
return nullptr;
}
EKCKeyItem macTek = m_network->m_cryptoLookup->find(frame->getMACKId());
if (macTek.isInvalid() || macTek.algId() != frame->getMACAlgId()) {
LogWarning(LOG_P25, P25_KMM_STR ", MAC TEK not found, RSI = %u", frame->getSrcLLId());
return nullptr;
}
uint8_t tek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(tek, 0x00U, sizeof(tek));
macTek.getKey(tek);
if (!frame->verifyMAC(tek, buffer.get(), len)) {
LogWarning(LOG_P25, P25_KMM_STR ", invalid MAC, RSI = %u", frame->getSrcLLId());
return nullptr;
}
}
KMMAuthContext auth;
auth.authenticated = frame->getMACType() == KMM_MAC::ENH_MAC;
auth.hasMessageNumber = frame->getHasMessageNumber();
auth.messageNumber = frame->getMessageNumber();
auth.algorithmId = frame->getMACAlgId();
auth.keyId = frame->getMACKId();
auth.format = frame->getMACFormat();
if (frame->getHasMessageNumber()) {
const uint32_t rsi = frame->getSrcLLId();
const uint16_t received = frame->getMessageNumber();
uint64_t fingerprint = 1469598103934665603ULL;
const uint32_t declared = (((uint32_t)buffer[1U] << 8U) | buffer[2U]) + 3U;
for (uint32_t i = 0U; i < declared; ++i) {
fingerprint ^= buffer[i];
fingerprint *= 1099511628211ULL;
}
auto lastIt = m_rsiInboundMessageNumber.find(rsi);
if (lastIt != m_rsiInboundMessageNumber.end()) {
const uint16_t last = lastIt->second;
const uint16_t distance = (uint16_t)(received - last);
const bool identicalRetry = distance == 0U &&
frame->getResponseKind() == KMM_ResponseKind::IMMEDIATE &&
m_rsiInboundFingerprint.find(rsi) != m_rsiInboundFingerprint.end() &&
m_rsiInboundFingerprint[rsi] == fingerprint;
if (!identicalRetry && (distance == 0U || distance >= 1680U)) {
LogWarning(LOG_P25, P25_KMM_STR ", invalid/replayed message number, RSI = %u, MN = %u", rsi, received);
return nullptr;
}
}
m_rsiInboundMessageNumber[rsi] = received;
m_rsiInboundFingerprint[rsi] = fingerprint;
}
if (llId == 0U) {
llId = frame->getSrcLLId();
}
// does this llId have a RSI message number setup?
if (m_rsiMessageNumber.find(llId) == m_rsiMessageNumber.end()) {
// maintain outbound state independently from the validated inbound MN
if (m_rsiMessageNumber.find(llId) == m_rsiMessageNumber.end())
m_rsiMessageNumber[llId] = 0U;
} else {
// roll message number
uint16_t mn = m_rsiMessageNumber[llId];
mn++;
m_rsiMessageNumber[llId] = mn;
}
switch (frame->getMessageId()) {
case KMM_MessageType::HELLO:
@ -468,37 +584,37 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
// respond with No-Service if KMF services are disabled
if (!m_network->m_kmfServicesEnabled)
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
else {
if (kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_UKEK ||
(kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_NO_UKEK && m_allowNoUKEKRekey)) {
lookups::RadioId ridEntry = m_network->m_ridLookup->find(kmm->getSrcLLId());
if (ridEntry.radioDefault()) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u has no key policy entry", kmm->toString().c_str(), kmm->getSrcLLId());
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
}
if (!ridEntry.radioEnabled()) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u disabled", kmm->toString().c_str(), kmm->getSrcLLId());
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
}
if (!ridEntry.canRekey()) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey denied; RID %u not rekeyable", kmm->toString().c_str(), kmm->getSrcLLId());
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
}
// send rekey-command
EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(llId);
if (keyItem.isInvalid()) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, no UKEK found for rekey request, llId = %u", kmm->toString().c_str(), llId);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
} else {
return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize);
return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize, auth);
}
} else {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey request denied, llId = %u", kmm->toString().c_str(), llId);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
}
}
}
@ -552,9 +668,9 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
// respond with No-Service if KMF services are disabled
if (!m_network->m_kmfServicesEnabled)
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth);
else
return write_KMM_Dereg_Response(llId, kmm->getSrcLLId(), payloadSize);
return write_KMM_Dereg_Response(llId, kmm->getSrcLLId(), payloadSize, auth);
}
break;
case KMM_MessageType::REG_RSP:
@ -583,7 +699,8 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
/* Helper used to return a Rekey-Command KMM to the calling SU. */
UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize)
UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize,
const KMMAuthContext& auth)
{
uint8_t mi[MI_LENGTH_BYTES];
::memset(mi, 0x00U, MI_LENGTH_BYTES);
@ -617,10 +734,8 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
kekAlgId = keyItem.algId();
kekKId = keyItem.kId();
if (m_network->m_debug) {
if (m_network->m_debug)
LogDebugEx(LOG_P25, "P25OTARService::cryptKMM()", "keyLength = %u", keyLength);
Utils::dump(1U, "P25OTARService::cryptKMM(), Key", kekKey, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
}
else {
if (!m_allowNoUKEKRekey) {
@ -638,10 +753,8 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
outKmm.setDstLLId(kmmRSI);
outKmm.setMACType(KMM_MAC::ENH_MAC);
outKmm.setMACAlgId(kekAlgId);
outKmm.setMACKId(kekKId);
outKmm.setMACFormat(KMM_MAC_FORMAT_CBC);
outKmm.setMACFormat(auth.authenticated ? auth.format : KMM_MAC_FORMAT_CBC);
outKmm.setHasMessageNumber(true);
outKmm.setMessageNumber(mn);
outKmm.setAlgId(kekAlgId);
@ -656,6 +769,40 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
allowedKIds = ridEntry.allowedKIds();
// AACA-D 13.5: the MAC key is a TEK, never the UKEK used for inner
// key wrap; select an AES TEK already authorized for this subscriber
EKCKeyItem macTekItem;
if (auth.authenticated) {
macTekItem = m_network->m_cryptoLookup->find(auth.keyId);
if (macTekItem.isInvalid() || macTekItem.algId() != auth.algorithmId ||
(!allowedKIds.empty() && std::find(allowedKIds.begin(), allowedKIds.end(), auth.keyId) == allowedKIds.end())) {
LogError(LOG_P25, P25_KMM_STR ", %s, authenticated request MAC TEK is not authorized for response, RSI = %u",
outKmm.toString().c_str(), kmmRSI);
return nullptr;
}
} else {
for (const EKCKeyItem& candidate : m_network->m_cryptoLookup->keys()) {
if (candidate.algId() == ALGO_AES_256 &&
(allowedKIds.empty() || std::find(allowedKIds.begin(), allowedKIds.end(),
(uint16_t)candidate.kId()) != allowedKIds.end())) {
macTekItem = candidate;
break;
}
}
}
if (macTekItem.isInvalid()) {
LogError(LOG_P25, P25_KMM_STR ", %s, no common AES TEK available for MAC, RSI = %u",
outKmm.toString().c_str(), kmmRSI);
return nullptr;
}
uint8_t macTek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(macTek, 0x00U, sizeof(macTek));
macTekItem.getKey(macTek);
outKmm.setMACAlgId(macTekItem.algId());
outKmm.setMACKId((uint16_t)macTekItem.kId());
KeysetItem ks;
ks.keysetId(1U);
ks.algId(ALGO_AES_256); // we currently can only OTAR AES256 keys
@ -695,7 +842,7 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
}
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(wrappedKey.get(), keyLength);
@ -714,6 +861,12 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
outKmm.setKeysets(keysets);
if (outKmm.fullLength() > 512U) {
LogError(LOG_P25, P25_KMM_STR ", %s, rekey exceeds AACA-D DLD maximum, len = %u, llId = %u, RSI = %u",
outKmm.toString().c_str(), outKmm.fullLength(), outKmm.getSrcLLId(), outKmm.getDstLLId());
return nullptr;
}
if (m_verbose) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, RSI = %u, keyCount = %u", outKmm.toString().c_str(),
outKmm.getSrcLLId(), outKmm.getDstLLId(), ks.keys().size());
@ -725,7 +878,9 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
UInt8Array kmmFrame = std::make_unique<uint8_t[]>(frameLength);
outKmm.encode(kmmFrame.get());
outKmm.generateMAC(kekKey, kmmFrame.get());
outKmm.generateMAC(macTek, kmmFrame.get());
m_rsiMessageNumber[llId] = (uint16_t)(mn + 1U);
return kmmFrame;
}
@ -753,7 +908,8 @@ UInt8Array P25OTARService::write_KMM_Reg_Command(uint32_t llId, uint32_t kmmRSI,
/* Helper used to return a Deregistration-Response KMM to the calling SU. */
UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize)
UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize,
const KMMAuthContext& auth)
{
KMMDeregistrationResponse outKmm = KMMDeregistrationResponse();
outKmm.setSrcLLId(WUID_FNE);
@ -765,18 +921,13 @@ UInt8Array P25OTARService::write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmR
outKmm.getSrcLLId(), outKmm.getDstLLId());
}
const uint32_t frameLength = outKmm.fullLength();
if (payloadSize != nullptr)
*payloadSize = frameLength;
UInt8Array kmmFrame = std::make_unique<uint8_t[]>(frameLength);
outKmm.encode(kmmFrame.get());
return kmmFrame;
return encode_KMM_Response(outKmm, payloadSize, auth);
}
/* Helper used to return a No-Service KMM to the calling SU. */
UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize)
UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize,
const KMMAuthContext& auth)
{
KMMNoService outKmm = KMMNoService();
outKmm.setSrcLLId(WUID_FNE);
@ -787,13 +938,43 @@ UInt8Array P25OTARService::write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, u
outKmm.getSrcLLId(), outKmm.getDstLLId());
}
const uint32_t frameLength = outKmm.fullLength();
return encode_KMM_Response(outKmm, payloadSize, auth);
}
/* Encodes a response using the authenticated request's MAC format and MN. */
UInt8Array P25OTARService::encode_KMM_Response(KMMFrame& frame, uint32_t* payloadSize,
const KMMAuthContext& auth)
{
uint8_t macTek[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(macTek, 0x00U, sizeof(macTek));
if (auth.authenticated) {
EKCKeyItem key = m_network->m_cryptoLookup->find(auth.keyId);
if (key.isInvalid() || key.algId() != auth.algorithmId)
return nullptr;
key.getKey(macTek);
frame.setMACType(KMM_MAC::ENH_MAC);
frame.setMACAlgId(auth.algorithmId);
frame.setMACKId(auth.keyId);
frame.setMACFormat(auth.format);
if (auth.hasMessageNumber) {
frame.setHasMessageNumber(true);
frame.setMessageNumber(auth.messageNumber);
}
}
const uint32_t frameLength = frame.fullLength();
if (payloadSize != nullptr)
*payloadSize = frameLength;
UInt8Array kmmFrame = std::make_unique<uint8_t[]>(frameLength);
outKmm.encode(kmmFrame.get());
return kmmFrame;
UInt8Array encoded = std::make_unique<uint8_t[]>(frameLength);
::memset(encoded.get(), 0x00U, frameLength);
frame.encode(encoded.get());
if (auth.authenticated)
frame.generateMAC(macTek, encoded.get());
return encoded;
}
/* Helper used to return a Zeroize KMM to the calling SU. */

@ -24,6 +24,7 @@ class FNETestHooks;
#include "common/concurrent/unordered_map.h"
#include "common/p25/P25Defines.h"
#include "common/p25/Crypto.h"
#include "common/p25/kmm/KMMFrame.h"
#include "common/network/udp/Socket.h"
#include "common/network/RawFrameQueue.h"
#include "network/TrafficNetwork.h"
@ -105,6 +106,18 @@ namespace network
void close();
private:
/**
* @brief Context for KMM message authentication.
*/
struct KMMAuthContext {
bool authenticated = false; //!< Indicates if the KMM message is authenticated.
bool hasMessageNumber = false; //!< Indicates if the KMM message has a message number.
uint16_t messageNumber = 0U; //!< Message number of the KMM message.
uint8_t algorithmId = P25DEF::ALGO_UNENCRYPT; //!< Algorithm ID used for the KMM message.
uint16_t keyId = 0U; //!< Key ID used for the KMM message.
uint16_t format = P25DEF::KMM_MAC_FORMAT_CBC; //!< MAC format used for the KMM message.
};
network::udp::Socket* m_socket;
network::RawFrameQueue* m_frameQueue;
@ -114,6 +127,8 @@ namespace network
network::callhandler::packetdata::P25PacketData* m_packetData;
concurrent::unordered_map<uint32_t, uint16_t> m_rsiMessageNumber;
concurrent::unordered_map<uint32_t, uint16_t> m_rsiInboundMessageNumber;
concurrent::unordered_map<uint32_t, uint64_t> m_rsiInboundFingerprint;
bool m_allowNoUKEKRekey;
@ -156,9 +171,11 @@ namespace network
* @param kmmRSI KMM Radio Set Identifier.
* @param flags Hello KMM flags.
* @param[out] payloadSize Size of the returned KMM payload.
* @param auth Authentication context containing MAC/MN fields to mirror.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize);
UInt8Array write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRSI, uint8_t flags, uint32_t* payloadSize,
const KMMAuthContext& auth);
/**
* @brief Helper used to return a Registration-Command KMM to the calling SU.
@ -174,18 +191,31 @@ namespace network
* @param llId Logical Link Address.
* @param kmmRSI KMM Radio Set Identifier.
* @param[out] payloadSize Size of the returned KMM payload.
* @param auth Authentication context containing MAC/MN fields to mirror.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize);
UInt8Array write_KMM_Dereg_Response(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize,
const KMMAuthContext& auth);
/**
* @brief Helper used to return a No-Service KMM to the calling SU.
* @param llId Logical Link Address.
* @param kmmRSI KMM Radio Set Identifier.
* @param[out] payloadSize Size of the returned KMM payload.
* @param auth Authentication context containing MAC/MN fields to mirror.
* @returns UInt8Array Buffer containing the processed KMM frame (if any).
*/
UInt8Array write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize);
UInt8Array write_KMM_NoService(uint32_t llId, uint32_t kmmRSI, uint32_t* payloadSize,
const KMMAuthContext& auth);
/**
* @brief Encodes a response and mirrors an authenticated request's MAC/MN fields.
* @param frame KMM frame to encode.
* @param[out] payloadSize Size of the returned KMM payload.
* @param auth Authentication context containing MAC/MN fields to mirror.
*/
UInt8Array encode_KMM_Response(p25::kmm::KMMFrame& frame, uint32_t* payloadSize,
const KMMAuthContext& auth);
/**
* @brief Helper used to return a Zeroize KMM to the calling SU.

@ -3009,7 +3009,7 @@ void TrafficNetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId,
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.kId(rspKi->kId());
ki.sln(rspKi->sln());
ki.setKey(key, keyLength);
@ -3079,7 +3079,7 @@ void TrafficNetwork::processLLAResponse(uint32_t srcId, p25::kmm::KeyItem* rspKi
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.kId(rspKi->kId());
ki.sln(rspKi->sln());
ki.setKey(key, keyLength);

@ -149,7 +149,7 @@ void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacke
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(key, keyLength);

@ -120,7 +120,7 @@ void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPa
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(key, keyLength);

@ -70,6 +70,14 @@ FNEPeerConnection& FNETestHooks::addPeer(TrafficNetwork& network, uint32_t peerI
return *connection;
}
/**
* @brief Passes an encoded KMM through the FNE OTAR message dispatcher.
* @param network The TrafficNetwork instance.
* @param packet The KMM packet data.
* @param llId The logical link ID.
* @param payloadSize The size of the processed payload.
* @return A unique pointer to the processed KMM frame, or nullptr if processing failed.
*/
std::unique_ptr<uint8_t[]> FNETestHooks::processOTARKMM(TrafficNetwork& network,
const std::vector<uint8_t>& packet, uint32_t llId, uint32_t& payloadSize)
{
@ -81,11 +89,86 @@ std::unique_ptr<uint8_t[]> FNETestHooks::processOTARKMM(TrafficNetwork& network,
false, &payloadSize);
}
/**
* @brief Passes a DLD KMM through the public P25 OTAR bearer entry point.
* @param network The TrafficNetwork instance.
* @param packet The DLD packet data.
* @param llId The logical link ID.
* @param n The sequence number.
* @param encrypted Whether the packet is encrypted.
* @param algoId The algorithm ID used for encryption.
* @param kid The key ID used for encryption.
* @param mi The message integrity value.
* @return True if the packet was successfully processed, false otherwise.
*/
bool FNETestHooks::processOTARDLD(TrafficNetwork& network, const std::vector<uint8_t>& packet,
uint32_t llId, uint8_t n, bool encrypted, uint8_t algoId, uint16_t kid, const uint8_t* mi)
{
if (packet.empty() || network.m_p25OTARService == nullptr)
return false;
return network.m_p25OTARService->processDLD(packet.data(), (uint32_t)packet.size(), llId, n,
encrypted, algoId, kid, mi);
}
/**
* @brief Passes a complete Version-0 DLI datagram through the network receive task.
* @param network The TrafficNetwork instance.
* @param datagram The DLI packet data.
* @return True if the packet was successfully processed, false otherwise.
*/
void FNETestHooks::processOTARDLI(TrafficNetwork& network, const std::vector<uint8_t>& datagram)
{
if (datagram.empty() || network.m_p25OTARService == nullptr)
return;
OTARPacketRequest* req = new OTARPacketRequest();
req->obj = network.m_p25OTARService;
req->address = {};
req->addrLen = 0U;
req->length = (int)datagram.size();
req->buffer = new uint8_t[datagram.size()];
::memcpy(req->buffer, datagram.data(), datagram.size());
P25OTARService::taskNetworkRx(req);
}
/**
* @brief Checks if the specified inbound message number exists for the given RSI.
* @param network The TrafficNetwork instance.
* @param rsi The RSI to check.
* @param mn The message number to check.
* @return True if the inbound message number exists and matches, false otherwise.
*/
bool FNETestHooks::hasOTARInboundMessageNumber(TrafficNetwork& network, uint32_t rsi, uint16_t mn)
{
if (network.m_p25OTARService == nullptr)
return false;
auto it = network.m_p25OTARService->m_rsiInboundMessageNumber.find(rsi);
return it != network.m_p25OTARService->m_rsiInboundMessageNumber.end() && it->second == mn;
}
/**
* @brief Enables or disables KMF services for the specified TrafficNetwork.
* @param network The TrafficNetwork instance.
* @param enabled True to enable KMF services, false to disable.
*/
void FNETestHooks::setKMFServicesEnabled(TrafficNetwork& network, bool enabled)
{
network.m_kmfServicesEnabled = enabled;
}
/**
* @brief Adds a cryptographic key to the network's crypto lookup.
* @param network The TrafficNetwork instance.
* @param key The cryptographic key to add.
*/
void FNETestHooks::addCryptoKey(TrafficNetwork& network, const EKCKeyItem& key)
{
network.m_cryptoLookup->addEntry(key);
}
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------

@ -95,8 +95,47 @@ public:
*/
static std::unique_ptr<uint8_t[]> processOTARKMM(network::TrafficNetwork& network,
const std::vector<uint8_t>& packet, uint32_t llId, uint32_t& payloadSize);
/** @brief Enables or disables KMF services for a test TrafficNetwork. */
/**
* @brief Passes a DLD KMM through the public P25 OTAR bearer entry point.
* @param network The TrafficNetwork that owns the OTAR service.
* @param packet Encoded DLD bytes.
* @param llId Logical Link ID associated with the message.
* @param n Sequence number of the DLD packet.
* @param encrypted Whether the packet is encrypted.
* @param algoId The algorithm ID used for encryption.
* @param kid The key ID used for encryption.
* @param mi The message integrity value.
* @return True if the packet was successfully processed, false otherwise.
*/
static bool processOTARDLD(network::TrafficNetwork& network, const std::vector<uint8_t>& packet,
uint32_t llId, uint8_t n, bool encrypted = false, uint8_t algoId = P25DEF::ALGO_UNENCRYPT,
uint16_t kid = 0U, const uint8_t* mi = nullptr);
/**
* @brief Passes a complete Version-0 DLI datagram through the network receive task.
* @param network The TrafficNetwork that owns the OTAR service.
* @param datagram Encoded DLI bytes.
*/
static void processOTARDLI(network::TrafficNetwork& network, const std::vector<uint8_t>& datagram);
/**
* @brief Checks if the specified inbound message number exists for the given RSI.
* @param network The TrafficNetwork that owns the OTAR service.
* @param rsi The RSI to check.
* @param mn The message number to check.
* @return True if the inbound message number exists and matches, false otherwise.
*/
static bool hasOTARInboundMessageNumber(network::TrafficNetwork& network, uint32_t rsi, uint16_t mn);
/**
* @brief Enables or disables KMF services for the specified TrafficNetwork.
* @param network The TrafficNetwork that owns the KMF services.
* @param enabled True to enable KMF services, false to disable.
*/
static void setKMFServicesEnabled(network::TrafficNetwork& network, bool enabled);
/**
* @brief Adds a cryptographic key to the network's crypto lookup.
* @param network The TrafficNetwork instance.
* @param key The cryptographic key to add.
*/
static void addCryptoKey(network::TrafficNetwork& network, const EKCKeyItem& key);
};
#endif // __FNE_TEST_HOOKS_H__

@ -97,7 +97,7 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") {
ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(0U);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.sln(0U);
ki.kId(0x4983U);
@ -113,6 +113,11 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") {
outKmm.encode(kmmFrame.get());
outKmm.generateMAC(macTek, kmmFrame.get());
REQUIRE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength()));
kmmFrame[20U] ^= 0x01U;
REQUIRE_FALSE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength()));
kmmFrame[20U] ^= 0x01U;
Utils::dump(2U, "P25_KMM_ReKey_CBC_Test, GeneratedDataBlock", kmmFrame.get(), outKmm.fullLength());
for (uint32_t i = 0; i < outKmm.fullLength(); i++) {
@ -141,3 +146,41 @@ TEST_CASE("KMM ReKey Command CBC Test", "[p25][kmm_cbc][cap]") {
REQUIRE(failed==false);
}
TEST_CASE("KMM Key Format follows AACA-D Table 70", "[p25][kmm][key-format]")
{
REQUIRE(KMM_KEY_FORMAT_TEK == 0x00U);
REQUIRE(KMM_KEY_FORMAT_KEK == 0x80U);
REQUIRE(KMM_KEY_FORMAT_DELETE == 0x20U);
REQUIRE(KMM_BODY_FORMAT_TEK_INCLUDED == 0x80U);
REQUIRE(KMM_BODY_FORMAT_KEK_MISSING == 0x40U);
auto encodedKeyFormat = [](uint8_t format) {
KMMRekeyCommand command;
command.setSrcLLId(0x010203U);
command.setDstLLId(0x040506U);
command.setDecryptInfoFmt(KMM_DECRYPT_INSTRUCT_NONE);
KeysetItem keyset;
keyset.keysetId(1U);
keyset.algId(ALGO_AES_256);
keyset.keyLength(1U);
KeyItem key;
const uint8_t material = 0x5AU;
key.keyFormat(format);
key.sln(1U);
key.kId(2U);
key.setKey(&material, 1U);
keyset.push_back(key);
command.setKeysets({ keyset });
std::vector<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);
}

@ -11,6 +11,7 @@
#include "common/p25/P25Defines.h"
#include "common/p25/Crypto.h"
#include "common/p25/kmm/KMMRekeyCommand.h"
#include "common/p25/kmm/KMMHello.h"
#include "common/Log.h"
#include "common/Utils.h"
@ -79,7 +80,7 @@ TEST_CASE("KMM ReKey Command CMAC Test", "[p25][kmm_cmac][cap]") {
ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(0U);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.sln(0U);
ki.kId(0x4983U);
@ -95,6 +96,11 @@ TEST_CASE("KMM ReKey Command CMAC Test", "[p25][kmm_cmac][cap]") {
outKmm.encode(kmmFrame.get());
outKmm.generateMAC(macTek, kmmFrame.get());
REQUIRE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength()));
kmmFrame[20U] ^= 0x01U;
REQUIRE_FALSE(outKmm.verifyMAC(macTek, kmmFrame.get(), outKmm.fullLength()));
kmmFrame[20U] ^= 0x01U;
Utils::dump(2U, "P25_KMM_ReKey_CMAC_Test, GeneratedDataBlock", kmmFrame.get(), outKmm.fullLength());
for (uint32_t i = 0; i < outKmm.fullLength(); i++) {
@ -144,9 +150,25 @@ TEST_CASE("KMM Hello CMAC Test (No Message Number)", "[p25][kmm_cmac][cap]") {
}
REQUIRE(failed==false);
KMMHello hello;
hello.setDstLLId(0x643BA8U);
hello.setSrcLLId(0x712B1DU);
hello.setResponseKind(KMM_ResponseKind::NONE);
hello.setMACType(KMM_MAC::ENH_MAC);
hello.setMACAlgId(ALGO_AES_256);
hello.setMACKId(0x2F62U);
hello.setMACFormat(KMM_MAC_FORMAT_CMAC);
hello.setFlag(KMM_HelloFlag::IDENT_ONLY);
REQUIRE(hello.fullLength() == sizeof(expectedFrame));
UInt8Array encoded = std::make_unique<uint8_t[]>(hello.fullLength());
::memset(encoded.get(), 0x00U, hello.fullLength());
hello.encode(encoded.get());
hello.generateMAC(macTek, encoded.get());
REQUIRE(::memcmp(encoded.get(), expectedFrame, sizeof(expectedFrame)) == 0);
}
TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac][cap]") {
TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cbc][cap]") {
// Verify explicit MN presence works even when the message number value is zero.
KMMRekeyCommand outKmm = KMMRekeyCommand();
@ -157,7 +179,7 @@ TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac]
outKmm.setMACType(KMM_MAC::ENH_MAC);
outKmm.setMACAlgId(ALGO_AES_256);
outKmm.setMACKId(0x2F62U);
outKmm.setMACFormat(KMM_MAC_FORMAT_CMAC);
outKmm.setMACFormat(KMM_MAC_FORMAT_CBC);
outKmm.setHasMessageNumber(true);
outKmm.setMessageNumber(0x0000U);
@ -171,7 +193,7 @@ TEST_CASE("KMM Rekey Encodes Message Number Zero When Present", "[p25][kmm_cmac]
ks.keyLength(P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(0U);
ki.keyFormat(KMM_KEY_FORMAT_TEK);
ki.sln(0U);
ki.kId(0x4983U);

@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Test Suite
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*/
#include "host/Defines.h"
#include "common/p25/P25Defines.h"
#include "common/p25/Crypto.h"
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <cstring>
#include <vector>
using namespace p25::crypto;
using namespace p25::defines;
TEST_CASE("AES PDU OFB covers messages larger than 255 octets", "[p25][crypto][otar]")
{
uint8_t key[32U];
uint8_t mi[MI_LENGTH_BYTES];
for (uint32_t i = 0U; i < sizeof(key); ++i) key[i] = (uint8_t)i;
for (uint32_t i = 0U; i < sizeof(mi); ++i) mi[i] = (uint8_t)(0xA0U + i);
std::vector<uint8_t> original(512U), encrypted(512U);
for (uint32_t i = 0U; i < original.size(); ++i) original[i] = (uint8_t)i;
encrypted = original;
P25Crypto enc;
enc.setMI(mi); enc.setTEKAlgoId(ALGO_AES_256); enc.setKey(key, sizeof(key)); enc.generateKeystream();
enc.cryptAES_PDU(encrypted.data(), (uint32_t)encrypted.size());
REQUIRE(encrypted[300U] != original[300U]);
P25Crypto dec;
dec.setMI(mi); dec.setTEKAlgoId(ALGO_AES_256); dec.setKey(key, sizeof(key)); dec.generateKeystream();
dec.cryptAES_PDU(encrypted.data(), (uint32_t)encrypted.size());
REQUIRE(encrypted == original);
}
TEST_CASE("AES voice encryption retains the AAAD-B IMBE keystream mapping", "[p25][crypto][voice]")
{
// TIA-102.AAAD-B Annex C.4 AES-256 key and MI. The expected first
// LDU1 IMBE mask is octets 12-22 of the usable FDMA keystream after
// discarding the first OFB iteration (AAAD-B Table 5-5).
const uint8_t key[32U] = {
0x01U, 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU,
0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U,
0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U, 0x23U,
0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x01U, 0x23U, 0x45U
};
const uint8_t mi[MI_LENGTH_BYTES] = {
0x12U, 0x34U, 0x56U, 0x78U, 0x90U, 0xABU, 0xCDU, 0xEFU, 0x00U
};
const uint8_t expected[RAW_IMBE_LENGTH_BYTES] = {
0xF5U, 0x71U, 0xCBU, 0x02U, 0x9AU, 0x93U,
0xAEU, 0xFFU, 0x9BU, 0xDBU, 0x7BU
};
uint8_t imbe[RAW_IMBE_LENGTH_BYTES] = { 0U };
P25Crypto vectorCrypto;
vectorCrypto.setMI(mi);
vectorCrypto.setTEKAlgoId(ALGO_AES_256);
vectorCrypto.setKey(key, sizeof(key));
vectorCrypto.generateKeystream();
vectorCrypto.cryptAES_IMBE(imbe, DUID::LDU1);
REQUIRE(::memcmp(imbe, expected, sizeof(expected)) == 0);
for (DUID::E duid : { DUID::LDU1, DUID::LDU2 }) {
std::array<std::array<uint8_t, RAW_IMBE_LENGTH_BYTES>, 9U> plain{};
for (size_t frame = 0U; frame < plain.size(); ++frame)
for (size_t octet = 0U; octet < plain[frame].size(); ++octet)
plain[frame][octet] = (uint8_t)(frame * plain[frame].size() + octet);
auto encrypted = plain;
P25Crypto tx;
tx.setMI(mi); tx.setTEKAlgoId(ALGO_AES_256); tx.setKey(key, sizeof(key)); tx.generateKeystream();
for (auto& frame : encrypted)
tx.cryptAES_IMBE(frame.data(), duid);
REQUIRE(encrypted != plain);
P25Crypto rx;
rx.setMI(mi); rx.setTEKAlgoId(ALGO_AES_256); rx.setKey(key, sizeof(key)); rx.generateKeystream();
for (auto& frame : encrypted)
rx.cryptAES_IMBE(frame.data(), duid);
REQUIRE(encrypted == plain);
}
}

@ -26,33 +26,45 @@ using namespace p25;
using namespace p25::defines;
using namespace p25::kmm;
namespace {
/**
* @brief Test harness for KMM-related FNE functionality.
*/
class KMMFNEHarness {
public:
/**
* @brief Initializes a new instance of the KMMFNEHarness class.
*/
KMMFNEHarness() :
host("fne-kmm-test.yml"),
crypto("", "", "", false, 0U, true),
traffic(&host, "127.0.0.1", 62031U, 999999U, "test-password", "test-fne",
false, false, false, false, true, true, true, true, true,
0U, false, true, true, 5U, 10U, 2U)
{
/* stub */
traffic.setLookups(nullptr, nullptr, nullptr, nullptr, &crypto, nullptr);
}
HostFNE host;
CryptoContainer crypto;
network::TrafficNetwork traffic;
};
template<typename T>
std::vector<uint8_t> encodeKMM(T& frame)
{
std::vector<uint8_t> bytes(frame.fullLength(), 0U);
frame.encode(bytes.data());
return bytes;
namespace {
/**
* @brief Encodes a KMM frame into a byte vector.
* @tparam T The type of the KMM frame.
* @param frame The KMM frame to encode.
* @return A byte vector containing the encoded KMM frame.
*/
template<typename T>
std::vector<uint8_t> encodeKMM(T& frame)
{
std::vector<uint8_t> bytes(frame.fullLength(), 0U);
frame.encode(bytes.data());
return bytes;
}
}
} // namespace
TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm][factory]")
{
SECTION("HELLO frame round-trips with message number") {
@ -107,11 +119,29 @@ TEST_CASE("KMM factory decodes encoded HELLO and NO_SERVICE frames", "[p25][kmm]
}
}
TEST_CASE("KMM air-facing factory rejects malformed lengths", "[p25][kmm][security]")
{
KMMHello hello;
hello.setDstLLId(WUID_FNE);
hello.setSrcLLId(0x654321U);
std::vector<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("FNE OTAR dispatcher follows AACA response-kind procedures", "[p25][kmm][otar]")
{
KMMFNEHarness harness;
constexpr uint32_t SU_RSI = 0x654321U;
constexpr uint32_t KMF_RSI = 0x123456U;
constexpr uint32_t KMF_RSI = WUID_FNE;
SECTION("Response Kind 1 Hello produces no OTAR response") {
KMMHello request;
@ -186,3 +216,154 @@ TEST_CASE("FNE OTAR dispatcher follows AACA response-kind procedures", "[p25][km
REQUIRE(decoded->getSrcLLId() == WUID_FNE);
}
}
TEST_CASE("FNE OTAR authenticates and replay-checks KMM before dispatch", "[p25][kmm][otar][security]")
{
KMMFNEHarness harness;
constexpr uint32_t SU_RSI = 0x654321U;
uint8_t tek[32U];
for (uint32_t i = 0U; i < sizeof(tek); ++i) tek[i] = (uint8_t)i;
EKCKeyItem item;
item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U);
item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
FNETestHooks::addCryptoKey(harness.traffic, item);
auto makeSigned = [&](uint16_t mn, uint8_t flag, uint16_t macFormat = KMM_MAC_FORMAT_CBC) {
KMMHello hello;
hello.setDstLLId(WUID_FNE); hello.setSrcLLId(SU_RSI);
hello.setResponseKind(KMM_ResponseKind::IMMEDIATE);
hello.setHasMessageNumber(true); hello.setMessageNumber(mn); hello.setFlag(flag);
hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256);
hello.setMACKId(0x1234U); hello.setMACFormat(macFormat);
std::vector<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("P25 OTAR DLD processes KMM through the packet-data service entry point", "[p25][kmm][otar][dld]")
{
KMMFNEHarness harness;
constexpr uint32_t SU_RSI = 0x654321U;
constexpr uint16_t MN = 0x1234U;
uint8_t tek[32U];
for (uint32_t i = 0U; i < sizeof(tek); ++i)
tek[i] = (uint8_t)i;
EKCKeyItem item;
item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U);
item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
FNETestHooks::addCryptoKey(harness.traffic, item);
KMMHello hello;
hello.setDstLLId(WUID_FNE);
hello.setSrcLLId(SU_RSI);
hello.setResponseKind(KMM_ResponseKind::NONE);
hello.setHasMessageNumber(true); hello.setMessageNumber(MN);
hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256);
hello.setMACKId(0x1234U); hello.setMACFormat(KMM_MAC_FORMAT_CBC);
hello.setFlag(KMM_HelloFlag::IDENT_ONLY);
std::vector<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));
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 OTAR DLI validates its Version-0 preamble and dispatches KMM", "[p25][kmm][otar][dli]")
{
constexpr uint32_t SU_RSI = 0x654321U;
constexpr uint16_t MN = 0x2345U;
uint8_t tek[32U];
for (uint32_t i = 0U; i < sizeof(tek); ++i)
tek[i] = (uint8_t)i;
auto makeHarnessKey = [](KMMFNEHarness& harness) {
EKCKeyItem item;
item.id(1U); item.algId(ALGO_AES_256); item.kId(0x1234U); item.sln(1U);
item.keyMaterial("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
FNETestHooks::addCryptoKey(harness.traffic, item);
};
KMMHello hello;
hello.setDstLLId(WUID_FNE); hello.setSrcLLId(SU_RSI);
hello.setResponseKind(KMM_ResponseKind::NONE);
hello.setHasMessageNumber(true); hello.setMessageNumber(MN);
hello.setFlag(KMM_HelloFlag::IDENT_ONLY);
hello.setMACType(KMM_MAC::ENH_MAC); hello.setMACAlgId(ALGO_AES_256);
hello.setMACKId(0x1234U); hello.setMACFormat(KMM_MAC_FORMAT_CBC);
std::vector<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::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));
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.