initial support for adding KID ACLs per RID, this change allows the RID ACL list to contain whether or not a RID can request keys from the FNE KMF and wwhether it can request a OTAR rekey (future), it additionally adds a pipe delimited list of KIDs the RID is allowed to request;

r05a06_dev
Bryan Biedenkapp 3 days ago
parent 7326f42c2a
commit 5d7de897f3

@ -222,7 +222,7 @@ Here is a listing of files in the configs folder in this repo that pertain to FN
- `adj_site_map.example.yml` - This is an example configuration file configuring adjacent site mappings for trunked `dvmhost` instances.
- `fne-config.example.yml` - This is the main/primary example configuration file for an FNE instance.
- `peer_list.example.dat` - This is a simple CSV-style file containing access control permissions for peers allowed to connect to the FNE (this includes both downstream peers (like `dvmhost` or `dvmbridge`) and other `dvmfne` instances connecting *to* the FNE instance).
- `rid_acl.example.dat` - This is a simple CSV-style file containing the access control permissions for radio ID (RID)s allowed to use a configured system/network.
- `rid_acl.example.dat` - This is a simple CSV-style file containing the access control permissions for radio ID (RID)s allowed to use a configured system/network. It also carries per-RID key policy for whether a RID can request keys, whether it can be OTAR rekeyed, and an optional allowed key ID list.
- `talkgroup_rules.example.yml` - This is the second most important configuration file for an FNE, this file describes all the talkgroups and their related access control and configuration parameters.
There is another file that is attributed to the FNE that an example is not provided for and that is the `key-container.ekc` file. This file provides cryptographic material needed for providing keyloading functionality across a configured system/network.

