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
GitHub
Claude Opus 4.8
Ben Meadors
parent
fc91a69ca4
commit
952c825167
@@ -341,6 +341,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
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
|
||||
concurrency::Lock *cryptLock;
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@ class CryptoEngine
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
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 aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
@@ -94,6 +105,10 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_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)
|
||||
uint8_t xeddsa_public_key[32] = {0};
|
||||
uint8_t xeddsa_private_key[32] = {0};
|
||||
|
||||
+56
-19
@@ -524,36 +524,60 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
bool decrypted = false;
|
||||
ChannelIndex chIndex = 0;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Attempt PKI decryption first. The sender's key may come from the hot
|
||||
// store or the warm tier (nodes evicted from the hot store keep their key
|
||||
// there), so DMs from long-tail nodes still decrypt.
|
||||
meshtastic_NodeInfoLite_public_key_t fromKey = {0, {0}};
|
||||
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");
|
||||
// Resolve the sender's public key: prefer the one stored in NodeDB (hot store or warm tier), else
|
||||
// fall back to a not-yet-committed key held during an in-progress key-verification handshake.
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic) || crypto->getPendingPublicKey(p->from, remotePublic);
|
||||
|
||||
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!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
rawSize -= MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
size_t payloadSize = rawSize - MESHTASTIC_PKC_OVERHEAD;
|
||||
if (pb_decode_from_bytes(bytes, payloadSize, &meshtastic_Data_msg, &decodedtmp) &&
|
||||
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
|
||||
decrypted = true;
|
||||
rawSize = payloadSize; // commit the overhead subtraction only on full success
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
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->decoded = decodedtmp;
|
||||
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 {
|
||||
// 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!");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("PKC decrypt attempted but failed!");
|
||||
}
|
||||
}
|
||||
#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
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Destination key from the hot store or the warm tier (evicted
|
||||
// long-tail nodes keep their key there)
|
||||
// Resolve the destination's public key: prefer NodeDB (hot store or warm tier - evicted
|
||||
// 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}};
|
||||
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
|
||||
// is not in the local nodedb
|
||||
// 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) &&
|
||||
// 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_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!");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
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) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
|
||||
Reference in New Issue
Block a user