fix: stop unexpected NodeNum regeneration from PKI key loss (#10808)
* fix: stop unexpected NodeNum regeneration from PKI key loss A node's NodeNum is derived from its PKI public key (my_node_num == crc32(public_key)), so it changes only when the keypair is regenerated -- which happens only when a keygen path runs while config.security.private_key.size != 32. Two paths could trigger that without the user intending a new identity: 1. Boot: loadFromDisk() collapsed every non-success loadProto() result for the config file into installDefaultConfig() (preserveKey=false), which memset()s config and wipes the private key. loadProto() distinguishes DECODE_FAILED (file present but undecodable/undecryptable) from OTHER_FAILURE (absent), so a transient/corrupt read silently minted a new identity. A new configDecodeFailed flag now gates the boot keygen and the boot config-save directly (not via region, so it survives the userprefs/region overrides that run later in loadFromDisk): identity is frozen, the unreadable file is left intact for a later clean boot to recover, and the node boots radio-silent (region UNSET, TX off). A genuinely-absent config still gets defaults + keygen. 2. AdminModule security SET: config.security = c.payload_variant.security wholesale-clobbered the keypair, then regenerated whenever the incoming private_key.size != 32. A client editing an unrelated security field without round-tripping the private key would re-mint identity. The existing keypair is now preserved when the incoming config omits it; first provisioning and key import are unchanged. Intentional reset goes through factory_reset. Native PlatformIO unit suite (Docker): 499/499 test cases pass. * test: cover security keypair preservation; clarify load-failure comment Addresses PR review feedback: - Add native unit tests (test_admin_radio) asserting handleSetConfig preserves the existing identity keypair when a security SET omits the private key, and applies a full supplied keypair (key import). Guards against reintroduced NodeNum/keypair regeneration. AdminModuleTestShim (now a friend) defers saves so the lightweight harness doesn't saveToDisk an uninitialized node database. - Clarify the non-DECODE_FAILED config-load comment: OTHER_FAILURE / NO_FILESYSTEM cover an absent or unopenable file, not just first boot.
This commit is contained in:
@@ -1037,16 +1037,24 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
config.has_bluetooth = true;
|
||||
config.bluetooth = c.payload_variant.bluetooth;
|
||||
break;
|
||||
case meshtastic_Config_security_tag:
|
||||
case meshtastic_Config_security_tag: {
|
||||
LOG_INFO("Set config: Security");
|
||||
config.security = c.payload_variant.security;
|
||||
meshtastic_Config_SecurityConfig incoming = c.payload_variant.security;
|
||||
// Preserve our keypair when a SET omits the private key but we already hold one: regenerating would
|
||||
// change our NodeNum (== crc32(public_key)) and orphan us on the mesh. A SET without the key is a
|
||||
// partial/legacy client, not an identity reset (that goes through factory_reset). Done outside the
|
||||
// PKI guard so non-PKI builds keep their key bytes too.
|
||||
if (incoming.private_key.size != 32 && config.security.private_key.size == 32) {
|
||||
LOG_WARN("Security set omitted private key; preserving existing identity keypair");
|
||||
incoming.private_key = config.security.private_key;
|
||||
incoming.public_key = config.security.public_key;
|
||||
}
|
||||
config.security = incoming;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Only regenerate keys if the private key is not 32 bytes
|
||||
// First provisioning (no key) generates one; a private key supplied without its public key derives it.
|
||||
if (config.security.private_key.size != 32) {
|
||||
nodeDB->generateCryptoKeyPair();
|
||||
}
|
||||
// If user provided a private key of correct size but no public key, generate the public key from private key
|
||||
else if (config.security.private_key.size == 32 && config.security.public_key.size == 0) {
|
||||
} else if (config.security.public_key.size == 0) {
|
||||
nodeDB->generateCryptoKeyPair(config.security.private_key.bytes);
|
||||
}
|
||||
#endif
|
||||
@@ -1063,6 +1071,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
||||
requiresReboot = true;
|
||||
|
||||
break;
|
||||
}
|
||||
case meshtastic_Config_device_ui_tag:
|
||||
// NOOP! This is handled by handleStoreDeviceUIConfig
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user