@ -7,7 +7,11 @@
# * ENABLED [REQUIRED] - Flag indicating whether or not this radio ID entry is enabled and valid.
# * ALIAS [OPTIONAL] - Textual string representing an alias for this radio ID entry.
# * IP ADDRESS [OPTIONAL] - IP Address assigned to this radio ID.
# * CAN REQUEST KEYS [OPTIONAL] - Flag indicating whether this RID can request keys. (1 = Enabled / 0 = Disabled)
# * CAN REKEY [OPTIONAL] - Flag indicating whether this RID can receive OTAR rekey payloads. (1 = Enabled / 0 = Disabled)
# * ALLOWED KIDS [OPTIONAL] - Pipe-delimited list of allowed 16-bit key IDs for this RID in hexadecimal. (e.g. 0064|0101|0A50)
# If this list is left empty/blank, the RID is assumed to be allowed to request any/all keys.
#
# Entry Format: "RID,Enabled (1 = Enabled / 0 = Disabled),Optional Alias,Optional IP Address,<newline>"
# Entry Format: "RID,Enabled (1 = Enabled / 0 = Disabled),Optional Alias,Optional IP Address,Can Request Keys,Can Rekey,Allowed KIDs,<newline>"
# Example:
#1234,1,RID Alias,IP Address,
#1234,1,RID Alias,IP Address,1,1,0064|0101,

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2022,2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2022,2025-2026 Bryan Biedenkapp, N2PLL
* Copyright (c) 2024 Patrick McDonnell, W3AXL
*
*/
@ -16,9 +16,11 @@
using namespace lookups;
#include <cstdlib>
#include <iomanip>
#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
// ---------------------------------------------------------------------------
// Static Class Members
@ -75,27 +77,30 @@ void RadioIdLookup::clear()
void RadioIdLookup::toggleEntry(uint32_t id, bool enabled)
{
RadioId rid = find(id);
addEntry(id, enabled, rid.radioAlias());
addEntry(id, enabled, rid.radioAlias(), rid.radioIPAddress(), rid.canRequestKeys(), rid.canRekey(), rid.allowedKIds());
}
/* Adds a new entry to the lookup table by the specified unique ID. */
void RadioIdLookup::addEntry(uint32_t id, bool enabled, const std::string& alias, const std::string& ipAddress)
void RadioIdLookup::addEntry(uint32_t id, bool enabled, const std::string& alias, const std::string& ipAddress,
bool canRequestKeys, bool canRekey, const std::vector<uint16_t>& allowedKIds)
{
if ((id == p25::defines::WUID_ALL) || (id == p25::defines::WUID_FNE)) {
return;
}
RadioId entry = RadioId(enabled, false, alias, ipAddress);
RadioId entry = RadioId(enabled, false, alias, ipAddress, canRequestKeys, canRekey, allowedKIds);
__LOCK_TABLE();
try {
RadioId _entry = m_table.at(id);
// if either the alias or the enabled flag doesn't match, update the entry
if (_entry.radioEnabled() != enabled || _entry.radioAlias() != alias) {
// if any configured parameter doesn't match, update the entry
if (_entry.radioEnabled() != enabled || _entry.radioAlias() != alias ||
_entry.radioIPAddress() != ipAddress || _entry.canRequestKeys() != canRequestKeys ||
_entry.canRekey() != canRekey || _entry.allowedKIds() != allowedKIds) {
//LogDebug(LOG_HOST, "Updating existing RID %d (%s) in ACL", id, alias.c_str());
_entry = RadioId(enabled, false, alias, ipAddress);
_entry = RadioId(enabled, false, alias, ipAddress, canRequestKeys, canRekey, allowedKIds);
m_table[id] = _entry;
} else {
//LogDebug(LOG_HOST, "No changes made to RID %d (%s) in ACL", id, alias.c_str());
@ -212,6 +217,9 @@ bool RadioIdLookup::load()
bool radioEnabled = ::atoi(parsed[1].c_str()) == 1;
std::string alias = "";
std::string ipAddress = "";
bool canRequestKeys = false;
bool canRekey = false;
std::vector<uint16_t> allowedKIds;
// check for an optional alias field
if (parsed.size() >= 3) {
@ -223,10 +231,33 @@ bool RadioIdLookup::load()
ipAddress = parsed[3];
}
m_table[id] = RadioId(radioEnabled, false, alias, ipAddress);
// check for optional key-request permission
if (parsed.size() >= 5) {
canRequestKeys = ::atoi(parsed[4].c_str()) == 1;
}
// check for optional rekey permission
if (parsed.size() >= 6) {
canRekey = ::atoi(parsed[5].c_str()) == 1;
}
// check for optional allowed key IDs list (pipe-delimited)
if (parsed.size() >= 7) {
allowedKIds = parseKIdList(parsed[6]);
}
m_table[id] = RadioId(radioEnabled, false, alias, ipAddress, canRequestKeys, canRekey, allowedKIds);
if (m_verbose) {
LogInfoEx(LOG_HOST, "Radio NAME: %s RID: %u ENABLED: %u IPADDR: %s", alias.c_str(), id, radioEnabled, ipAddress.c_str());
std::string kIdList;
if (allowedKIds.empty()) {
kIdList = "ALL";
} else {
kIdList = serializeKIdList(allowedKIds);
}
LogInfoEx(LOG_HOST, "Radio NAME: %s RID: %u ENABLED: %u IPADDR: %s CANREQKEYS: %u CANREKEY: %u ALLOWEDKIDS: %s",
alias.c_str(), id, radioEnabled, ipAddress.c_str(), canRequestKeys, canRekey, kIdList.c_str());
}
}
}
@ -278,21 +309,22 @@ bool RadioIdLookup::save(bool quiet)
bool enabled = entry.second.radioEnabled();
std::string alias = entry.second.radioAlias();
std::string ipAddress = entry.second.radioIPAddress();
bool canRequestKeys = entry.second.canRequestKeys();
bool canRekey = entry.second.canRekey();
std::string allowedKIds = serializeKIdList(entry.second.allowedKIds());
// format into a string
line = std::to_string(rid) + "," + std::to_string(enabled) + ",";
// add the alias if we have one
if (alias.length() > 0) {
line += alias;
line += ",";
}
// add the IP address if we have one
if (ipAddress.length() > 0) {
line += ipAddress;
line += ",";
}
line += alias;
line += ",";
line += ipAddress;
line += ",";
line += std::to_string(canRequestKeys);
line += ",";
line += std::to_string(canRekey);
line += ",";
line += allowedKIds;
line += ",";
line += "\n";
file << line;
@ -309,3 +341,59 @@ bool RadioIdLookup::save(bool quiet)
return true;
}
/* Parses a string of KIDs from the lookup file into a vector of uint16_t KIDs. */
std::vector<uint16_t> RadioIdLookup::parseKIdList(const std::string& input)
{
std::vector<uint16_t> kids;
if (input.empty()) {
return kids;
}
// tokenize the input string by pipe delimiter and parse each token as a hex value for a KID, ensuring no
// duplicates and that each KID is in the valid range of 0x0000 to 0xFFFF
std::stringstream ss(input);
std::string token;
while (std::getline(ss, token, '|')) {
if (token.empty()) {
continue;
}
// parse token as hex value for KID
uint32_t value = (uint32_t)::strtoul(token.c_str(), nullptr, 16);
if (value > 0xFFFFU) {
continue;
}
uint16_t kid = (uint16_t)value;
// check for duplicates before adding to the list
if (std::find(kids.begin(), kids.end(), kid) == kids.end()) {
kids.push_back(kid);
}
}
return kids;
}
/* Serializes a list of KIDs into a string for storage in the lookup file. */
std::string RadioIdLookup::serializeKIdList(const std::vector<uint16_t>& kids)
{
if (kids.empty()) {
return "";
}
// serialize the list of KIDs into a pipe-delimited string
std::stringstream ss;
for (size_t i = 0U; i < kids.size(); i++) {
ss << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << kids[i];
if (i + 1U < kids.size()) {
ss << "|";
}
}
return ss.str();
}

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2022,2024,2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2022,2024,2025-2026 Bryan Biedenkapp, N2PLL
* Copyright (c) 2024 Patrick McDonnell, W3AXL
*
*/
@ -27,6 +27,7 @@
#include <string>
#include <unordered_map>
#include <vector>
namespace lookups
{
@ -46,7 +47,11 @@ namespace lookups
RadioId() :
m_radioEnabled(false),
m_radioDefault(false),
m_radioAlias("")
m_radioAlias(""),
m_radioIPAddress(""),
m_canRequestKeys(false),
m_canRekey(false),
m_allowedKIds()
{
/* stub */
}
@ -60,7 +65,10 @@ namespace lookups
m_radioEnabled(radioEnabled),
m_radioDefault(radioDefault),
m_radioAlias(""),
m_radioIPAddress("")
m_radioIPAddress(""),
m_canRequestKeys(false),
m_canRekey(false),
m_allowedKIds()
{
/* stub */
}
@ -72,11 +80,15 @@ namespace lookups
* @param radioAlias Textual alias for the radio.
* @param ipAddress Textual IP Address for the radio.
*/
RadioId(bool radioEnabled, bool radioDefault, const std::string& radioAlias, const std::string& ipAddress = "") :
RadioId(bool radioEnabled, bool radioDefault, const std::string& radioAlias, const std::string& ipAddress = "",
bool canRequestKeys = false, bool canRekey = false, const std::vector<uint16_t>& allowedKIds = std::vector<uint16_t>()) :
m_radioEnabled(radioEnabled),
m_radioDefault(radioDefault),
m_radioAlias(radioAlias),
m_radioIPAddress(ipAddress)
m_radioIPAddress(ipAddress),
m_canRequestKeys(canRequestKeys),
m_canRekey(canRekey),
m_allowedKIds(allowedKIds)
{
/* stub */
}
@ -92,6 +104,9 @@ namespace lookups
m_radioDefault = data.m_radioDefault;
m_radioAlias = data.m_radioAlias;
m_radioIPAddress = data.m_radioIPAddress;
m_canRequestKeys = data.m_canRequestKeys;
m_canRekey = data.m_canRekey;
m_allowedKIds = data.m_allowedKIds;
}
return *this;
@ -104,12 +119,16 @@ namespace lookups
* @param radioAlias Textual alias for the radio.
* @param ipAddress Textual IP Address for the radio.
*/
void set(bool radioEnabled, bool radioDefault, const std::string& radioAlias, const std::string& ipAddress = "")
void set(bool radioEnabled, bool radioDefault, const std::string& radioAlias, const std::string& ipAddress = "",
bool canRequestKeys = false, bool canRekey = false, const std::vector<uint16_t>& allowedKIds = std::vector<uint16_t>())
{
m_radioEnabled = radioEnabled;
m_radioDefault = radioDefault;
m_radioAlias = radioAlias;
m_radioIPAddress = ipAddress;
m_canRequestKeys = canRequestKeys;
m_canRekey = canRekey;
m_allowedKIds = allowedKIds;
}
public:
@ -129,6 +148,18 @@ namespace lookups
* @brief IP Address for the radio.
*/
DECLARE_RO_PROPERTY_PLAIN(std::string, radioIPAddress);
/**
* @brief Flag indicating if the radio can request keys from FNE.
*/
DECLARE_RO_PROPERTY_PLAIN(bool, canRequestKeys);
/**
* @brief Flag indicating if the radio can receive OTAR rekey command payloads.
*/
DECLARE_RO_PROPERTY_PLAIN(bool, canRekey);
/**
* @brief Allowed encryption key IDs for this radio.
*/
DECLARE_RO_PROPERTY_PLAIN(std::vector<uint16_t>, allowedKIds);
};
// ---------------------------------------------------------------------------
@ -170,7 +201,8 @@ namespace lookups
* @param alias Alias for the radio ID
* @param ipAddress IP Address for Radio
*/
void addEntry(uint32_t id, bool enabled, const std::string& alias, const std::string& ipAddress = "");
void addEntry(uint32_t id, bool enabled, const std::string& alias, const std::string& ipAddress = "",
bool canRequestKeys = false, bool canRekey = false, const std::vector<uint16_t>& allowedKIds = std::vector<uint16_t>());
/**
* @brief Erases an existing entry from the lookup table by the specified unique ID.
* @param id Unique ID to erase.
@ -226,6 +258,19 @@ namespace lookups
private:
static std::mutex s_mutex; //!< Mutex used for change locking.
static bool s_locked; //!< Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
/**
* @brief Parses a string of KIDs from the lookup file into a vector of uint16_t KIDs.
* @param input String representation of KID list from the lookup file.
* @returns Vector of KIDs parsed from the input string.
*/
std::vector<uint16_t> parseKIdList(const std::string& input);
/**
* @brief Serializes a list of KIDs into a string for storage in the lookup file.
* @param kids List of KIDs to serialize.
* @returns String representation of the KID list.
*/
std::string serializeKIdList(const std::vector<uint16_t>& kids);
};
} // namespace lookups

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2025-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
@ -28,6 +28,7 @@ using namespace p25::kmm;
#include <cassert>
#include <chrono>
#include <algorithm>
// ---------------------------------------------------------------------------
// Macros
@ -456,6 +457,22 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
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);
}
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);
}
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);
}
// send rekey-command
EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(llId);
if (keyItem.isInvalid()) {
@ -615,6 +632,15 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
outKmm.setAlgId(kekAlgId);
outKmm.setKId(kekKId);
lookups::RadioId ridEntry = m_network->m_ridLookup->find(kmmRSI);
std::vector<uint16_t> allowedKIds;
if (ridEntry.radioDefault()) {
LogWarning(LOG_P25, P25_KMM_STR ", %s, aborting rekey, RID %u has no key policy entry", outKmm.toString().c_str(), kmmRSI);
return nullptr;
}
allowedKIds = ridEntry.allowedKIds();
KeysetItem ks;
ks.keysetId(1U);
ks.algId(ALGO_AES_256); // we currently can only OTAR AES256 keys
@ -630,6 +656,15 @@ UInt8Array P25OTARService::write_KMM_Rekey_Command(uint32_t llId, uint32_t kmmRS
continue;
}
// check if this keyItem is allowed for the RID
if (!allowedKIds.empty() && std::find(allowedKIds.begin(), allowedKIds.end(), (uint16_t)keyItem.kId()) == allowedKIds.end()) {
if (m_verbose) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, skipping kId = %u; not allowed for RID %u", outKmm.toString().c_str(),
keyItem.kId(), kmmRSI);
}
continue;
}
uint8_t key[P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES];
::memset(key, 0x00U, P25DEF::MAX_WRAPPED_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);

