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:
Jason Rhinelander 2020-09-01 03:21:25 -03:00 committed by GitHub
parent d21d1ab15e
commit 0bfafd15b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 375 additions and 0 deletions

View file

@ -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)

View 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} + "'");
}