mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-13 23:21:19 +10:00
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
@@ -1,11 +1,15 @@
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "CryptoEngine.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include <RNG.h>
|
||||
#include <SHA256.h>
|
||||
|
||||
KeyVerificationModule *keyVerificationModule;
|
||||
@@ -34,7 +38,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
{
|
||||
updateState();
|
||||
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 &&
|
||||
currentState == KEY_VERIFICATION_IDLE) {
|
||||
@@ -48,9 +52,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
commitVerifiedRemoteNode();
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -63,9 +65,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
}
|
||||
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||
// 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()
|
||||
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 &&
|
||||
r->hash1.size == 0) {
|
||||
r->hash1.size == 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);
|
||||
});)
|
||||
|
||||
@@ -95,7 +101,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
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) {
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
@@ -108,10 +115,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
LOG_DEBUG("User selected %d for key verification", selected);
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -139,22 +145,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
{
|
||||
LOG_DEBUG("keyVerification start");
|
||||
// generate nonce
|
||||
updateState();
|
||||
updateState(false);
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
updateState(true);
|
||||
currentNonce = random();
|
||||
currentNonceTimestamp = getTime();
|
||||
currentRemoteNode = remoteNode;
|
||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||
KeyVerification.nonce = currentNonce;
|
||||
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);
|
||||
p->to = remoteNode;
|
||||
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->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -171,9 +184,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||
LOG_WARN("Key Verification requested, but already in a request");
|
||||
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;
|
||||
|
||||
@@ -188,15 +198,43 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
response.nonce = scratch.nonce;
|
||||
currentRemoteNode = req.from;
|
||||
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.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
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.finalize(hash1, 32);
|
||||
|
||||
@@ -205,15 +243,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(hash1, 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;
|
||||
memcpy(response.hash2.bytes, hash2, 32);
|
||||
|
||||
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);
|
||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
screen->showSimpleBanner(message, 30000); LOG_DEBUG("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
if (cn) {
|
||||
cn->level = meshtastic_LogRecord_Level_WARNING;
|
||||
@@ -227,7 +269,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||
service->sendClientNotification(cn);
|
||||
}
|
||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
LOG_DEBUG("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
return responsePacket;
|
||||
}
|
||||
|
||||
@@ -236,14 +278,18 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
SHA256 hash;
|
||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||
uint8_t scratch_hash[32] = {0};
|
||||
LOG_WARN("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
return; // should we throw an error here?
|
||||
LOG_DEBUG("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||
// handshake (bootstrap case).
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
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
|
||||
hash.reset();
|
||||
hash.update(&incomingNumber, sizeof(incomingNumber));
|
||||
@@ -252,7 +298,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
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.reset();
|
||||
@@ -297,13 +343,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
return;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::updateState()
|
||||
void KeyVerificationModule::updateState(bool resetTimer)
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
} else if (resetTimer) {
|
||||
currentNonceTimestamp = getTime();
|
||||
}
|
||||
}
|
||||
@@ -318,6 +364,32 @@ void KeyVerificationModule::resetToIdle()
|
||||
currentSecurityNumber = 0;
|
||||
currentRemoteNode = 0;
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user