@ -37,6 +37,7 @@ using namespace compress;
#include <streambuf>
// ---------------------------------------------------------------------------
#include <algorithm>
// Constants
// ---------------------------------------------------------------------------
@ -1807,6 +1808,35 @@ void TrafficNetwork::taskNetworkRx(NetPacketRequest* req)
{
KMMModifyKey* modifyKey = static_cast<KMMModifyKey*>(frame.get());
if (modifyKey->getAlgId() > 0U && modifyKey->getKId() > 0U) {
uint32_t requestingRid = modifyKey->getSrcLLId();
lookups::RadioId ridEntry = network->m_ridLookup->find(requestingRid);
if (ridEntry.radioDefault()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u has no key policy entry, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
break;
}
if (!ridEntry.radioEnabled()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u is disabled, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
break;
}
if (!ridEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u cannot request keys, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
break;
}
std::vector<uint16_t> allowedKIds = ridEntry.allowedKIds();
// check if this RID is allowed to request the KID in question
if (!allowedKIds.empty() && std::find(allowedKIds.begin(), allowedKIds.end(), modifyKey->getKId()) == allowedKIds.end()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key kID = $%04X but RID %u is not permitted for that key, no response",
peerId, connection->identWithQualifier().c_str(), modifyKey->getKId(), requestingRid);
break;
}
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());

