309 lines
6.8 KiB
C++
309 lines
6.8 KiB
C++
#include "signing_key.h"
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
|
|
#include "json.hpp"
|
|
|
|
#include "sodium_helpers.h"
|
|
|
|
namespace BareMinimumCrypto {
|
|
using nlohmann::json;
|
|
namespace fs = std::filesystem;
|
|
|
|
string get_machine_id () {
|
|
ifstream f;
|
|
f.open ("/etc/machine-id", ifstream::binary);
|
|
string machine_id;
|
|
if (! f.is_open ()) {
|
|
return machine_id;
|
|
}
|
|
|
|
f >> machine_id;
|
|
return machine_id;
|
|
}
|
|
|
|
Bytes HumanKeyFile::to_msgpack () const {
|
|
const auto j = json {
|
|
// All BMC msgpack artifacts should have this string
|
|
{"app", "4B27CL32"},
|
|
// Breaking changes should generate a new Base32 schema.
|
|
{"schema", "3T6XF5DZ"},
|
|
{"salt", json::binary (salt)},
|
|
{"time_created", time_created.x},
|
|
{"pubkey", json::binary (pubkey)},
|
|
{"machine_id", machine_id},
|
|
};
|
|
return json::to_msgpack (j);
|
|
}
|
|
|
|
optional <HumanKeyFile> HumanKeyFile::try_from_msgpack (const json & j)
|
|
{
|
|
if (j ["app"] != "4B27CL32") {
|
|
return nullopt;
|
|
}
|
|
if (j ["schema"] != "3T6XF5DZ") {
|
|
return nullopt;
|
|
}
|
|
|
|
return HumanKeyFile {
|
|
j ["salt"].get_binary (),
|
|
Instant (j ["time_created"]),
|
|
j ["pubkey"].get_binary (),
|
|
j ["machine_id"],
|
|
};
|
|
}
|
|
|
|
Bytes MachineKeyFile::to_msgpack () const {
|
|
const auto j = json {
|
|
// All BMC msgpack artifacts should have this string
|
|
{"app", "4B27CL32"},
|
|
// 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> MachineKeyFile::try_from_msgpack (const json & j)
|
|
{
|
|
if (j ["app"] != "4B27CL32") {
|
|
return nullopt;
|
|
}
|
|
if (j ["schema"] != "2PVHIKMA") {
|
|
return nullopt;
|
|
}
|
|
|
|
return MachineKeyFile {
|
|
j ["secretkey"].get_binary (),
|
|
Instant (j ["time_created"]),
|
|
j ["machine_id"],
|
|
};
|
|
}
|
|
|
|
Bytes MachineKeyFile::pubkey () const {
|
|
Bytes 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 Bytes 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;
|
|
}
|
|
|
|
string get_passphrase_from_user () {
|
|
cout << "Type or paste passphrase (it will be visible in the console)" << endl;
|
|
string passphrase;
|
|
cin >> passphrase;
|
|
|
|
return passphrase;
|
|
}
|
|
|
|
optional <json> try_load_msgpack_file (const string & file_path) {
|
|
ifstream f;
|
|
f.open (file_path, ifstream::binary);
|
|
if (! f.is_open ()) {
|
|
return nullopt;
|
|
}
|
|
|
|
f.seekg (0, ifstream::end);
|
|
const auto len = f.tellg ();
|
|
f.seekg (0, ifstream::beg);
|
|
|
|
if (len > 1024 * 1024) {
|
|
return nullopt;
|
|
}
|
|
|
|
Bytes bytes;
|
|
bytes.resize (len);
|
|
|
|
f.read ((char *)bytes.data (), bytes.size ());
|
|
|
|
return json::from_msgpack (bytes);
|
|
}
|
|
|
|
optional <SigningKey> HumanKeyFile::unlock_key (const Bytes & salt, const string & passphrase)
|
|
{
|
|
Bytes seed;
|
|
seed.resize (crypto_sign_SEEDBYTES);
|
|
|
|
if (crypto_pwhash (
|
|
seed.data (), seed.size (),
|
|
passphrase.data (), passphrase.size (),
|
|
salt.data (),
|
|
crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
|
crypto_pwhash_ALG_DEFAULT
|
|
) != 0) {
|
|
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);
|
|
|
|
Bytes pk;
|
|
pk.resize (crypto_sign_PUBLICKEYBYTES);
|
|
if (crypto_sign_seed_keypair (pk.data (), key.sk.data (), seed.data ()) != 0) {
|
|
return nullopt;
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
// The whole process for a passphrased key is like this:
|
|
// Passphrase + random salt -->
|
|
// Seed -->
|
|
// Secret key + Public key
|
|
|
|
// Passphrases should be mandatory for keys that can sign other keys.
|
|
|
|
optional <SigningKey> HumanKeyFile::generate (const string & file_path, const string & passphrase)
|
|
{
|
|
try_sodium_init ();
|
|
|
|
if (passphrase.size () < 8) {
|
|
return nullopt;
|
|
}
|
|
|
|
Bytes salt;
|
|
salt.resize (crypto_pwhash_SALTBYTES);
|
|
randombytes_buf (salt.data (), salt.size ());
|
|
|
|
auto key_opt = unlock_key (salt, passphrase);
|
|
if (! key_opt) {
|
|
return nullopt;
|
|
}
|
|
const auto key = std::move (*key_opt);
|
|
|
|
const auto machine_id = get_machine_id ();
|
|
|
|
HumanKeyFile key_on_disk {
|
|
salt,
|
|
Instant::now (),
|
|
key.pubkey (),
|
|
machine_id,
|
|
};
|
|
const auto msg = key_on_disk.to_msgpack ();
|
|
|
|
if (! save_key_file (file_path, msg)) {
|
|
return nullopt;
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
optional <SigningKey> HumanKeyFile::load (const string & file_path, const string & passphrase)
|
|
{
|
|
const auto j = std::move (*try_load_msgpack_file (file_path));
|
|
const auto human_key = std::move (*HumanKeyFile::try_from_msgpack (j));
|
|
|
|
const auto key = std::move (*unlock_key (human_key.salt, passphrase));
|
|
|
|
return key;
|
|
}
|
|
|
|
optional <SigningKey> MachineKeyFile::generate (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;
|
|
}
|
|
|
|
SigningKey::SigningKey () {
|
|
try_sodium_init ();
|
|
|
|
Bytes pk;
|
|
pk.resize (crypto_sign_PUBLICKEYBYTES);
|
|
sk.resize (crypto_sign_SECRETKEYBYTES);
|
|
|
|
crypto_sign_keypair (pk.data (), sk.data ());
|
|
}
|
|
|
|
Bytes SigningKey::pubkey () const {
|
|
Bytes pk;
|
|
pk.resize (crypto_sign_PUBLICKEYBYTES);
|
|
crypto_sign_ed25519_sk_to_pk (pk.data (), sk.data ());
|
|
return pk;
|
|
}
|
|
|
|
Bytes SigningKey::pub_to_msgpack () const {
|
|
const json j = {
|
|
{"key", json::binary (pubkey ())},
|
|
};
|
|
return json::to_msgpack (j);
|
|
}
|
|
|
|
optional <ExpiringSignature> SigningKey::sign (
|
|
const Bytes & payload,
|
|
TimeRange tr
|
|
) const {
|
|
try_sodium_init ();
|
|
|
|
if (tr.duration () > about_1_year) {
|
|
return nullopt;
|
|
}
|
|
|
|
const json j {
|
|
{"not_before", tr.not_before},
|
|
{"not_after", tr.not_after},
|
|
{"payload", json::binary (payload)},
|
|
};
|
|
|
|
const auto cert = json::to_msgpack (j);
|
|
|
|
Bytes sig;
|
|
sig.resize (crypto_sign_BYTES);
|
|
|
|
crypto_sign_detached (sig.data (), nullptr, cert.data (), cert.size (), sk.data ());
|
|
|
|
return ExpiringSignature {
|
|
cert,
|
|
sig,
|
|
pubkey (),
|
|
};
|
|
}
|
|
|
|
optional <ExpiringSignature> SigningKey::sign_key (const Bytes & pubkey, Instant now) const
|
|
{
|
|
return sign (pubkey, TimeRange::from_start_and_dur (now, about_3_months));
|
|
}
|
|
|
|
optional <ExpiringSignature> SigningKey::sign_data (const Bytes & v, Instant now) const
|
|
{
|
|
return sign (v, TimeRange::from_start_and_dur (now, about_1_week));
|
|
}
|
|
}
|