mirror of
https://github.com/oxen-io/oxen-core.git
synced 2023-12-14 02:22:56 +01:00
Add loki-sn-keys SN key management tool (#1241)
This allows inspecting, generating, and restoring both Ed25519 and legacy (naming in line with PR #1240 now avoiding the non-Ed25519 keys by default) secret key files as needed by lokid: $ ./loki-sn-keys generate key_ed25519 Generated SN Ed25519 secret key in key_ed25519 Public key: 8f94f6bbc1d5876484309fc7fd41396c6bd66c4631a34f04895887547cb374dd X25519 pubkey: f3e42d1aff8db8232433ec86e3f56cdda44d4c510144f9b6786520175b08c97f Lokinet address: t6kxpq6b4sdsjbbou9d94oj3pti7c5ngggtw6brjmndie9fuquqo.snode $ ./loki-sn-keys legacy key Generated SN legacy private key in key Public key: f3a513ddfbe946c79c62c27cca2260b80606a2da1b4f75ba16c793a5750a8510 $ ./loki-sn-keys show key key (legacy SN keypair) ========== Private key: c8c17cc296c3e8bb603098b5d2535ad86662cdb24cd4c785c0485d0a490a2356 Public key: f3a513ddfbe946c79c62c27cca2260b80606a2da1b4f75ba16c793a5750a8510 $ ./loki-sn-keys show key_ed25519 key_ed25519 (Ed25519 SN keypair) ========== Secret key: 3f078900bac5f3e97d5fe451a6e332826d29aae2e13fec895c1b527af88093c3 Public key: 8f94f6bbc1d5876484309fc7fd41396c6bd66c4631a34f04895887547cb374dd X25519 pubkey: f3e42d1aff8db8232433ec86e3f56cdda44d4c510144f9b6786520175b08c97f Lokinet address: t6kxpq6b4sdsjbbou9d94oj3pti7c5ngggtw6brjmndie9fuquqo.snode $ ./loki-sn-keys restore key_ed25519-2 Enter the Ed25519 secret key: 3f078900bac5f3e97d5fe451a6e332826d29aae2e13fec895c1b527af88093c3 Public key: 8f94f6bbc1d5876484309fc7fd41396c6bd66c4631a34f04895887547cb374dd X25519 pubkey: f3e42d1aff8db8232433ec86e3f56cdda44d4c510144f9b6786520175b08c97f Lokinet address: t6kxpq6b4sdsjbbou9d94oj3pti7c5ngggtw6brjmndie9fuquqo.snode Is this correct? Press Enter to continue, Ctrl-C to cancel. Saved secret key to key_ed25519-2 $ ./loki-sn-keys restore-legacy key-2 Enter the legacy SN private key: c8c17cc296c3e8bb603098b5d2535ad86662cdb24cd4c785c0485d0a490a2356 Public key: f3a513ddfbe946c79c62c27cca2260b80606a2da1b4f75ba16c793a5750a8510 Is this correct? Press Enter to continue, Ctrl-C to cancel. Saved secret key to key-2
This commit is contained in:
parent
d21d1ab15e
commit
0bfafd15b6
2 changed files with 375 additions and 0 deletions
|
@ -105,3 +105,6 @@ target_link_libraries(blockchain_stats PRIVATE blockchain_tools_common_libs)
|
|||
# PRIVATE
|
||||
# blockchain_tools_common_libs
|
||||
# p2p)
|
||||
|
||||
loki_add_executable(sn_key_tool "loki-sn-keys" sn_key_tool.cpp)
|
||||
target_link_libraries(sn_key_tool PRIVATE sodium lokimq)
|
||||
|
|
372
src/blockchain_utilities/sn_key_tool.cpp
Normal file
372
src/blockchain_utilities/sn_key_tool.cpp
Normal file
|
@ -0,0 +1,372 @@
|
|||
#include <algorithm>
|
||||
extern "C" {
|
||||
#include <sodium.h>
|
||||
}
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <lokimq/hex.h>
|
||||
#include <lokimq/base32z.h>
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
std::string_view arg0;
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
int usage(int exit_code, std::string_view msg = ""sv) {
|
||||
if (!msg.empty())
|
||||
std::cout << "\n" << msg << "\n\n";
|
||||
std::cout << "Usage: " << arg0 << R"( COMMAND [OPTIONS...] where support COMMANDs are:
|
||||
|
||||
generate [--overwrite] FILENAME
|
||||
|
||||
Generates a new Ed25519 service node keypair and writes the secret key to
|
||||
FILENAME. If FILENAME contains the string "PUBKEY" it will be replaced
|
||||
with the generated public key value (in hex).
|
||||
|
||||
For an active service node this file is named `key_ed25519` in the lokid
|
||||
data directory.
|
||||
|
||||
If FILENAME already exists the command will fail unless the `--overwrite`
|
||||
flag is specified.
|
||||
|
||||
legacy [--overwrite] FILENAME
|
||||
|
||||
Generates a new service node legacy keypair and write the private key to
|
||||
FILENAME. If FILENAME contains the string "PUBKEY" it will be replaced with
|
||||
the generated public key value (in hex).
|
||||
|
||||
If FILENAME already exists the command will fail unless the `--overwrite`
|
||||
flag is specified.
|
||||
|
||||
Note that legacy keypairs are not needed as of Loki 8.x; you can use just a
|
||||
Ed25519 keypair (and this is the default for new service node
|
||||
installations).
|
||||
|
||||
show [--ed25519|--legacy] FILENAME
|
||||
|
||||
Reads FILENAME as a service node secret key (Ed25519 or legacy) and
|
||||
displays it as a hex value along with the associated public key. The
|
||||
displayed secret key can be saved and later used to recreate the secret key
|
||||
file with the `restore` command.
|
||||
|
||||
--ed25519 and --legacy are not normally required as they can usually be
|
||||
inferred from the size of the given file (32 bytes = legacy, 64 bytes =
|
||||
Ed25519). The options can be used to force the file to be interpreted as
|
||||
a secret key of the specified type.
|
||||
|
||||
restore [--overwrite] FILENAME
|
||||
restore-legacy [--overwrite] FILENAME
|
||||
|
||||
Restore an Ed25519 (restore) or legacy (restore-legacy) secret key and
|
||||
write it to FILENAME. You will be prompted to provide a secret key hex
|
||||
value (as produced by the show command) and asked to confirm the public key
|
||||
for confirmation. As with `generate', if FILENAME contains the string
|
||||
"PUBKEY" it will be replaced with the actual public key (in hex).
|
||||
|
||||
If FILENAME already exists the command will fail unless the `--overwrite`
|
||||
flag is specified.
|
||||
|
||||
)";
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
int error(int exit_code, std::string_view msg) {
|
||||
std::cout << "\n" << msg << "\n\n";
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
using ustring = std::basic_string<unsigned char>;
|
||||
using ustring_view = std::basic_string_view<unsigned char>;
|
||||
|
||||
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey_from_privkey(ustring_view privkey) {
|
||||
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey;
|
||||
// noclamp because Monero keys are not clamped at all, and because sodium keys are pre-clamped.
|
||||
crypto_scalarmult_ed25519_base_noclamp(pubkey.data(), privkey.data());
|
||||
return pubkey;
|
||||
}
|
||||
template <size_t N, std::enable_if_t<(N >= 32), int> = 0>
|
||||
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey_from_privkey(const std::array<unsigned char, N>& privkey) {
|
||||
return pubkey_from_privkey(ustring_view{privkey.data(), 32});
|
||||
}
|
||||
|
||||
int generate(bool ed25519, std::list<std::string_view> args) {
|
||||
bool overwrite = false;
|
||||
if (!args.empty()) {
|
||||
if (args.front() == "--overwrite") {
|
||||
overwrite = true;
|
||||
args.pop_front();
|
||||
} else if (args.back() == "--overwrite") {
|
||||
overwrite = true;
|
||||
args.pop_back();
|
||||
}
|
||||
}
|
||||
if (args.empty())
|
||||
return error(2, "generate requires a FILENAME");
|
||||
else if (args.size() > 1)
|
||||
return error(2, "unknown arguments to 'generate'");
|
||||
|
||||
std::string filename{args.front()};
|
||||
size_t pubkey_pos = filename.find("PUBKEY");
|
||||
if (pubkey_pos != std::string::npos)
|
||||
overwrite = true;
|
||||
|
||||
if (!overwrite) {
|
||||
std::ifstream f{filename};
|
||||
if (f.good())
|
||||
return error(2, filename + " to generate already exists, pass `--overwrite' if you want to overwrite it");
|
||||
}
|
||||
|
||||
std::array<unsigned char, crypto_sign_PUBLICKEYBYTES> pubkey;
|
||||
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> seckey;
|
||||
|
||||
crypto_sign_keypair(pubkey.data(), seckey.data());
|
||||
std::array<unsigned char, crypto_hash_sha512_BYTES> privkey_signhash;
|
||||
crypto_hash_sha512(privkey_signhash.data(), seckey.data(), 32);
|
||||
// Clamp it to prevent small subgroups:
|
||||
privkey_signhash[0] &= 248;
|
||||
privkey_signhash[31] &= 63;
|
||||
privkey_signhash[31] |= 64;
|
||||
|
||||
ustring_view privkey{privkey_signhash.data(), 32};
|
||||
|
||||
// Double-check that we did it properly:
|
||||
if (pubkey_from_privkey(privkey) != pubkey)
|
||||
return error(11, "Internal error: pubkey check failed");
|
||||
|
||||
if (pubkey_pos != std::string::npos)
|
||||
filename.replace(pubkey_pos, 6, lokimq::to_hex(pubkey.begin(), pubkey.end()));
|
||||
std::ofstream out{filename, std::ios::trunc | std::ios::binary};
|
||||
if (!out.good())
|
||||
return error(2, "Failed to open output file '" + filename + "': " + std::strerror(errno));
|
||||
if (ed25519)
|
||||
out.write(reinterpret_cast<const char*>(seckey.data()), seckey.size());
|
||||
else
|
||||
out.write(reinterpret_cast<const char*>(privkey.data()), privkey.size());
|
||||
|
||||
if (!out.good())
|
||||
return error(2, "Failed to write to output file '" + filename + "': " + std::strerror(errno));
|
||||
|
||||
std::cout << "Generated SN " << (ed25519 ? "Ed25519 secret key" : "legacy private key") << " in " << filename << "\n";
|
||||
|
||||
if (ed25519) {
|
||||
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
|
||||
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
|
||||
return error(14, "Internal error: unable to convert Ed25519 pubkey to X25519 pubkey");
|
||||
std::cout <<
|
||||
"Public key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) <<
|
||||
"\nX25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
|
||||
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode\n";
|
||||
} else {
|
||||
std::cout << "Public key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int show(std::list<std::string_view> args) {
|
||||
|
||||
bool legacy = false, ed25519 = false;
|
||||
if (!args.empty()) {
|
||||
if (args.front() == "--legacy") {
|
||||
legacy = true;
|
||||
args.pop_front();
|
||||
} else if (args.back() == "--legacy") {
|
||||
legacy = true;
|
||||
args.pop_back();
|
||||
} else if (args.front() == "--ed25519") {
|
||||
ed25519 = true;
|
||||
args.pop_front();
|
||||
} else if (args.back() == "--ed25519") {
|
||||
ed25519 = true;
|
||||
args.pop_back();
|
||||
}
|
||||
}
|
||||
if (args.empty())
|
||||
return error(2, "show requires a FILENAME");
|
||||
else if (args.size() > 1)
|
||||
return error(2, "unknown arguments to 'show'");
|
||||
|
||||
std::string filename{args.front()};
|
||||
std::ifstream in{filename, std::ios::binary};
|
||||
if (!in.good())
|
||||
return error(2, "Unable to open '" + filename + "': " + std::strerror(errno));
|
||||
|
||||
in.seekg(0, std::ios::end);
|
||||
auto size = in.tellg();
|
||||
in.seekg(0, std::ios::beg);
|
||||
if (!legacy && !ed25519) {
|
||||
if (size == 32)
|
||||
legacy = true;
|
||||
else if (size == 64)
|
||||
ed25519 = true;
|
||||
}
|
||||
if (!legacy && !ed25519)
|
||||
return error(2, "Could not autodetect key type from " + std::to_string(size) + "-byte file; check the file or pass the --ed25519 or --legacy argument");
|
||||
|
||||
if (size < 32)
|
||||
return error(2, "File size (" + std::to_string(size) + " bytes) is too small to be a secret key");
|
||||
|
||||
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey;
|
||||
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
|
||||
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> seckey;
|
||||
in.read(reinterpret_cast<char*>(seckey.data()), size >= 64 ? 64 : 32);
|
||||
if (!in.good())
|
||||
return error(2, "Failed to read from " + filename + ": " + std::strerror(errno));
|
||||
|
||||
if (legacy) {
|
||||
pubkey = pubkey_from_privkey(seckey);
|
||||
|
||||
std::cout << filename << " (legacy SN keypair)" << "\n==========" <<
|
||||
"\nPrivate key: " << lokimq::to_hex(seckey.begin(), seckey.begin() + 32) <<
|
||||
"\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::array<unsigned char, crypto_hash_sha512_BYTES> privkey_signhash;
|
||||
crypto_hash_sha512(privkey_signhash.data(), seckey.data(), 32);
|
||||
privkey_signhash[0] &= 248;
|
||||
privkey_signhash[31] &= 63;
|
||||
privkey_signhash[31] |= 64;
|
||||
|
||||
ustring_view privkey{privkey_signhash.data(), 32};
|
||||
pubkey = pubkey_from_privkey(privkey);
|
||||
if (size >= 64 && ustring_view{pubkey.data(), pubkey.size()} != ustring_view{seckey.data() + 32, 32})
|
||||
return error(13, "Error: derived pubkey (" + lokimq::to_hex(pubkey.begin(), pubkey.end()) + ")"
|
||||
" != embedded pubkey (" + lokimq::to_hex(seckey.begin() + 32, seckey.end()) + ")");
|
||||
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
|
||||
return error(14, "Unable to convert Ed25519 pubkey to X25519 pubkey; is this a really valid secret key?");
|
||||
|
||||
std::cout << filename << " (Ed25519 SN keypair)" << "\n==========" <<
|
||||
"\nSecret key: " << lokimq::to_hex(seckey.begin(), seckey.begin() + 32) <<
|
||||
"\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) <<
|
||||
"\nX25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
|
||||
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode\n\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int restore(bool ed25519, std::list<std::string_view> args) {
|
||||
bool overwrite = false;
|
||||
if (!args.empty()) {
|
||||
if (args.front() == "--overwrite") {
|
||||
overwrite = true;
|
||||
args.pop_front();
|
||||
} else if (args.back() == "--overwrite") {
|
||||
overwrite = true;
|
||||
args.pop_back();
|
||||
}
|
||||
}
|
||||
if (args.empty())
|
||||
return error(2, "restore requires a FILENAME");
|
||||
else if (args.size() > 1)
|
||||
return error(2, "unknown arguments to 'restore'");
|
||||
|
||||
std::string filename{args.front()};
|
||||
size_t pubkey_pos = filename.find("PUBKEY");
|
||||
|
||||
if (ed25519)
|
||||
std::cout << "Enter the Ed25519 secret key:\n";
|
||||
else
|
||||
std::cout << "Enter the legacy SN private key:\n";
|
||||
char buf[129];
|
||||
std::cin.getline(buf, 129);
|
||||
if (!std::cin.good())
|
||||
return error(7, "Invalid input, aborting!");
|
||||
std::string_view skey_hex{buf};
|
||||
|
||||
// Advanced feature: if you provide the concatenated privkey and pubkey in hex, we won't prompt
|
||||
// for verification (as long as the pubkey matches what we derive from the privkey).
|
||||
if (!(skey_hex.size() == 64 || skey_hex.size() == 128) || !lokimq::is_hex(skey_hex))
|
||||
return error(7, "Invalid input: provide the secret key as 64 hex characters");
|
||||
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> skey;
|
||||
std::array<unsigned char, crypto_sign_PUBLICKEYBYTES> pubkey;
|
||||
std::optional<std::array<unsigned char, crypto_sign_PUBLICKEYBYTES>> pubkey_expected;
|
||||
lokimq::from_hex(skey_hex.begin(), skey_hex.begin() + 64, skey.begin());
|
||||
if (skey_hex.size() == 128)
|
||||
lokimq::from_hex(skey_hex.begin() + 64, skey_hex.end(), pubkey_expected.emplace().begin());
|
||||
|
||||
if (ed25519) {
|
||||
crypto_sign_seed_keypair(pubkey.data(), skey.data(), skey.data());
|
||||
} else {
|
||||
pubkey = pubkey_from_privkey(skey);
|
||||
}
|
||||
|
||||
std::cout << "\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n";
|
||||
if (ed25519) {
|
||||
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
|
||||
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
|
||||
return error(14, "Unable to convert Ed25519 pubkey to X25519 pubkey; is this a really valid secret key?");
|
||||
std::cout << "X25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
|
||||
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode";
|
||||
}
|
||||
|
||||
if (pubkey_expected) {
|
||||
if (*pubkey_expected != pubkey)
|
||||
return error(2, "Derived pubkey (" + lokimq::to_hex(pubkey.begin(), pubkey.end()) + ") doesn't match "
|
||||
"provided pubkey (" + lokimq::to_hex(pubkey_expected->begin(), pubkey_expected->end()) + ")");
|
||||
} else {
|
||||
std::cout << "\nIs this correct? Press Enter to continue, Ctrl-C to cancel.\n";
|
||||
std::cin.getline(buf, 129);
|
||||
if (!std::cin.good())
|
||||
error(99, "Aborted");
|
||||
}
|
||||
|
||||
if (pubkey_pos != std::string::npos)
|
||||
filename.replace(pubkey_pos, 6, lokimq::to_hex(pubkey.begin(), pubkey.end()));
|
||||
|
||||
if (!overwrite) {
|
||||
std::ifstream f{filename};
|
||||
if (f.good())
|
||||
return error(2, filename + " to generate already exists, pass `--overwrite' if you want to overwrite it");
|
||||
}
|
||||
|
||||
std::ofstream out{filename, std::ios::trunc | std::ios::binary};
|
||||
if (!out.good())
|
||||
return error(2, "Failed to open output file '" + filename + "': " + std::strerror(errno));
|
||||
if (ed25519)
|
||||
out.write(reinterpret_cast<const char*>(skey.data()), skey.size());
|
||||
else
|
||||
out.write(reinterpret_cast<const char*>(skey.data()), 32);
|
||||
|
||||
if (!out.good())
|
||||
return error(2, "Failed to write to output file '" + filename + "': " + std::strerror(errno));
|
||||
|
||||
std::cout << "Saved secret key to " << filename << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
arg0 = argv[0];
|
||||
if (argc < 2)
|
||||
return usage(1, "No command specified!");
|
||||
|
||||
std::string_view cmd{argv[1]};
|
||||
std::list<std::string_view> args{argv + 2, argv + argc};
|
||||
|
||||
if (sodium_init() == -1) {
|
||||
std::cerr << "Sodium initialization failed! Unable to continue.\n\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
for (auto& flag : {"--help"sv, "-h"sv, "-?"sv})
|
||||
for (auto& arg : args)
|
||||
if (arg == flag)
|
||||
return usage(0);
|
||||
|
||||
if (cmd == "generate")
|
||||
return generate(true, std::move(args));
|
||||
if (cmd == "legacy")
|
||||
return generate(false, std::move(args));
|
||||
if (cmd == "show")
|
||||
return show(std::move(args));
|
||||
if (cmd == "restore")
|
||||
return restore(true, std::move(args));
|
||||
if (cmd == "restore-legacy")
|
||||
return restore(false, std::move(args));
|
||||
|
||||
return usage(1, "Unknown command `" + std::string{cmd} + "'");
|
||||
}
|
Loading…
Reference in a new issue