diff --git a/bare_minimum_crypto/cpp/bmc_main.cpp b/bare_minimum_crypto/cpp/bmc_main.cpp index 2de2402..ac4d022 100644 --- a/bare_minimum_crypto/cpp/bmc_main.cpp +++ b/bare_minimum_crypto/cpp/bmc_main.cpp @@ -50,28 +50,53 @@ int file (const string & file_path) { cout << "Schema: " << schema << endl; if (schema == "3T6XF5DZ") { - cout << "File is a passphrase-protected secret key" << endl; + cout << "File is a secret key for humans (It's passphrase-protected)" << endl; // Read msgpack fields - const Instant time_created (j ["time_created"]); - const auto pubkey = j ["pubkey"].get_binary (); - const string key_machine_id = j ["machine_id"]; + const auto key = std::move (*HumanKeyFile::try_from_msgpack (j)); // Read data from other places const auto now = Instant::now (); // Print normal stuff - cout << "Generated at Unix time " << time_created.x - << " (" << now.x - time_created.x << " seconds ago)" + cout << "Generated at Unix time " << key.time_created.x + << " (" << now.x - key.time_created.x << " seconds ago)" << endl; - cout << "Generated on machine ID " << key_machine_id << endl; - cout << "Claims to have Base64 pubkey " << base64_encode (pubkey) << endl; + cout << "Generated on machine ID " << key.machine_id << endl; + cout << "Claims to have Base64 pubkey " << base64_encode (key.pubkey) << endl; // Print warnings - if (now.x < time_created.x) { + if (now.x < key.time_created.x) { cout << "* The key was generated in the past. Someone's clock is wrong." << endl; } - if (get_machine_id () != key_machine_id) { + if (get_machine_id () != key.machine_id) { + cout << "* The key was generated on another machine. You should report this." << endl; + } + if (fs::status (file_path).permissions () != fs::perms::owner_read) { + cout << "* The key doesn't have the right permissions. Try `chmod 400` on it." << endl; + } + } + else if (schema == "2PVHIKMA") { + cout << "File is a secret key for machines (No passphrase)" << endl; + + // Read msgpack fields + const auto key = std::move (*MachineKeyFile::try_from_msgpack (j)); + + // Read data from other places + const auto now = Instant::now (); + + // Print normal stuff + cout << "Generated at Unix time " << key.time_created.x + << " (" << now.x - key.time_created.x << " seconds ago)" + << endl; + cout << "Generated on machine ID " << key.machine_id << endl; + cout << "Claims to have Base64 pubkey " << base64_encode (key.pubkey ()) << endl; + + // Print warnings + if (now.x < key.time_created.x) { + cout << "* The key was generated in the past. Someone's clock is wrong." << endl; + } + if (get_machine_id () != key.machine_id) { cout << "* The key was generated on another machine. You should report this." << endl; } if (fs::status (file_path).permissions () != fs::perms::owner_read) { @@ -136,7 +161,8 @@ int test () { int main (int argc, char ** argv) { cxxopts::Options options ("BareMinimumCrypto", "Simple crypto things you might need."); options.add_options () - ("generate-ca-key", "Generate a passphrase-protected certificate authority key", cxxopts::value ()) + ("generate-human-key", "Generate a passphrase-protected key for human use", cxxopts::value ()) + ("generate-machine-key", "Generate a key for machine use, with no passphrase", cxxopts::value ()) ("file", "Print info about any file generated by BMC", cxxopts::value ()) ("test", "Run self-test") ("help", "Print usage") @@ -147,13 +173,23 @@ int main (int argc, char ** argv) { if (result.count ("help")) { cout << options.help () << endl; } - else if (result.count ("generate-ca-key")) { - const auto file_path = result ["generate-ca-key"].as (); + else if (result.count ("generate-human-key")) { + const auto file_path = result ["generate-human-key"].as (); cout << "Type passphrase (it will be visible in the console)" << endl; string passphrase; cin >> passphrase; - auto key_opt = SigningKey::generate_to_file (file_path, passphrase); + auto key_opt = SigningKey::generate_human_key_file (file_path, passphrase); + if (! key_opt) { + cerr << "Error. Key was not generated" << endl; + return 1; + } + cout << "The pubkey is `" << base64_encode (key_opt->pubkey ()) << "`" << endl; + } + else if (result.count ("generate-machine-key")) { + const auto file_path = result ["generate-machine-key"].as (); + + auto key_opt = SigningKey::generate_machine_key_file (file_path); if (! key_opt) { cerr << "Error. Key was not generated" << endl; return 1; diff --git a/bare_minimum_crypto/cpp/signing_key.cpp b/bare_minimum_crypto/cpp/signing_key.cpp index 809849a..8af7574 100644 --- a/bare_minimum_crypto/cpp/signing_key.cpp +++ b/bare_minimum_crypto/cpp/signing_key.cpp @@ -23,7 +23,7 @@ namespace BareMinimumCrypto { return machine_id; } - vector SigningKeyFile::to_msgpack () const { + vector HumanKeyFile::to_msgpack () const { const auto j = json { // Breaking changes should generate a new Base32 schema. {"schema", "3T6XF5DZ"}, @@ -35,6 +35,70 @@ namespace BareMinimumCrypto { return json::to_msgpack (j); } + optional HumanKeyFile::try_from_msgpack (const json & j) + { + if (j ["schema"] != "3T6XF5DZ") { + return nullopt; + } + + return HumanKeyFile { + j ["salt"].get_binary (), + Instant (j ["time_created"]), + j ["pubkey"].get_binary (), + j ["machine_id"], + }; + } + + vector MachineKeyFile::to_msgpack () const { + const auto j = json { + // Breaking changes should generate a new Base32 schema. + {"schema", "2PVHIKMA"}, + {"secretkey", json::binary (secretkey)}, + {"time_created", time_created.x}, + {"machine_id", machine_id}, + }; + return json::to_msgpack (j); + } + + optional MachineKeyFile::try_from_msgpack (const json & j) + { + if (j ["schema"] != "2PVHIKMA") { + return nullopt; + } + + return MachineKeyFile { + j ["secretkey"].get_binary (), + Instant (j ["time_created"]), + j ["machine_id"], + }; + } + + vector MachineKeyFile::pubkey () const { + vector pk; + pk.resize (crypto_sign_PUBLICKEYBYTES); + crypto_sign_ed25519_sk_to_pk (pk.data (), secretkey.data ()); + return pk; + } + + bool save_key_file (const string & file_path, const vector msg) + { + ofstream f; + f.open (file_path, ofstream::binary); + if (! f.is_open ()) { + return false; + } + // Best-effort. It probably fails on Windows. + fs::permissions (file_path, + fs::perms::owner_read, + fs::perm_options::replace + ); + + f.write ((const char *)msg.data (), msg.size ()); + f.close (); + + return true; + } + // The whole process for a passphrased key is like this: // Passphrase + random salt --> // Seed --> @@ -42,7 +106,10 @@ namespace BareMinimumCrypto { // Passphrases should be mandatory for keys that can sign other keys. - optional SigningKey::generate_to_file (const string & file_path, const string & passphrase) { + optional SigningKey::generate_human_key_file (const string & file_path, const string & passphrase) + { + try_sodium_init (); + if (passphrase.size () < 8) { return nullopt; } @@ -64,6 +131,7 @@ namespace BareMinimumCrypto { return nullopt; } + // This generates a redundant key but that's fine. SigningKey key; key.pk.resize (crypto_sign_PUBLICKEYBYTES); key.sk.resize (crypto_sign_SECRETKEYBYTES); @@ -74,7 +142,7 @@ namespace BareMinimumCrypto { const auto machine_id = get_machine_id (); - SigningKeyFile key_on_disk { + HumanKeyFile key_on_disk { salt, Instant::now (), key.pk, @@ -82,18 +150,29 @@ namespace BareMinimumCrypto { }; const auto msg = key_on_disk.to_msgpack (); - ofstream f; - f.open (file_path, ofstream::binary); - if (! f.is_open ()) { + if (! save_key_file (file_path, msg)) { return nullopt; } - fs::permissions (file_path, - fs::perms::owner_read, - fs::perm_options::replace - ); - f.write ((const char *)msg.data (), msg.size ()); - f.close (); + return key; + } + + optional SigningKey::generate_machine_key_file (const string & file_path) + { + const SigningKey key; + + const auto machine_id = get_machine_id (); + + MachineKeyFile key_on_disk { + key.sk, + Instant::now (), + machine_id, + }; + const auto msg = key_on_disk.to_msgpack (); + + if (! save_key_file (file_path, msg)) { + return nullopt; + } return key; } diff --git a/bare_minimum_crypto/cpp/signing_key.h b/bare_minimum_crypto/cpp/signing_key.h index c0df333..2cb805c 100644 --- a/bare_minimum_crypto/cpp/signing_key.h +++ b/bare_minimum_crypto/cpp/signing_key.h @@ -5,22 +5,36 @@ #include #include +#include "json.hpp" + #include "expiring_signature.h" #include "time_helpers.h" namespace BareMinimumCrypto { using namespace std; + using nlohmann::json; string get_machine_id (); - struct SigningKeyFile { + struct HumanKeyFile { vector salt; Instant time_created; vector pubkey; string machine_id; vector to_msgpack () const; - static optional try_from_msgpack (const vector & msg); + static optional try_from_msgpack (const json & msg); + }; + + struct MachineKeyFile { + vector secretkey; + Instant time_created; + string machine_id; + + vector to_msgpack () const; + static optional try_from_msgpack (const json & msg); + + vector pubkey () const; }; struct SigningKey { @@ -33,9 +47,11 @@ namespace BareMinimumCrypto { // or filesystem nonsense right after this function returns. // It also doesn't do the rename trick. The caller may do that. - static optional generate_to_file (const string & file_path, const string & passphrase); + static optional generate_human_key_file (const string & file_path, const string & passphrase); - static optional load_from_file (const string & file_path, const string & passphrase); + static optional generate_machine_key_file (const string & file_path); + + static optional load_human_key_file (const string & file_path, const string & passphrase); vector pubkey () const; vector pub_to_msgpack () const; diff --git a/bare_minimum_crypto/schemas.md b/bare_minimum_crypto/schemas.md index 9062970..5eaa6d6 100644 --- a/bare_minimum_crypto/schemas.md +++ b/bare_minimum_crypto/schemas.md @@ -1,3 +1,9 @@ +- 2PVHIKMA - 3T6XF5DZ -3T6XF5DZ is a secret key protected by a passphrase. +2PVHIKMA is a secret key for machines. It has no passphrase, so +computer programs can use it automatically. + +3T6XF5DZ is a secret key for humans. It is protected by a passphrase. +The passphrase should be saved in a password manager or written on paper, +and not kept in the same disk as the key.