@ -1025,6 +1025,16 @@ void RESTAPI::restAPI_GetRIDQuery(const HTTPPayload& request, HTTPPayload& reply
ridObj["enabled"].set<bool>(enabled);
std::string alias = entry.second.radioAlias();
ridObj["alias"].set<std::string>(alias);
bool canRequestKeys = entry.second.canRequestKeys();
ridObj["canRequestKeys"].set<bool>(canRequestKeys);
bool canRekey = entry.second.canRekey();
ridObj["canRekey"].set<bool>(canRekey);
json::array allowedKIds = json::array();
std::vector<uint16_t> kIds = entry.second.allowedKIds();
for (uint16_t kId : kIds) {
allowedKIds.push_back(json::value((double)kId));
}
ridObj["allowedKIds"].set<json::array>(allowedKIds);
rids.push_back(json::value(ridObj));
}
@ -1070,10 +1080,54 @@ void RESTAPI::restAPI_PutRIDAdd(const HTTPPayload& request, HTTPPayload& reply,
alias = req["alias"].get<std::string>();
}
bool canRequestKeys = false;
if (req.find("canRequestKeys") != req.end()) {
if (!req["canRequestKeys"].is<bool>()) {
errorPayload(reply, "canRequestKeys was not a valid boolean");
return;
}
canRequestKeys = req["canRequestKeys"].get<bool>();
}
bool canRekey = false;
if (req.find("canRekey") != req.end()) {
if (!req["canRekey"].is<bool>()) {
errorPayload(reply, "canRekey was not a valid boolean");
return;
}
canRekey = req["canRekey"].get<bool>();
}
std::vector<uint16_t> allowedKIds;
if (req.find("allowedKIds") != req.end()) {
if (!req["allowedKIds"].is<json::array>()) {
errorPayload(reply, "allowedKIds was not a valid JSON array");
return;
}
json::array kIdArray = req["allowedKIds"].get<json::array>();
for (auto entry : kIdArray) {
if (!entry.is<uint32_t>()) {
errorPayload(reply, "allowedKIds entry was not a valid number");
return;
}
uint32_t value = entry.get<uint32_t>();
if (value > 0xFFFFU) {
errorPayload(reply, "allowedKIds entry exceeded 16-bit key id range");
return;
}
allowedKIds.push_back((uint16_t)value);
}
}
LogInfoEx(LOG_REST, "request to add RID ACL, rid = %u", rid);
// The addEntry function will automatically update an existing entry, so no need to check for an exisitng one here
m_ridLookup->addEntry(rid, enabled, alias);
m_ridLookup->addEntry(rid, enabled, alias, "", canRequestKeys, canRekey, allowedKIds);
/*
if (m_network != nullptr) {
m_network->m_forceListUpdate = true;

Loading…
Cancel
Save

Powered by TurnKey Linux.