From a2df37356f6fcce0dc66e3674112fa8da368f61f Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 11:21:20 -0800 Subject: [PATCH] Support working with ed25519 seeds in addition to raw keypairs. Our ed25519 library uses a representation of its key pair that is largely incompatible with modern implementations, which mostly work with the original 32-byte seed; Peters' impentation represents the private key as the clamped sha512 of the seed. This change: - preserves the original seed when generating keys - adds CLI commands to obtain the seed via `get prv.seed`, under the same conditions as `get prv.key` is allowed - adds support for `set prv.key` to supply a seed, in which case the keypair will be re-generated from it. This is mostly to enable external key management using modern libraries, but could also be of use on devices where we don't have a trustworthy entropy source. I split Identity::writeTo(uint8_t*,size_t) into explicit forms for the thing being written; the original implementation wrote a different thing depending on the length, which would be ambiguous between pubkey and seed and cumbersome if it tried to return all three in one long buffer. Identity::readFrom() did not have that ambiguity problem because keys can't be set from pubkey alone, though it might be preferable to split readFrom() up as well and not use magic length values. --- src/Identity.cpp | 36 ++++++++++++++++++++++++++---------- src/Identity.h | 15 ++++++++++++++- src/helpers/CommonCLI.cpp | 18 +++++++++++++++++- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index 832989283..56cf7bfdb 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -43,7 +43,6 @@ LocalIdentity::LocalIdentity(const char* prv_hex, const char* pub_hex) : Identit } LocalIdentity::LocalIdentity(RNG* rng) { - uint8_t seed[SEED_SIZE]; rng->random(seed, SEED_SIZE); ed25519_create_keypair(pub_key, prv_key, seed); } @@ -51,12 +50,17 @@ LocalIdentity::LocalIdentity(RNG* rng) { bool LocalIdentity::readFrom(Stream& s) { bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE); success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE); + memset(seed, 0, SEED_SIZE); + if (success) { + s.readBytes(seed, SEED_SIZE); + } return success; } bool LocalIdentity::writeTo(Stream& s) const { bool success = (s.write(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE); success = success && (s.write(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE); + success = success && (s.write(seed, SEED_SIZE) == SEED_SIZE); return success; } @@ -65,26 +69,38 @@ void LocalIdentity::printTo(Stream& s) const { s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println(); } -size_t LocalIdentity::writeTo(uint8_t* dest, size_t max_len) { +size_t LocalIdentity::writePubkeyTo(uint8_t* dest, size_t max_len) { + if (max_len < PUB_KEY_SIZE) return 0; // not big enough + memcpy(dest, pub_key, PUB_KEY_SIZE); + return PUB_KEY_SIZE; +} + +size_t LocalIdentity::writePrvkeyTo(uint8_t* dest, size_t max_len) { if (max_len < PRV_KEY_SIZE) return 0; // not big enough + memcpy(dest, prv_key, PRV_KEY_SIZE); + return PRV_KEY_SIZE; +} - if (max_len < PRV_KEY_SIZE + PUB_KEY_SIZE) { // only room for prv_key - memcpy(dest, prv_key, PRV_KEY_SIZE); - return PRV_KEY_SIZE; - } - memcpy(dest, prv_key, PRV_KEY_SIZE); // otherwise can fit prv + pub keys - memcpy(&dest[PRV_KEY_SIZE], pub_key, PUB_KEY_SIZE); - return PRV_KEY_SIZE + PUB_KEY_SIZE; +size_t LocalIdentity::writeSeedTo(uint8_t* dest, size_t max_len) { + if (max_len < SEED_SIZE) return 0; // not big enough + memcpy(dest, seed, SEED_SIZE); + return SEED_SIZE; } void LocalIdentity::readFrom(const uint8_t* src, size_t len) { if (len == PRV_KEY_SIZE + PUB_KEY_SIZE) { // has prv + pub keys memcpy(prv_key, src, PRV_KEY_SIZE); memcpy(pub_key, &src[PRV_KEY_SIZE], PUB_KEY_SIZE); + memset(seed, 0, SEED_SIZE); } else if (len == PRV_KEY_SIZE) { memcpy(prv_key, src, PRV_KEY_SIZE); // now need to re-calculate the pub_key ed25519_derive_pub(pub_key, prv_key); + memset(seed, 0, SEED_SIZE); + } else if (len == SEED_SIZE) { + memcpy(seed, src, SEED_SIZE); + // re-generate the keypair from the given seed + ed25519_create_keypair(pub_key, prv_key, seed); } } @@ -96,4 +112,4 @@ void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_k ed25519_key_exchange(secret, other_pub_key, prv_key); } -} \ No newline at end of file +} diff --git a/src/Identity.h b/src/Identity.h index 42fb9d9ae..885055fbc 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -46,6 +46,7 @@ class Identity { */ class LocalIdentity : public Identity { uint8_t prv_key[PRV_KEY_SIZE]; + uint8_t seed[SEED_SIZE]; public: LocalIdentity(); LocalIdentity(const char* prv_hex, const char* pub_hex); @@ -76,7 +77,19 @@ class LocalIdentity : public Identity { bool readFrom(Stream& s); bool writeTo(Stream& s) const; void printTo(Stream& s) const; - size_t writeTo(uint8_t* dest, size_t max_len); + size_t writePubkeyTo(uint8_t* dest, size_t max_len); + size_t writePrvkeyTo(uint8_t* dest, size_t max_len); + size_t writeSeedTo(uint8_t* dest, size_t max_len); + /** + * \brief Set the Ed25519 keypair. + * \param src IN - the source for the key(s) or seed + * \param len IN - length of the input; if equal to SEED_SIZE, src is + * assumed to be a new seed, from which new private and public keys + * will be generated. If equal to PRV_KEY_SIZE, the corresponding + * public key will be re-generated. If equal to PRV_KEY_SIZE+ + * PUB_KEY_SIZE, no key regen is needed. The seed can only later + * be obtained via the `get prv.seed` CLI if SEED_SIZE is used. + */ void readFrom(const uint8_t* src, size_t len); }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 88327aa89..aa0077160 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -268,9 +268,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->guest_password); } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only uint8_t prv_key[PRV_KEY_SIZE]; - int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); + int len = _callbacks->getSelfId().writePrvkeyTo(prv_key, PRV_KEY_SIZE); mesh::Utils::toHex(tmp, prv_key, len); sprintf(reply, "> %s", tmp); + } else if (sender_timestamp == 0 && memcmp(config, "prv.seed", 8) == 0) { // from serial command line only + uint8_t seed[SEED_SIZE]; + int len = _callbacks->getSelfId().writeSeedTo(seed, SEED_SIZE); + mesh::Utils::toHex(tmp, seed, len); + sprintf(reply, "> %s", tmp); } else if (memcmp(config, "name", 4) == 0) { sprintf(reply, "> %s", _prefs->node_name); } else if (memcmp(config, "repeat", 6) == 0) { @@ -393,6 +398,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error, invalid key"); } + } else if (sender_timestamp == 0 && memcmp(config, "prv.seed ", 9) == 0) { // from serial command line only + uint8_t seed[SEED_SIZE]; + bool success = mesh::Utils::fromHex(seed, SEED_SIZE, &config[9]); + if (success) { + mesh::LocalIdentity new_id; + new_id.readFrom(seed, SEED_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, invalid seed"); + } } else if (memcmp(config, "name ", 5) == 0) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); savePrefs();