#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; // Not sure why the Base64 lib fails to provide this API string b64_encode (const vector & v) { string s; s.resize (Base64::EncodedLength (v.size ())); Base64::Encode ((const char *)v.data (), v.size (), s.data (), s.size ()); return s; } optional > b64_decode (const string & s) { vector v; v.resize (Base64::DecodedLength (s.data (), s.size ())); if (! Base64::Decode (s.data (), s.size (), (char *)v.data (), v.size ())) { return nullopt; } return v; } 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"); } } struct VerifiedData { vector payload; string purpose; }; optional try_verify_signed_data ( const ExpiringSignature & sig, int64_t now, const vector & pubkey ) { try_sodium_init (); if (pubkey.size () != crypto_sign_PUBLICKEYBYTES) { return nullopt; } if (crypto_sign_verify_detached ( sig.sig.data (), (const uint8_t *)sig.cert_s.data (), sig.cert_s.size (), pubkey.data () ) != 0) { return nullopt; } const json j = json::parse (sig.cert_s); const int64_t not_before = j ["not_before"]; const int64_t not_after = j ["not_after"]; if (now < not_before) { return nullopt; } if (now > not_after) { return nullopt; } const string purpose = j ["purpose"]; const string payload_b64 = j ["payload_b64"]; string payload_s; Base64::Decode (payload_b64, &payload_s); const vector payload; return VerifiedData { payload, purpose }; } optional verify_signed_data ( const ExpiringSignature & sig, int64_t now, const vector & pubkey ) { try { return try_verify_signed_data (sig, now, pubkey); } catch (json::exception &) { return nullopt; } } 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 check_base64 () { vector v {1, 2, 3, 4, 5, 6}; const auto s = b64_encode (v); if (s != "AQIDBAUG") { cerr << "Base64 encoding failed" << endl; return 1; } // Trivial decode const auto v2 = std::move (*b64_decode (s)); if (v2 != v) { cerr << "Base64 trivial decode failed" << endl; return 1; } // Decode should fail const auto v3 = b64_decode ("AQIDBAUG."); if (v3 != nullopt) { cerr << "Base64 decode should have failed" << endl; return 1; } return 0; } int main () { if (check_base64 () != 0) { return 1; } // 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 << "not_before: " << (int64_t)j ["not_before"] << endl; } cerr << "Done." << endl; return 0; }