#include #include #include #include #include #include #include // From https://github.com/tkislan/base64 #include "base64.h" #include "json.hpp" using namespace std; using chrono::duration_cast; using chrono::seconds; using chrono::system_clock; using nlohmann::json; const int64_t about_3_months = (int64_t)105 * 86400; struct ExpiringSignature { string cert_s; vector sig; // C++ nonsense bool operator == (const ExpiringSignature & o) const { return cert_s == o.cert_s && sig == o.sig ; } bool operator != (const ExpiringSignature & o) const { return ! (*this == o); } }; int64_t get_seconds_since_epoch () { const auto utc_now = system_clock::now (); return duration_cast (utc_now.time_since_epoch ()).count (); } void try_sodium_init () { if (sodium_init () < 0) { throw std::runtime_error ("Can't initialize libsodium"); } } string to_base64 (const vector & v) { const string s ((const char *)v.data (), v.size ()); string b64; Base64::Encode (s, &b64); return b64; } class SigningKey { vector pk; vector sk; public: SigningKey () { try_sodium_init (); pk.resize (crypto_sign_PUBLICKEYBYTES); sk.resize (crypto_sign_SECRETKEYBYTES); crypto_sign_keypair (pk.data (), sk.data ()); } vector pubkey () const { return pk; } string pub_to_base64 () const { return to_base64 (pk); } optional sign_binary ( const vector & payload, string purpose, int64_t now ) const { try_sodium_init (); const auto not_after = now + about_3_months; const json j { {"not_before", now}, {"not_after", not_after}, {"purpose", purpose}, {"payload_b64", to_base64 (payload)}, }; const auto cert_s = j.dump (); vector sig; sig.resize (crypto_sign_BYTES); crypto_sign_detached (sig.data (), nullptr, (const uint8_t *)cert_s.data (), cert_s.size (), sk.data ()); return ExpiringSignature { cert_s, sig, }; } optional sign_key (const SigningKey & k, int64_t now) const { return sign_binary (k.pk, "4QHAB7O5 trusted public key", now); } }; // Most tests will use a virtual clock. But just as a smoke test, // make sure real time is realistic. int check_real_time () { const auto seconds_since_epoch = get_seconds_since_epoch (); const auto time_of_writing = 1610844872; if (seconds_since_epoch < time_of_writing) { cerr << "Error: Real time is in the past." << endl; return 1; } const int64_t about_100_years = (int64_t)100 * 365 * 86400; if (seconds_since_epoch > time_of_writing + about_100_years) { cerr << "Error: Real time is in the far future." << endl; return 1; } return 0; } int main () { // Suppose we generate a root key and keep it somewhere safe // (not a server) SigningKey root_key; cerr << "Root pub key " << root_key.pub_to_base64 () << endl; if (check_real_time () != 0) { return 1; } // Suppose the server generates a signing key SigningKey signing_key; cerr << "Signing key " << signing_key.pub_to_base64 () << endl; const auto now = get_seconds_since_epoch (); const ExpiringSignature cert = std::move (*root_key.sign_key (signing_key, now)); { // Check that a different time results in a different cert const auto cert_2 = std::move (*root_key.sign_key (signing_key, now)); const auto cert_3 = std::move (*root_key.sign_key (signing_key, now + 1)); if (cert != cert_2) { cerr << "Certs should have been identical" << endl; return 1; } if (cert == cert_3) { cerr << "Certs should have been different" << endl; return 1; } if (cert == cert_3) { cerr << "Signatures should have been different" << endl; return 1; } } { cerr << "Cert:" << endl; cerr << cert.cert_s << endl; cerr << to_base64 (cert.sig) << endl; } // Suppose the client knows our root public key const auto root_pubkey = root_key.pubkey (); if (crypto_sign_verify_detached (cert.sig.data (), (const uint8_t *)cert.cert_s.data (), cert.cert_s.size (), root_pubkey.data ()) != 0) { cerr << "Bad signature" << endl; return 1; } if (crypto_sign_verify_detached (cert.sig.data (), (const uint8_t *)cert.cert_s.data (), cert.cert_s.size () - 1, root_pubkey.data ()) == 0) { cerr << "Signature should not have verified" << endl; return 1; } const json j = json::parse (cert.cert_s); cerr << "Done." << endl; return 0; }