ptth/bare_minimum_crypto/cpp/signing_key.cpp

298 lines
6.5 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 {
// 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 ["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 {
// 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 ["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,
};
}
optional <ExpiringSignature> SigningKey::sign_key (const SigningKey & k, Instant now) const
{
return sign (k.pub_to_msgpack (), 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));
}
}