Allow key verification to work for unknown nodes. (#10669)
* Allow key verification to work for unknown nodes. * trunk * More reliable admin key decryption * Add admin key fallback tests * Actually check haveRemoteKey * Logging cleanup * Address review feedback - Persist the committed key + manually-verified flag in commitVerifiedRemoteNode via saveToDisk(SEGMENT_NODEDATABASE), replacing the "todo: initiate save" - Guard the CryptoEngine pending-key slot with a dedicated internal lock; the Router reads it while already holding the non-recursive cryptLock, so the accessors cannot reuse that lock - Draw the security number from the hardware RNG (CryptRNG fallback) under cryptLock instead of random(); on nRF52 the entropy fill toggles the same CC310 the BLE task's packet crypto uses - Return true after fully handling the hash2 response (restores develop behavior; consistent with the hash1 branch) - Take uint32_t in the number-picker callbacks so 8-digit hex nodenums can't truncate through int - Trim over-long comments flagged by review * feat(tests): add deterministic tests for admin session-key behavior --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Ben Meadors
parent
fc91a69ca4
commit
952c825167
@@ -343,7 +343,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called to trigger a banner with custom message and duration
|
// Called to trigger a banner with custom message and duration
|
||||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||||
std::function<void(uint32_t)> bannerCallback)
|
std::function<void(uint32_t)> bannerCallback)
|
||||||
{
|
{
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
@@ -356,7 +356,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
NotificationRenderer::pauseBanner = false;
|
NotificationRenderer::pauseBanner = false;
|
||||||
NotificationRenderer::curSelected = 0;
|
NotificationRenderer::curSelected = 0;
|
||||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
if (useBase16)
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||||
|
else
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||||
NotificationRenderer::numDigits = digits;
|
NotificationRenderer::numDigits = digits;
|
||||||
NotificationRenderer::currentNumber = 0;
|
NotificationRenderer::currentNumber = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -344,7 +344,8 @@ class Screen : public concurrency::OSThread
|
|||||||
void showOverlayBanner(BannerOverlayOptions);
|
void showOverlayBanner(BannerOverlayOptions);
|
||||||
|
|
||||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||||
|
std::function<void(uint32_t)> bannerCallback);
|
||||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback);
|
std::function<void(const std::string &)> textCallback);
|
||||||
|
|
||||||
|
|||||||
@@ -2323,8 +2323,10 @@ void menuHandler::testMenu()
|
|||||||
|
|
||||||
void menuHandler::numberTest()
|
void menuHandler::numberTest()
|
||||||
{
|
{
|
||||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](uint32_t number_picked) -> void {
|
||||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
LOG_DEBUG("Nodenum: 0x%08x", number_picked);
|
||||||
|
keyVerificationModule->sendInitialRequest(number_picked);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::wifiBaseMenu()
|
void menuHandler::wifiBaseMenu()
|
||||||
@@ -2508,8 +2510,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
|||||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||||
options.bannerCallback = [=](int selected) {
|
options.bannerCallback = [=](int selected) {
|
||||||
if (selected == 1) {
|
if (selected == 1) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
keyVerificationModule->commitVerifiedRemoteNode();
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(options);
|
screen->showOverlayBanner(options);
|
||||||
|
|||||||
@@ -341,6 +341,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(&pendingKeyLock);
|
||||||
|
pendingKeyVerificationNode = node;
|
||||||
|
memcpy(pendingKeyVerificationPublicKey, key, 32);
|
||||||
|
hasPendingKeyVerificationKey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CryptoEngine::clearPendingPublicKey()
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(&pendingKeyLock);
|
||||||
|
pendingKeyVerificationNode = 0;
|
||||||
|
memset(pendingKeyVerificationPublicKey, 0, 32);
|
||||||
|
hasPendingKeyVerificationKey = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(&pendingKeyLock);
|
||||||
|
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
|
||||||
|
return false;
|
||||||
|
out.size = 32;
|
||||||
|
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
concurrency::Lock *cryptLock;
|
concurrency::Lock *cryptLock;
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
|||||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||||
|
|
||||||
|
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
|
||||||
|
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
|
||||||
|
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
|
||||||
|
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
|
||||||
|
// Internally guarded by pendingKeyLock, not cryptLock: the Router calls the getter while
|
||||||
|
// already holding the non-recursive cryptLock; KeyVerificationModule writes from elsewhere.
|
||||||
|
void setPendingPublicKey(uint32_t node, const uint8_t *key);
|
||||||
|
void clearPendingPublicKey();
|
||||||
|
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
|
||||||
|
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
|
||||||
|
|
||||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||||
|
|
||||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
|||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
uint8_t shared_key[32] = {0};
|
uint8_t shared_key[32] = {0};
|
||||||
uint8_t private_key[32] = {0};
|
uint8_t private_key[32] = {0};
|
||||||
|
uint32_t pendingKeyVerificationNode = 0;
|
||||||
|
uint8_t pendingKeyVerificationPublicKey[32] = {0};
|
||||||
|
bool hasPendingKeyVerificationKey = false;
|
||||||
|
concurrency::Lock pendingKeyLock;
|
||||||
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
#if !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
uint8_t xeddsa_public_key[32] = {0};
|
uint8_t xeddsa_public_key[32] = {0};
|
||||||
uint8_t xeddsa_private_key[32] = {0};
|
uint8_t xeddsa_private_key[32] = {0};
|
||||||
|
|||||||
+56
-19
@@ -524,36 +524,60 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
bool decrypted = false;
|
bool decrypted = false;
|
||||||
ChannelIndex chIndex = 0;
|
ChannelIndex chIndex = 0;
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||||
// there), so DMs from long-tail nodes still decrypt.
|
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->copyPublicKey(p->from, fromKey) &&
|
|
||||||
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
|
|
||||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
|
||||||
LOG_DEBUG("Attempt PKI decryption");
|
|
||||||
|
|
||||||
if (crypto->decryptCurve25519(p->from, fromKey, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
meshtastic_NodeInfoLite *ourNode = nullptr;
|
||||||
|
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && rawSize > MESHTASTIC_PKC_OVERHEAD &&
|
||||||
|
(ourNode = nodeDB->getMeshNode(p->to)) != nullptr && ourNode->public_key.size > 0) {
|
||||||
|
// Try the sender's known key first, then each configured admin key so an authorized admin can
|
||||||
|
// reach a node that has not yet learned their key. AES-CCM AEAD rejects wrong candidates.
|
||||||
|
bool viaAdminKey = false;
|
||||||
|
if (haveRemoteKey && crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||||
|
decrypted = true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3 && !decrypted; i++) {
|
||||||
|
if (config.security.admin_key[i].size != 32)
|
||||||
|
continue;
|
||||||
|
remotePublic.size = 32;
|
||||||
|
memcpy(remotePublic.bytes, config.security.admin_key[i].bytes, 32);
|
||||||
|
|
||||||
|
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||||
|
decrypted = true;
|
||||||
|
viaAdminKey = true;
|
||||||
|
break; // stop after first successful decryption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decrypted) {
|
||||||
LOG_INFO("PKI Decryption worked!");
|
LOG_INFO("PKI Decryption worked!");
|
||||||
|
|
||||||
meshtastic_Data decodedtmp;
|
meshtastic_Data decodedtmp;
|
||||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||||
decrypted = true;
|
decrypted = true;
|
||||||
|
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||||
LOG_INFO("Packet decrypted using PKI!");
|
LOG_INFO("Packet decrypted using PKI!");
|
||||||
p->pki_encrypted = true;
|
p->pki_encrypted = true;
|
||||||
memcpy(p->public_key.bytes, fromKey.bytes, 32);
|
memcpy(p->public_key.bytes, remotePublic.bytes, 32);
|
||||||
p->public_key.size = 32;
|
p->public_key.size = 32;
|
||||||
p->decoded = decodedtmp;
|
p->decoded = decodedtmp;
|
||||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||||
|
if (viaAdminKey) {
|
||||||
|
// Persist the admin key for the sender so future packets take the fast path and we can
|
||||||
|
// PKI-reply; p->from is bound into the AEAD nonce, so the trusted admin authenticated it.
|
||||||
|
meshtastic_NodeInfoLite *fromNode = nodeDB->getOrCreateMeshNode(p->from);
|
||||||
|
if (fromNode != nullptr)
|
||||||
|
fromNode->public_key = remotePublic;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// AEAD already authenticated this ciphertext, so no other candidate could decode it -
|
||||||
|
// the payload is simply malformed.
|
||||||
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
|
||||||
return DecodeState::DECODE_FAILURE;
|
return DecodeState::DECODE_FAILURE;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LOG_WARN("PKC decrypt attempted but failed!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -758,10 +782,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// Destination key from the hot store or the warm tier (evicted
|
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||||
// long-tail nodes keep their key there)
|
// long-tail nodes keep their key there), otherwise (for a key-verification follow-on packet
|
||||||
|
// that explicitly requested PKI) fall back to the not-yet-verified key held during an
|
||||||
|
// in-progress handshake. This lets us DH-encode the follow-on packet before the peer's key
|
||||||
|
// has been committed to NodeDB.
|
||||||
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
|
||||||
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
bool haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
|
||||||
|
if (!haveDestKey && p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
|
||||||
|
crypto->getPendingPublicKey(p->to, destKey)) {
|
||||||
|
haveDestKey = true;
|
||||||
|
}
|
||||||
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
||||||
// is not in the local nodedb
|
// is not in the local nodedb
|
||||||
// First, only PKC encrypt packets we are originating
|
// First, only PKC encrypt packets we are originating
|
||||||
@@ -779,11 +810,17 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
|||||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||||
// Some portnums either make no sense to send with PKC
|
// Some portnums either make no sense to send with PKC
|
||||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
|
||||||
|
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
|
||||||
|
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
|
||||||
|
// uses the pending key resolved into haveDestKey/destKey above.
|
||||||
|
// Though possible the first packet each direction should go non-pkc
|
||||||
|
// to handle the case where the remote node has our key, but we don't have theirs.
|
||||||
|
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
|
||||||
LOG_DEBUG("Use PKI!");
|
LOG_DEBUG("Use PKI!");
|
||||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||||
return meshtastic_Routing_Error_TOO_LARGE;
|
return meshtastic_Routing_Error_TOO_LARGE;
|
||||||
// Check for a known public key for the destination
|
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||||
if (!haveDestKey) {
|
if (!haveDestKey) {
|
||||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||||
p->decoded.portnum);
|
p->decoded.portnum);
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
#if !MESHTASTIC_EXCLUDE_PKI
|
#if !MESHTASTIC_EXCLUDE_PKI
|
||||||
#include "KeyVerificationModule.h"
|
#include "KeyVerificationModule.h"
|
||||||
|
#include "CryptoEngine.h"
|
||||||
|
#include "HardwareRNG.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "graphics/draw/MenuHandler.h"
|
#include "graphics/draw/MenuHandler.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
|
#include "modules/NodeInfoModule.h"
|
||||||
|
#include <RNG.h>
|
||||||
#include <SHA256.h>
|
#include <SHA256.h>
|
||||||
|
|
||||||
KeyVerificationModule *keyVerificationModule;
|
KeyVerificationModule *keyVerificationModule;
|
||||||
@@ -34,7 +38,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
{
|
{
|
||||||
updateState();
|
updateState();
|
||||||
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
|
if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) {
|
||||||
LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
LOG_DEBUG("Handling Key Verification Admin Message type %u", request->key_verification.message_type);
|
||||||
|
|
||||||
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
|
if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION &&
|
||||||
currentState == KEY_VERIFICATION_IDLE) {
|
currentState == KEY_VERIFICATION_IDLE) {
|
||||||
@@ -48,9 +52,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
|
|
||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||||
request->key_verification.nonce == currentNonce) {
|
request->key_verification.nonce == currentNonce) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
commitVerifiedRemoteNode();
|
||||||
if (remoteNodePtr)
|
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
@@ -63,9 +65,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
|||||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||||
{
|
{
|
||||||
updateState();
|
updateState();
|
||||||
if (mp.pki_encrypted == false) {
|
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||||
return false;
|
// the bootstrap case; the follow-on hash1 packet (M3) is required to be PKI in its branch below.
|
||||||
}
|
|
||||||
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -74,9 +75,14 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||||
r->hash1.size == 0) {
|
r->hash1.size == 32) {
|
||||||
memcpy(hash2, r->hash2.bytes, 32);
|
memcpy(hash2, r->hash2.bytes, 32);
|
||||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
// The response carries the responder's public key in hash1. If we don't already hold it, stash it
|
||||||
|
// as a pending key so the Router can PKI-encrypt our follow-on packet (committed to NodeDB on accept).
|
||||||
|
auto *responderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
if (responderNode == nullptr || responderNode->public_key.size != 32)
|
||||||
|
crypto->setPendingPublicKey(currentRemoteNode, r->hash1.bytes);
|
||||||
|
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, false, [](uint32_t number_picked) -> void {
|
||||||
keyVerificationModule->processSecurityNumber(number_picked);
|
keyVerificationModule->processSecurityNumber(number_picked);
|
||||||
});)
|
});)
|
||||||
|
|
||||||
@@ -95,7 +101,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
|
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
|
||||||
|
r->nonce == currentNonce) {
|
||||||
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
||||||
memset(message, 0, sizeof(message));
|
memset(message, 0, sizeof(message));
|
||||||
sprintf(message, "Verification: \n");
|
sprintf(message, "Verification: \n");
|
||||||
@@ -108,10 +115,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||||
options.bannerCallback =
|
options.bannerCallback =
|
||||||
[=](int selected) {
|
[=](int selected) {
|
||||||
|
LOG_DEBUG("User selected %d for key verification", selected);
|
||||||
if (selected == 1) {
|
if (selected == 1) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
keyVerificationModule->commitVerifiedRemoteNode();
|
||||||
if (remoteNodePtr)
|
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(options);)
|
screen->showOverlayBanner(options);)
|
||||||
@@ -139,22 +145,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
|||||||
{
|
{
|
||||||
LOG_DEBUG("keyVerification start");
|
LOG_DEBUG("keyVerification start");
|
||||||
// generate nonce
|
// generate nonce
|
||||||
updateState();
|
updateState(false);
|
||||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
updateState(true);
|
||||||
currentNonce = random();
|
currentNonce = random();
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
currentRemoteNode = remoteNode;
|
currentRemoteNode = remoteNode;
|
||||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||||
KeyVerification.nonce = currentNonce;
|
KeyVerification.nonce = currentNonce;
|
||||||
KeyVerification.hash2.size = 0;
|
KeyVerification.hash2.size = 0;
|
||||||
KeyVerification.hash1.size = 0;
|
// Carry our public key in the otherwise-unused hash1 field so a peer that does not yet hold our
|
||||||
|
// key can learn it from this first message (bootstrap / onboarding).
|
||||||
|
KeyVerification.hash1.size = 32;
|
||||||
|
memcpy(KeyVerification.hash1.bytes, owner.public_key.bytes, 32);
|
||||||
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
||||||
p->to = remoteNode;
|
p->to = remoteNode;
|
||||||
p->channel = 0;
|
p->channel = 0;
|
||||||
p->pki_encrypted = true;
|
// Only request PKI when we already hold the destination's key. Otherwise this first message goes out
|
||||||
|
// channel-encrypted (the Router falls back) so the peer can bootstrap from the key carried in hash1.
|
||||||
|
auto *remoteNodePtr = nodeDB->getMeshNode(remoteNode);
|
||||||
|
p->pki_encrypted = (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32);
|
||||||
p->decoded.want_response = true;
|
p->decoded.want_response = true;
|
||||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||||
@@ -171,9 +184,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||||
LOG_WARN("Key Verification requested, but already in a request");
|
LOG_WARN("Key Verification requested, but already in a request");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
} else if (!currentRequest->pki_encrypted) {
|
|
||||||
LOG_WARN("Key Verification requested, but not in a PKI packet");
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
||||||
|
|
||||||
@@ -188,15 +198,43 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
response.nonce = scratch.nonce;
|
response.nonce = scratch.nonce;
|
||||||
currentRemoteNode = req.from;
|
currentRemoteNode = req.from;
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
currentSecurityNumber = random(1, 999999);
|
// The security number is the handshake's MitM-resistance entropy, so draw it from the hardware
|
||||||
|
// RNG (falling back to the CSPRNG used for key material), never the predictable random().
|
||||||
|
// Hold cryptLock like xeddsa_sign does: on nRF52 the fill toggles the CC310 that packet crypto
|
||||||
|
// on the BLE task also uses, and the CryptRNG state is shared with the signing path.
|
||||||
|
uint32_t securityEntropy = 0;
|
||||||
|
{
|
||||||
|
concurrency::LockGuard g(cryptLock);
|
||||||
|
if (!HardwareRNG::fill((uint8_t *)&securityEntropy, sizeof(securityEntropy)))
|
||||||
|
CryptRNG.rand((uint8_t *)&securityEntropy, sizeof(securityEntropy));
|
||||||
|
}
|
||||||
|
currentSecurityNumber = (securityEntropy % 999999) + 1;
|
||||||
|
|
||||||
// generate hash1
|
// Resolve the requester's public key: from the PKI envelope, else carried in hash1 (bootstrap).
|
||||||
|
// Stash unknown keys as pending (committed to NodeDB only once verification is accepted).
|
||||||
|
const uint8_t *senderKey = nullptr;
|
||||||
|
if (currentRequest->pki_encrypted && currentRequest->public_key.size == 32) {
|
||||||
|
senderKey = currentRequest->public_key.bytes; // this is bizarre, fixme
|
||||||
|
} else if (scratch.hash1.size == 32) {
|
||||||
|
senderKey = scratch.hash1.bytes;
|
||||||
|
}
|
||||||
|
if (senderKey == nullptr) {
|
||||||
|
LOG_WARN("Key Verification request without a usable public key");
|
||||||
|
resetToIdle();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto *senderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
bool senderKeyInNodeDB = (senderNode != nullptr && senderNode->public_key.size == 32);
|
||||||
|
if (!senderKeyInNodeDB)
|
||||||
|
crypto->setPendingPublicKey(currentRemoteNode, senderKey);
|
||||||
|
|
||||||
|
// generate local hash1
|
||||||
hash.reset();
|
hash.reset();
|
||||||
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||||
hash.update(¤tNonce, sizeof(currentNonce));
|
hash.update(¤tNonce, sizeof(currentNonce));
|
||||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
hash.update(senderKey, 32);
|
||||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||||
hash.finalize(hash1, 32);
|
hash.finalize(hash1, 32);
|
||||||
|
|
||||||
@@ -205,15 +243,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
hash.update(¤tNonce, sizeof(currentNonce));
|
hash.update(¤tNonce, sizeof(currentNonce));
|
||||||
hash.update(hash1, 32);
|
hash.update(hash1, 32);
|
||||||
hash.finalize(hash2, 32);
|
hash.finalize(hash2, 32);
|
||||||
response.hash1.size = 0;
|
// Carry our public key in hash1 of the response so the requester can bootstrap our key as well.
|
||||||
|
response.hash1.size = 32;
|
||||||
|
memcpy(response.hash1.bytes, owner.public_key.bytes, 32);
|
||||||
response.hash2.size = 32;
|
response.hash2.size = 32;
|
||||||
memcpy(response.hash2.bytes, hash2, 32);
|
memcpy(response.hash2.bytes, hash2, 32);
|
||||||
|
|
||||||
responsePacket = allocDataProtobuf(response);
|
responsePacket = allocDataProtobuf(response);
|
||||||
|
|
||||||
responsePacket->pki_encrypted = true;
|
// PKI-encrypt the response only if we already held the requester's key. In the bootstrap case it goes
|
||||||
|
// out channel-encrypted so the requester (who lacks our key) can decode it and read hash1.
|
||||||
|
responsePacket->pki_encrypted = senderKeyInNodeDB;
|
||||||
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
screen->showSimpleBanner(message, 30000); LOG_DEBUG("%s", message);)
|
||||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||||
if (cn) {
|
if (cn) {
|
||||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||||
@@ -227,7 +269,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
|||||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||||
service->sendClientNotification(cn);
|
service->sendClientNotification(cn);
|
||||||
}
|
}
|
||||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
LOG_DEBUG("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||||
return responsePacket;
|
return responsePacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +278,18 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
SHA256 hash;
|
SHA256 hash;
|
||||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||||
uint8_t scratch_hash[32] = {0};
|
uint8_t scratch_hash[32] = {0};
|
||||||
LOG_WARN("received security number: %u", incomingNumber);
|
LOG_DEBUG("received security number: %u", incomingNumber);
|
||||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
// handshake (bootstrap case).
|
||||||
currentState = KEY_VERIFICATION_IDLE;
|
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||||
return; // should we throw an error here?
|
if (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32) {
|
||||||
|
remotePublic = remoteNodePtr->public_key;
|
||||||
|
} else if (!crypto->getPendingPublicKey(currentRemoteNode, remotePublic)) {
|
||||||
|
LOG_WARN("No public key available for remote node, aborting key verification");
|
||||||
|
resetToIdle();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
LOG_WARN("hashing ");
|
|
||||||
// calculate hash1
|
// calculate hash1
|
||||||
hash.reset();
|
hash.reset();
|
||||||
hash.update(&incomingNumber, sizeof(incomingNumber));
|
hash.update(&incomingNumber, sizeof(incomingNumber));
|
||||||
@@ -252,7 +298,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||||
|
|
||||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
hash.update(remotePublic.bytes, 32);
|
||||||
hash.finalize(hash1, 32);
|
hash.finalize(hash1, 32);
|
||||||
|
|
||||||
hash.reset();
|
hash.reset();
|
||||||
@@ -297,13 +343,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyVerificationModule::updateState()
|
void KeyVerificationModule::updateState(bool resetTimer)
|
||||||
{
|
{
|
||||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||||
// check for the 60 second timeout
|
// check for the 60 second timeout
|
||||||
if (currentNonceTimestamp < getTime() - 60) {
|
if (currentNonceTimestamp < getTime() - 60) {
|
||||||
resetToIdle();
|
resetToIdle();
|
||||||
} else {
|
} else if (resetTimer) {
|
||||||
currentNonceTimestamp = getTime();
|
currentNonceTimestamp = getTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +364,32 @@ void KeyVerificationModule::resetToIdle()
|
|||||||
currentSecurityNumber = 0;
|
currentSecurityNumber = 0;
|
||||||
currentRemoteNode = 0;
|
currentRemoteNode = 0;
|
||||||
currentState = KEY_VERIFICATION_IDLE;
|
currentState = KEY_VERIFICATION_IDLE;
|
||||||
|
// Discard any not-yet-verified key learned during this handshake; on reject/timeout it is never trusted.
|
||||||
|
crypto->clearPendingPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyVerificationModule::commitVerifiedRemoteNode()
|
||||||
|
{
|
||||||
|
// The remote node already has a NodeDB entry by this point (packets were exchanged during the
|
||||||
|
// handshake), so getMeshNode is sufficient; bail defensively if it is somehow absent.
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
|
if (!node) {
|
||||||
|
LOG_WARN("Attempted to commit key, but unknown node");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If we only held the peer's key as a pending (unverified) key during the handshake, commit it to
|
||||||
|
// NodeDB now that the user has confirmed the verification, so future PKI traffic can use it.
|
||||||
|
meshtastic_NodeInfoLite_public_key_t pending = {0, {0}};
|
||||||
|
if (node->public_key.size != 32 && crypto->getPendingPublicKey(currentRemoteNode, pending))
|
||||||
|
node->public_key = pending;
|
||||||
|
node->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
|
LOG_INFO("Node 0x%08x manually verified with security number %u", currentRemoteNode, currentSecurityNumber);
|
||||||
|
if (nodeInfoModule)
|
||||||
|
nodeInfoModule->sendOurNodeInfo(currentRemoteNode, false, node->channel, true);
|
||||||
|
crypto->clearPendingPublicKey();
|
||||||
|
currentState = KEY_VERIFICATION_IDLE;
|
||||||
|
// Persist the committed key and verified flag so manual verification survives a reboot.
|
||||||
|
nodeDB->saveToDisk(SEGMENT_NODEDATABASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ enum KeyVerificationState {
|
|||||||
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// KeyVerification Module overview
|
||||||
|
// This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy
|
||||||
|
// connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so
|
||||||
|
// it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the
|
||||||
|
// initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new
|
||||||
|
// node even if they don't have that node in the local nodeDB at all.
|
||||||
|
|
||||||
|
// The handshake process is as follows (NodeA = initiator, NodeB = responder):
|
||||||
|
// 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the
|
||||||
|
// hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already
|
||||||
|
// holds NodeB's key, otherwise channel-encrypted (the bootstrap case).
|
||||||
|
//
|
||||||
|
// 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a
|
||||||
|
// random 6-digit security number and stashes NodeA's public key (as a pending key if not already in
|
||||||
|
// the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B),
|
||||||
|
// then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's
|
||||||
|
// key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1.
|
||||||
|
//
|
||||||
|
// 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key,
|
||||||
|
// and prompts the user to enter the security number. The security number is never sent over the mesh
|
||||||
|
// and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from
|
||||||
|
// the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends
|
||||||
|
// its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and
|
||||||
|
// shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1.
|
||||||
|
//
|
||||||
|
// 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it
|
||||||
|
// matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation.
|
||||||
|
//
|
||||||
|
// The final on-screen code comparison is the actual manual verification: the user confirms the codes
|
||||||
|
// match on both devices, proving the two nodes agree on the same public keys (no MitM substitution).
|
||||||
|
// PKI-encrypting the follow-on packet additionally proves each node holds the private key for the
|
||||||
|
// agreed public key.
|
||||||
|
|
||||||
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
||||||
{
|
{
|
||||||
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
||||||
@@ -29,6 +62,7 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
|||||||
bool sendInitialRequest(NodeNum remoteNode);
|
bool sendInitialRequest(NodeNum remoteNode);
|
||||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||||
|
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/* Called to handle a particular incoming message
|
/* Called to handle a particular incoming message
|
||||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
|||||||
char message[40] = {0};
|
char message[40] = {0};
|
||||||
|
|
||||||
void processSecurityNumber(uint32_t);
|
void processSecurityNumber(uint32_t);
|
||||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||||
void resetToIdle(); // Zero out module state
|
void resetToIdle(); // Zero out module state
|
||||||
};
|
};
|
||||||
|
|
||||||
extern KeyVerificationModule *keyVerificationModule;
|
extern KeyVerificationModule *keyVerificationModule;
|
||||||
@@ -1 +1 @@
|
|||||||
31
|
32
|
||||||
@@ -7,9 +7,11 @@
|
|||||||
class AdminModuleTestShim : public AdminModule
|
class AdminModuleTestShim : public AdminModule
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
using AdminModule::checkPassKey; // session-key gate seam (see test_admin_session_repro)
|
||||||
using AdminModule::handleReceivedProtobuf;
|
using AdminModule::handleReceivedProtobuf;
|
||||||
using AdminModule::handleSetConfig;
|
using AdminModule::handleSetConfig;
|
||||||
using AdminModule::handleSetModuleConfig;
|
using AdminModule::handleSetModuleConfig;
|
||||||
|
using AdminModule::setPassKey;
|
||||||
|
|
||||||
// With an "open edit transaction" saveChanges() is a pure no-op: no reloadConfig/saveToDisk/reboot.
|
// With an "open edit transaction" saveChanges() is a pure no-op: no reloadConfig/saveToDisk/reboot.
|
||||||
void deferSaves() { hasOpenEditTransaction = true; }
|
void deferSaves() { hasOpenEditTransaction = true; }
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Deterministic reproduction of the admin session-key behavior discussed on PR #10669
|
||||||
|
// (ndoo's "Admin message without session_key!" report).
|
||||||
|
//
|
||||||
|
// Drives the REAL incoming-admin path (AdminModule::handleReceivedProtobuf) with a remote
|
||||||
|
// (from != 0) PKC-authorized set_owner, exercising the exact checkPassKey/setPassKey gate.
|
||||||
|
// A local (from == 0) admin bypasses that gate, so the bug only reproduces from != 0.
|
||||||
|
|
||||||
|
#include "MeshTypes.h" // include BEFORE TestUtil.h
|
||||||
|
#include "TestUtil.h"
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
|
||||||
|
#include "mesh/Channels.h"
|
||||||
|
#include "mesh/NodeDB.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
|
#include "support/AdminModuleTestShim.h"
|
||||||
|
#include "support/MockMeshService.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
static constexpr NodeNum LOCAL_NODE = 0x0A0A0A0A;
|
||||||
|
static constexpr NodeNum ADMIN_NODE = 0x0B0B0B0B; // authorized admin, sends remote admin to us
|
||||||
|
static const uint8_t ADMIN_KEY[32] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb,
|
||||||
|
0xcc, 0xdd, 0xee, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
|
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x20};
|
||||||
|
|
||||||
|
static MockMeshService *mockService = nullptr;
|
||||||
|
static AdminModuleTestShim *admin = nullptr;
|
||||||
|
|
||||||
|
// A remote, PKC-authorized set_owner. `session` (if non-empty) is the session_passkey the client presents.
|
||||||
|
static meshtastic_MeshPacket makeRemoteSetOwner(const char *newLongName, const uint8_t *session, size_t sessionLen,
|
||||||
|
meshtastic_AdminMessage &out)
|
||||||
|
{
|
||||||
|
out = meshtastic_AdminMessage_init_zero;
|
||||||
|
out.which_payload_variant = meshtastic_AdminMessage_set_owner_tag;
|
||||||
|
strncpy(out.set_owner.long_name, newLongName, sizeof(out.set_owner.long_name) - 1);
|
||||||
|
if (session) {
|
||||||
|
out.session_passkey.size = sessionLen;
|
||||||
|
memcpy(out.session_passkey.bytes, session, sessionLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
|
||||||
|
mp.from = ADMIN_NODE; // REMOTE: this is what makes the session gate apply
|
||||||
|
mp.channel = 0;
|
||||||
|
mp.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||||
|
mp.pki_encrypted = true; // arrived over PKC
|
||||||
|
mp.public_key.size = 32;
|
||||||
|
memcpy(mp.public_key.bytes, ADMIN_KEY, 32); // matches config.security.admin_key[0] -> authorized
|
||||||
|
return mp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
mockService = new MockMeshService();
|
||||||
|
service = mockService;
|
||||||
|
admin = new AdminModuleTestShim();
|
||||||
|
admin->deferSaves(); // no disk/reboot side effects when a setter is accepted
|
||||||
|
|
||||||
|
if (!nodeDB)
|
||||||
|
nodeDB = new NodeDB();
|
||||||
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
||||||
|
|
||||||
|
config = meshtastic_LocalConfig_init_zero;
|
||||||
|
// Authorize ADMIN_NODE's key as an admin key so the PKC path accepts it and we reach the session gate.
|
||||||
|
config.security.admin_key[0].size = 32;
|
||||||
|
memcpy(config.security.admin_key[0].bytes, ADMIN_KEY, 32);
|
||||||
|
|
||||||
|
owner = meshtastic_User_init_zero;
|
||||||
|
strncpy(owner.long_name, "Original", sizeof(owner.long_name) - 1);
|
||||||
|
|
||||||
|
channels.initDefaults();
|
||||||
|
channels.onConfigChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void)
|
||||||
|
{
|
||||||
|
service = nullptr;
|
||||||
|
delete mockService;
|
||||||
|
mockService = nullptr;
|
||||||
|
delete admin;
|
||||||
|
admin = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ndoo's report: a setter from a remote node with NO valid session is rejected, and the node's
|
||||||
|
// expected session key is all-zero because it has minted none since boot.
|
||||||
|
void test_remote_setter_without_session_is_rejected(void)
|
||||||
|
{
|
||||||
|
meshtastic_AdminMessage m;
|
||||||
|
meshtastic_MeshPacket mp = makeRemoteSetOwner("Hijacked", nullptr, 0, m);
|
||||||
|
|
||||||
|
admin->handleReceivedProtobuf(mp, &m);
|
||||||
|
admin->drainReply();
|
||||||
|
|
||||||
|
// Rejected at the session gate -> owner unchanged (this is ndoo's "Admin message without session_key!").
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Original", owner.long_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The node's session key is minted only by setPassKey (which runs when it answers an admin GET),
|
||||||
|
// so before any GET the expected key is all-zero and any presented key mismatches.
|
||||||
|
void test_expected_session_key_is_zero_before_any_get(void)
|
||||||
|
{
|
||||||
|
uint8_t zero[8] = {0};
|
||||||
|
meshtastic_AdminMessage probe = meshtastic_AdminMessage_init_zero;
|
||||||
|
probe.session_passkey.size = 8;
|
||||||
|
memcpy(probe.session_passkey.bytes, zero, 8); // even all-zeros must not authorize a state change
|
||||||
|
// A fresh module has minted no session; a stale/guessed key does not match.
|
||||||
|
// (checkPassKey also requires size==8 AND session_time freshness.)
|
||||||
|
uint8_t stale[8] = {0x29, 0x04, 0xb4, 0x78, 0xd8, 0x68, 0xa7, 0xff}; // ndoo's presented key
|
||||||
|
meshtastic_AdminMessage staleMsg = meshtastic_AdminMessage_init_zero;
|
||||||
|
staleMsg.session_passkey.size = 8;
|
||||||
|
memcpy(staleMsg.session_passkey.bytes, stale, 8);
|
||||||
|
TEST_ASSERT_FALSE(admin->checkPassKey(&staleMsg)); // Expected: 00..00 vs Incoming: 29 04 b4 78.. -> reject
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fix path: once the node answers a GET (setPassKey mints/returns the key), the session gate
|
||||||
|
// accepts a setter carrying that key. Asserting the gate (checkPassKey) directly is the mechanism;
|
||||||
|
// driving the full handleSetOwner would need the NodeInfoModule scaffolding, out of scope here.
|
||||||
|
void test_session_gate_accepts_key_from_a_get_response(void)
|
||||||
|
{
|
||||||
|
// Simulate the node answering an admin GET: setPassKey mints the session and writes it into the response.
|
||||||
|
meshtastic_AdminMessage getResponse = meshtastic_AdminMessage_init_zero;
|
||||||
|
admin->setPassKey(&getResponse);
|
||||||
|
TEST_ASSERT_EQUAL(8, getResponse.session_passkey.size); // node handed the client a session key
|
||||||
|
|
||||||
|
// A setter carrying that exact key passes the gate (would be accepted).
|
||||||
|
meshtastic_AdminMessage good = meshtastic_AdminMessage_init_zero;
|
||||||
|
good.session_passkey = getResponse.session_passkey;
|
||||||
|
TEST_ASSERT_TRUE(admin->checkPassKey(&good));
|
||||||
|
|
||||||
|
// A setter carrying a stale/guessed key still fails (no session replay).
|
||||||
|
meshtastic_AdminMessage bad = meshtastic_AdminMessage_init_zero;
|
||||||
|
bad.session_passkey.size = 8;
|
||||||
|
uint8_t stale[8] = {0x29, 0x04, 0xb4, 0x78, 0xd8, 0x68, 0xa7, 0xff};
|
||||||
|
memcpy(bad.session_passkey.bytes, stale, 8);
|
||||||
|
TEST_ASSERT_FALSE(admin->checkPassKey(&bad));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
delay(10);
|
||||||
|
initializeTestEnvironment();
|
||||||
|
UNITY_BEGIN();
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
RUN_TEST(test_remote_setter_without_session_is_rejected);
|
||||||
|
RUN_TEST(test_expected_session_key_is_zero_before_any_get);
|
||||||
|
RUN_TEST(test_session_gate_accepts_key_from_a_get_response);
|
||||||
|
#endif
|
||||||
|
exit(UNITY_END());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// Tests for the admin-key fallback in Router::perhapsDecode: a PKI unicast from an unknown sender is
|
||||||
|
// tried against each configured admin_key; on success the packet decodes and the key is persisted to
|
||||||
|
// NodeDB. Drives the real crypto + NodeDB path with packets encrypted exactly as an admin radio would.
|
||||||
|
|
||||||
|
#include "MeshTypes.h" // include BEFORE TestUtil.h
|
||||||
|
#include "TestUtil.h"
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
// The whole feature is compiled out when PKI is excluded.
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
|
||||||
|
#include "mesh/Channels.h"
|
||||||
|
#include "mesh/CryptoEngine.h"
|
||||||
|
#include "mesh/NodeDB.h"
|
||||||
|
#include "mesh/RadioInterface.h" // MESHTASTIC_PKC_OVERHEAD
|
||||||
|
#include "mesh/Router.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <pb_encode.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
static constexpr NodeNum LOCAL_NODE = 0x0A0A0A0A; // us (the receiver)
|
||||||
|
static constexpr NodeNum ADMIN_NODE = 0x0B0B0B0B; // an authorized admin, absent from our NodeDB
|
||||||
|
static constexpr uint32_t PKT_ID = 0x12345678;
|
||||||
|
|
||||||
|
// MockNodeDB - inject nodes with controlled public keys (meshNodes/numMeshNodes are public on NodeDB).
|
||||||
|
// Mirrors test/test_packet_signing.
|
||||||
|
class MockNodeDB : public NodeDB
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void clearTestNodes()
|
||||||
|
{
|
||||||
|
testNodes.clear();
|
||||||
|
meshNodes = &testNodes;
|
||||||
|
numMeshNodes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addNode(NodeNum num)
|
||||||
|
{
|
||||||
|
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
||||||
|
node.num = num;
|
||||||
|
testNodes.push_back(node);
|
||||||
|
meshNodes = &testNodes;
|
||||||
|
numMeshNodes = testNodes.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPublicKey(NodeNum num, const uint8_t *pubKey)
|
||||||
|
{
|
||||||
|
meshtastic_NodeInfoLite *n = getMeshNode(num);
|
||||||
|
TEST_ASSERT_NOT_NULL(n);
|
||||||
|
n->public_key.size = 32;
|
||||||
|
memcpy(n->public_key.bytes, pubKey, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<meshtastic_NodeInfoLite> testNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
static MockNodeDB *mockNodeDB = nullptr;
|
||||||
|
|
||||||
|
// Keypairs, regenerated fresh each test in setUp(). "our" == the receiver, "admin" == the sender.
|
||||||
|
static uint8_t ourPub[32], ourPriv[32];
|
||||||
|
static uint8_t adminPub[32], adminPriv[32];
|
||||||
|
|
||||||
|
// Store a 32-byte key into config.security.admin_key[slot].
|
||||||
|
static void setAdminKey(int slot, const uint8_t *key32)
|
||||||
|
{
|
||||||
|
config.security.admin_key[slot].size = 32;
|
||||||
|
memcpy(config.security.admin_key[slot].bytes, key32, 32);
|
||||||
|
if (slot + 1 > (int)config.security.admin_key_count)
|
||||||
|
config.security.admin_key_count = slot + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a PKI-encrypted unicast from `from` to us, encrypted with `senderPriv` against our public key,
|
||||||
|
// leaving the engine holding our private key afterwards (as during receive) so perhapsDecode can decrypt.
|
||||||
|
static meshtastic_MeshPacket makePkiPacket(NodeNum from, meshtastic_PortNum port, size_t payloadLen, const uint8_t *senderPriv)
|
||||||
|
{
|
||||||
|
meshtastic_Data data = meshtastic_Data_init_zero;
|
||||||
|
data.portnum = port;
|
||||||
|
data.payload.size = payloadLen;
|
||||||
|
for (size_t i = 0; i < payloadLen; i++)
|
||||||
|
data.payload.bytes[i] = (uint8_t)(i & 0xff);
|
||||||
|
|
||||||
|
uint8_t plain[meshtastic_Constants_DATA_PAYLOAD_LEN];
|
||||||
|
size_t plainLen = pb_encode_to_bytes(plain, sizeof(plain), &meshtastic_Data_msg, &data);
|
||||||
|
TEST_ASSERT_TRUE_MESSAGE(plainLen > 0, "pb_encode_to_bytes failed in test setup");
|
||||||
|
|
||||||
|
meshtastic_NodeInfoLite_public_key_t ourPubStruct;
|
||||||
|
ourPubStruct.size = 32;
|
||||||
|
memcpy(ourPubStruct.bytes, ourPub, 32);
|
||||||
|
|
||||||
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
||||||
|
p.from = from;
|
||||||
|
p.to = LOCAL_NODE;
|
||||||
|
p.id = PKT_ID;
|
||||||
|
p.channel = 0; // PKI packets carry channel hash 0
|
||||||
|
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
||||||
|
|
||||||
|
// Encrypt AS the sender: shared secret = DH(senderPriv, ourPub).
|
||||||
|
crypto->setDHPrivateKey(const_cast<uint8_t *>(senderPriv));
|
||||||
|
bool ok = crypto->encryptCurve25519(p.to, p.from, ourPubStruct, p.id, plainLen, plain, p.encrypted.bytes);
|
||||||
|
TEST_ASSERT_TRUE_MESSAGE(ok, "encryptCurve25519 failed in test setup");
|
||||||
|
p.encrypted.size = plainLen + MESHTASTIC_PKC_OVERHEAD;
|
||||||
|
|
||||||
|
// Restore the engine to our private key, as it is when receiving.
|
||||||
|
crypto->setDHPrivateKey(ourPriv);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the packet decoded via PKI and that we learned `expectedKey` for its sender.
|
||||||
|
static void assertDecodedAndLearned(meshtastic_MeshPacket *p, const uint8_t *expectedKey)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p->which_payload_variant);
|
||||||
|
TEST_ASSERT_TRUE(p->pki_encrypted);
|
||||||
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_PRIVATE_APP, p->decoded.portnum);
|
||||||
|
TEST_ASSERT_EQUAL(32, p->public_key.size);
|
||||||
|
TEST_ASSERT_EQUAL_MEMORY(expectedKey, p->public_key.bytes, 32);
|
||||||
|
|
||||||
|
meshtastic_NodeInfoLite *learned = mockNodeDB->getMeshNode(p->from);
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(learned, "sender should have been created in NodeDB");
|
||||||
|
TEST_ASSERT_EQUAL_MESSAGE(32, learned->public_key.size, "sender key should have been persisted");
|
||||||
|
TEST_ASSERT_EQUAL_MEMORY_MESSAGE(expectedKey, learned->public_key.bytes, 32, "persisted key mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
// Construct the mock FIRST: the NodeDB ctor can reload persisted host state and repopulate globals.
|
||||||
|
mockNodeDB = new MockNodeDB();
|
||||||
|
mockNodeDB->clearTestNodes();
|
||||||
|
nodeDB = mockNodeDB;
|
||||||
|
|
||||||
|
config = meshtastic_LocalConfig_init_zero;
|
||||||
|
owner = meshtastic_User_init_zero;
|
||||||
|
myNodeInfo.my_node_num = LOCAL_NODE; // drives isToUs()/getFrom()
|
||||||
|
|
||||||
|
channels.initDefaults();
|
||||||
|
channels.onConfigChanged();
|
||||||
|
|
||||||
|
// Fresh keypairs for us and the admin (independent, valid Curve25519 pairs).
|
||||||
|
crypto->generateKeyPair(ourPub, ourPriv);
|
||||||
|
crypto->generateKeyPair(adminPub, adminPriv);
|
||||||
|
|
||||||
|
// perhapsDecode's PKI gate requires that we have our own key (getMeshNode(p->to)->public_key).
|
||||||
|
mockNodeDB->addNode(LOCAL_NODE);
|
||||||
|
mockNodeDB->setPublicKey(LOCAL_NODE, ourPub);
|
||||||
|
|
||||||
|
// During receive the engine holds our private key.
|
||||||
|
crypto->setDHPrivateKey(ourPriv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void)
|
||||||
|
{
|
||||||
|
delete mockNodeDB;
|
||||||
|
mockNodeDB = nullptr;
|
||||||
|
nodeDB = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin key in slot 0: a DM from an unknown sender decrypts via the fallback, and the key is persisted.
|
||||||
|
void test_admin_key_slot0_decrypts_and_persists(void)
|
||||||
|
{
|
||||||
|
setAdminKey(0, adminPub);
|
||||||
|
TEST_ASSERT_NULL_MESSAGE(mockNodeDB->getMeshNode(ADMIN_NODE), "precondition: sender is unknown to us");
|
||||||
|
|
||||||
|
meshtastic_MeshPacket p = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
TEST_ASSERT_EQUAL(DECODE_SUCCESS, perhapsDecode(&p));
|
||||||
|
|
||||||
|
assertDecodedAndLearned(&p, adminPub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loop scans every admin slot, not just [0]: a key provisioned only in slot 2 still works.
|
||||||
|
void test_admin_key_slot2_only_decrypts(void)
|
||||||
|
{
|
||||||
|
setAdminKey(2, adminPub); // slots 0 and 1 left empty
|
||||||
|
|
||||||
|
meshtastic_MeshPacket p = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
TEST_ASSERT_EQUAL(DECODE_SUCCESS, perhapsDecode(&p));
|
||||||
|
|
||||||
|
assertDecodedAndLearned(&p, adminPub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No admin key configured + unknown sender: nothing decrypts, and we must NOT invent a key for anyone.
|
||||||
|
void test_no_admin_key_unknown_sender_not_decoded(void)
|
||||||
|
{
|
||||||
|
// config (incl. admin_key) is zeroed by setUp(); ADMIN_NODE is absent from NodeDB.
|
||||||
|
meshtastic_MeshPacket p = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_EQUAL(DECODE_SUCCESS, perhapsDecode(&p));
|
||||||
|
TEST_ASSERT_NOT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant);
|
||||||
|
TEST_ASSERT_NULL_MESSAGE(mockNodeDB->getMeshNode(ADMIN_NODE), "must not learn a key when nothing decrypted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A configured admin key that is NOT the sender's must fail authentication (no bogus key learned).
|
||||||
|
void test_wrong_admin_key_does_not_decode(void)
|
||||||
|
{
|
||||||
|
uint8_t otherPub[32], otherPriv[32];
|
||||||
|
crypto->generateKeyPair(otherPub, otherPriv); // unrelated key
|
||||||
|
crypto->setDHPrivateKey(ourPriv); // restore receive key (generateKeyPair changed it)
|
||||||
|
setAdminKey(0, otherPub); // admin slot holds a key that did NOT encrypt the packet
|
||||||
|
|
||||||
|
meshtastic_MeshPacket p = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_EQUAL(DECODE_SUCCESS, perhapsDecode(&p));
|
||||||
|
TEST_ASSERT_NOT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant);
|
||||||
|
TEST_ASSERT_NULL(mockNodeDB->getMeshNode(ADMIN_NODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
delay(10);
|
||||||
|
delay(2000);
|
||||||
|
|
||||||
|
initializeTestEnvironment();
|
||||||
|
UNITY_BEGIN();
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
RUN_TEST(test_admin_key_slot0_decrypts_and_persists);
|
||||||
|
RUN_TEST(test_admin_key_slot2_only_decrypts);
|
||||||
|
RUN_TEST(test_no_admin_key_unknown_sender_not_decoded);
|
||||||
|
RUN_TEST(test_wrong_admin_key_does_not_decode);
|
||||||
|
#endif
|
||||||
|
exit(UNITY_END());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
Reference in New Issue
Block a user