Bound remote-initiated key verification sessions (#11101)

* Bound remote-initiated key verification sessions

Opening a session raises a 30 second banner and a client notification and
occupies the only verification slot, all before the peer has authenticated
anything, and nothing limited how often that could happen.

Adds an absolute session cap that incoming packets cannot refresh, a
cooldown between remote-initiated sessions measured from when the previous
one ended, and refreshes the idle deadline only when the protocol actually
advances rather than on any arriving packet.

The busy path now sets ignoreRequest so it no longer answers with a NAK.

Also replaces the getTime() - 60 timeout comparison, which underflowed
before the clock passed 60.

* Use elapsed-time comparison for the session timeout and condense comments
This commit is contained in:
Thomas Göttgens
2026-07-20 20:29:56 -05:00
committed by GitHub
co-authored by GitHub
parent d9f8839241
commit 53e510199a
2 changed files with 38 additions and 5 deletions
+32 -5
View File
@@ -6,12 +6,19 @@
#include "gps/RTC.h"
#include "graphics/draw/MenuHandler.h"
#include "main.h"
#include "mesh/Throttle.h"
#include "meshUtils.h"
#include "modules/AdminModule.h"
#include "modules/NodeInfoModule.h"
#include <RNG.h>
#include <SHA256.h>
#define KEY_VERIFICATION_TIMEOUT_SECS 60
// Hard cap on one session, independent of the refreshable idle timeout above.
#define KEY_VERIFICATION_MAX_SESSION_MS (3 * 60 * 1000UL)
// Minimum spacing between remote-initiated sessions.
#define KEY_VERIFICATION_REMOTE_COOLDOWN_MS (60 * 1000UL)
KeyVerificationModule *keyVerificationModule;
namespace
@@ -64,7 +71,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
{
updateState();
// No refresh here: this runs before the sender is checked, so any node could hold the session open.
updateState(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()
@@ -98,6 +106,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
service->sendClientNotification(cn);
}
LOG_INFO("Received hash2");
currentNonceTimestamp = getTime(); // the protocol advanced, so extend the deadline
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
return true;
@@ -134,6 +143,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
service->sendClientNotification(cn);
}
currentNonceTimestamp = getTime(); // the protocol advanced, so extend the deadline
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER;
return true;
}
@@ -160,6 +170,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
CryptRNG.rand((uint8_t *)&currentNonce, sizeof(currentNonce));
}
currentNonceTimestamp = getTime();
sessionStartedMs = millis();
currentRemoteNode = remoteNode;
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
KeyVerification.nonce = currentNonce;
@@ -190,10 +201,19 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
SHA256 hash;
NodeNum ourNodeNum = nodeDB->getNodeNum();
updateState();
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
if (currentState != KEY_VERIFICATION_IDLE) {
LOG_WARN("Key Verification requested, but already in a request");
ignoreRequest = true; // do not let the busy path emit a NAK back to the requester
return nullptr;
}
// Opening a session raises a banner and locks the only slot, before the peer has authenticated.
if (lastRemoteSessionMs != 0 && Throttle::isWithinTimespanMs(lastRemoteSessionMs, KEY_VERIFICATION_REMOTE_COOLDOWN_MS)) {
LOG_WARN("Key Verification requested, but within cooldown");
ignoreRequest = true;
return nullptr;
}
sessionStartedMs = millis();
sessionFromRemote = true;
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
auto req = *currentRequest;
@@ -357,11 +377,14 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
void KeyVerificationModule::updateState(bool resetTimer)
{
if (currentState != KEY_VERIFICATION_IDLE) {
// check for the 60 second timeout
if (currentNonceTimestamp < getTime() - 60) {
uint32_t now = getTime();
// Absolute cap first: it is millis() based, so it bounds the session even when the RTC is unset.
if (!Throttle::isWithinTimespanMs(sessionStartedMs, KEY_VERIFICATION_MAX_SESSION_MS)) {
resetToIdle();
} else if (now - currentNonceTimestamp >= KEY_VERIFICATION_TIMEOUT_SECS) {
resetToIdle();
} else if (resetTimer) {
currentNonceTimestamp = getTime();
currentNonceTimestamp = now;
}
}
}
@@ -370,8 +393,12 @@ void KeyVerificationModule::resetToIdle()
{
memset(hash1, 0, 32);
memset(hash2, 0, 32);
if (sessionFromRemote)
lastRemoteSessionMs = millis(); // start the cooldown when the session ends, not when it opened
sessionFromRemote = false;
currentNonce = 0;
currentNonceTimestamp = 0;
sessionStartedMs = 0;
currentSecurityNumber = 0;
currentRemoteNode = 0;
currentState = KEY_VERIFICATION_IDLE;
+6
View File
@@ -84,6 +84,12 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
private:
uint64_t currentNonce = 0;
uint32_t currentNonceTimestamp = 0;
// millis() the session opened, never refreshed, so a peer cannot hold the slot open indefinitely.
uint32_t sessionStartedMs = 0;
// millis() a remote-initiated session last ended. Spacing sessions bounds the slot DoS and the
// banner spam; stamped at the end rather than the start so the cooldown is a real gap.
uint32_t lastRemoteSessionMs = 0;
bool sessionFromRemote = false;
NodeNum currentRemoteNode = 0;
uint32_t currentSecurityNumber = 0;
KeyVerificationState currentState = KEY_VERIFICATION_IDLE;