Budget the admin-key PKI decrypt fallback (#11100)
This commit is contained in:
co-authored by
GitHub
parent
6bff231d53
commit
7bdc2569e8
+61
-9
@@ -553,6 +553,54 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
|
// The fallback costs three X25519 ops before the AEAD tag is checked. Budget is global because p->from is
|
||||||
|
// attacker-controlled; successful runs refund, and their key is then persisted for the fast path.
|
||||||
|
#define ADMIN_KEY_FALLBACK_BURST 8
|
||||||
|
#define ADMIN_KEY_FALLBACK_REFILL_MS 250
|
||||||
|
|
||||||
|
static uint32_t adminKeyFallbackTokens = ADMIN_KEY_FALLBACK_BURST;
|
||||||
|
static uint32_t adminKeyFallbackRefillMs = 0;
|
||||||
|
|
||||||
|
static bool adminKeyFallbackAllowed()
|
||||||
|
{
|
||||||
|
bool haveAdminKey = false;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (config.security.admin_key[i].size == 32) {
|
||||||
|
haveAdminKey = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!haveAdminKey)
|
||||||
|
return false; // nothing to try, so do not spend a token
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (adminKeyFallbackRefillMs == 0)
|
||||||
|
adminKeyFallbackRefillMs = now;
|
||||||
|
uint32_t elapsed = now - adminKeyFallbackRefillMs;
|
||||||
|
if (elapsed >= ADMIN_KEY_FALLBACK_REFILL_MS) {
|
||||||
|
uint32_t refill = elapsed / ADMIN_KEY_FALLBACK_REFILL_MS;
|
||||||
|
adminKeyFallbackRefillMs += refill * ADMIN_KEY_FALLBACK_REFILL_MS;
|
||||||
|
if (refill >= ADMIN_KEY_FALLBACK_BURST - adminKeyFallbackTokens)
|
||||||
|
adminKeyFallbackTokens = ADMIN_KEY_FALLBACK_BURST;
|
||||||
|
else
|
||||||
|
adminKeyFallbackTokens += refill;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminKeyFallbackTokens == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
adminKeyFallbackTokens--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void adminKeyFallbackRefund()
|
||||||
|
{
|
||||||
|
if (adminKeyFallbackTokens < ADMIN_KEY_FALLBACK_BURST)
|
||||||
|
adminKeyFallbackTokens++;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
concurrency::LockGuard g(cryptLock);
|
concurrency::LockGuard g(cryptLock);
|
||||||
@@ -597,17 +645,21 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
decrypted = true;
|
decrypted = true;
|
||||||
viaPendingKey = havePendingKey;
|
viaPendingKey = havePendingKey;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 3 && !decrypted; i++) {
|
if (!decrypted && adminKeyFallbackAllowed()) {
|
||||||
if (config.security.admin_key[i].size != 32)
|
for (int i = 0; i < 3 && !decrypted; i++) {
|
||||||
continue;
|
if (config.security.admin_key[i].size != 32)
|
||||||
remotePublic.size = 32;
|
continue;
|
||||||
memcpy(remotePublic.bytes, config.security.admin_key[i].bytes, 32);
|
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)) {
|
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||||
decrypted = true;
|
decrypted = true;
|
||||||
viaAdminKey = true;
|
viaAdminKey = true;
|
||||||
break; // stop after first successful decryption
|
break; // stop after first successful decryption
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (decrypted)
|
||||||
|
adminKeyFallbackRefund();
|
||||||
}
|
}
|
||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
LOG_INFO("PKI Decryption worked!");
|
LOG_INFO("PKI Decryption worked!");
|
||||||
|
|||||||
@@ -202,6 +202,35 @@ void test_wrong_admin_key_does_not_decode(void)
|
|||||||
TEST_ASSERT_NULL(mockNodeDB->getMeshNode(ADMIN_NODE));
|
TEST_ASSERT_NULL(mockNodeDB->getMeshNode(ADMIN_NODE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The fallback is budget-limited against flooding; see Router.cpp for why the budget is global.
|
||||||
|
void test_admin_key_fallback_is_rate_limited(void)
|
||||||
|
{
|
||||||
|
// Start from a full bucket regardless of what earlier tests consumed (8 tokens, one per 250ms).
|
||||||
|
delay(2500);
|
||||||
|
|
||||||
|
uint8_t otherPub[32], otherPriv[32];
|
||||||
|
crypto->generateKeyPair(otherPub, otherPriv);
|
||||||
|
crypto->setDHPrivateKey(ourPriv);
|
||||||
|
setAdminKey(0, otherPub); // wrong key, so every attempt below fails and keeps its token spent
|
||||||
|
|
||||||
|
// Drain the burst with undecryptable packets, as a flooding attacker would.
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
meshtastic_MeshPacket junk = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
TEST_ASSERT_NOT_EQUAL(DECODE_SUCCESS, perhapsDecode(&junk));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Budget exhausted: the fallback is skipped, so even a correct admin key does not decrypt.
|
||||||
|
setAdminKey(0, adminPub);
|
||||||
|
meshtastic_MeshPacket blocked = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
TEST_ASSERT_NOT_EQUAL_MESSAGE(DECODE_SUCCESS, perhapsDecode(&blocked), "fallback should be budget-limited");
|
||||||
|
|
||||||
|
// The budget refills, so the throttle is not a permanent lockout.
|
||||||
|
delay(600);
|
||||||
|
meshtastic_MeshPacket allowed = makePkiPacket(ADMIN_NODE, meshtastic_PortNum_PRIVATE_APP, 16, adminPriv);
|
||||||
|
TEST_ASSERT_EQUAL_MESSAGE(DECODE_SUCCESS, perhapsDecode(&allowed), "budget should refill over time");
|
||||||
|
assertDecodedAndLearned(&allowed, adminPub);
|
||||||
|
}
|
||||||
|
|
||||||
// A pending key is an unverified identity claim from whoever opened a key-verification handshake, so it
|
// A pending key is an unverified identity claim from whoever opened a key-verification handshake, so it
|
||||||
// must decrypt only the exchange itself. Otherwise they could send DMs that look PKI-authenticated as a
|
// must decrypt only the exchange itself. Otherwise they could send DMs that look PKI-authenticated as a
|
||||||
// node they never proved they are.
|
// node they never proved they are.
|
||||||
@@ -243,6 +272,7 @@ void setup()
|
|||||||
RUN_TEST(test_admin_key_slot2_only_decrypts);
|
RUN_TEST(test_admin_key_slot2_only_decrypts);
|
||||||
RUN_TEST(test_no_admin_key_unknown_sender_not_decoded);
|
RUN_TEST(test_no_admin_key_unknown_sender_not_decoded);
|
||||||
RUN_TEST(test_wrong_admin_key_does_not_decode);
|
RUN_TEST(test_wrong_admin_key_does_not_decode);
|
||||||
|
RUN_TEST(test_admin_key_fallback_is_rate_limited);
|
||||||
RUN_TEST(test_pending_key_decrypts_only_key_verification);
|
RUN_TEST(test_pending_key_decrypts_only_key_verification);
|
||||||
#endif
|
#endif
|
||||||
exit(UNITY_END());
|
exit(UNITY_END());
|
||||||
|
|||||||
Reference in New Issue
Block a user