#pragma once #include #include #include #include #include #include #include namespace xrpl { /* Validator key manifests ----------------------- Suppose the secret keys installed on a Ripple validator are compromised. Not only do you have to generate and install new key pairs on each validator, EVERY rippled needs to have its config updated with the new public keys, and is vulnerable to forged validation signatures until this is done. The solution is a new layer of indirection: A master secret key under restrictive access control is used to sign a "manifest": essentially, a certificate including the master public key, an ephemeral public key for verifying validations (which will be signed by its secret counterpart), a sequence number, and a digital signature. The manifest has two serialized forms: one which includes the digital signature and one which doesn't. There is an obvious causal dependency relationship between the (latter) form with no signature, the signature of that form, and the (former) form which includes that signature. In other words, a message can't contain a signature of itself. The code below stores a serialized manifest which includes the signature, and dynamically generates the signatureless form when it needs to verify the signature. An instance of ManifestCache stores, for each trusted validator, (a) its master public key, and (b) the most senior of all valid manifests it has seen for that validator, if any. On startup, the [validator_token] config entry (which contains the manifest for this validator) is decoded and added to the manifest cache. Other manifests are added as "gossip" received from rippled peers. When an ephemeral key is compromised, a new signing key pair is created, along with a new manifest vouching for it (with a higher sequence number), signed by the master key. When a rippled peer receives the new manifest, it verifies it with the master key and (assuming it's valid) discards the old ephemeral key and stores the new one. If the master key itself gets compromised, a manifest with sequence number 0xFFFFFFFF will supersede a prior manifest and discard any existing ephemeral key without storing a new one. These revocation manifests are loaded from the [validator_key_revocation] config entry as well as received as gossip from peers. Since no further manifests for this master key will be accepted (since no higher sequence number is possible), and no signing key is on record, no validations will be accepted from the compromised validator. */ //------------------------------------------------------------------------------ struct Manifest { /// The manifest in serialized form. std::string serialized; /// The master key associated with this manifest. PublicKey masterKey; /// The ephemeral key associated with this manifest. // A revoked manifest does not have a signingKey // This field is specified as "optional" in manifestFormat's // SOTemplate std::optional signingKey; /// The sequence number of this manifest. std::uint32_t sequence = 0; /// The domain, if one was specified in the manifest; empty otherwise. std::string domain; Manifest() = delete; Manifest( std::string const& serialized_, PublicKey const& masterKey_, std::optional const& signingKey_, std::uint32_t seq, std::string const& domain_) : serialized(serialized_) , masterKey(masterKey_) , signingKey(signingKey_) , sequence(seq) , domain(domain_) { } Manifest(Manifest const& other) = delete; Manifest& operator=(Manifest const& other) = delete; Manifest(Manifest&& other) = default; Manifest& operator=(Manifest&& other) = default; /// Returns `true` if manifest signature is valid bool verify() const; /// Returns hash of serialized manifest data uint256 hash() const; /// Returns `true` if manifest revokes master key // The maximum possible sequence number means that the master key has // been revoked static bool revoked(std::uint32_t sequence); /// Returns `true` if manifest revokes master key bool revoked() const; /// Returns manifest signature std::optional getSignature() const; /// Returns manifest master key signature Blob getMasterSignature() const; }; /** Format the specified manifest to a string for debugging purposes. */ std::string to_string(Manifest const& m); /** Constructs Manifest from serialized string @param s Serialized manifest string @return `std::nullopt` if string is invalid @note This does not verify manifest signatures. `Manifest::verify` should be called after constructing manifest. */ /** @{ */ std::optional deserializeManifest(Slice s, beast::Journal journal); inline std::optional deserializeManifest( std::string const& s, beast::Journal journal = beast::Journal(beast::Journal::getNullSink())) { return deserializeManifest(makeSlice(s), journal); } template < class T, class = std::enable_if_t::value || std::is_same::value>> std::optional deserializeManifest( std::vector const& v, beast::Journal journal = beast::Journal(beast::Journal::getNullSink())) { return deserializeManifest(makeSlice(v), journal); } /** @} */ inline bool operator==(Manifest const& lhs, Manifest const& rhs) { // In theory, comparing the two serialized strings should be // sufficient. return lhs.sequence == rhs.sequence && lhs.masterKey == rhs.masterKey && lhs.signingKey == rhs.signingKey && lhs.domain == rhs.domain && lhs.serialized == rhs.serialized; } inline bool operator!=(Manifest const& lhs, Manifest const& rhs) { return !(lhs == rhs); } struct ValidatorToken { std::string manifest; SecretKey validationSecret; }; std::optional loadValidatorToken( std::vector const& blob, beast::Journal journal = beast::Journal(beast::Journal::getNullSink())); enum class ManifestDisposition { /// Manifest is valid accepted = 0, /// Sequence is too old stale, /// The master key is not acceptable to us badMasterKey, /// The ephemeral key is not acceptable to us badEphemeralKey, /// Timely, but invalid signature invalid }; inline std::string to_string(ManifestDisposition m) { switch (m) { case ManifestDisposition::accepted: return "accepted"; case ManifestDisposition::stale: return "stale"; case ManifestDisposition::badMasterKey: return "badMasterKey"; case ManifestDisposition::badEphemeralKey: return "badEphemeralKey"; case ManifestDisposition::invalid: return "invalid"; default: return "unknown"; } } class DatabaseCon; /** Remembers manifests with the highest sequence number. */ class ManifestCache { private: beast::Journal j_; std::shared_mutex mutable mutex_; /** Active manifests stored by master public key. */ hash_map map_; /** Master public keys stored by current ephemeral public key. */ hash_map signingToMasterKeys_; std::atomic seq_{0}; public: explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j) { } /** A monotonically increasing number used to detect new manifests. */ std::uint32_t sequence() const { return seq_.load(); } /** Returns master key's current signing key. @param pk Master public key @return pk if no known signing key from a manifest @par Thread Safety May be called concurrently */ std::optional getSigningKey(PublicKey const& pk) const; /** Returns ephemeral signing key's master public key. @param pk Ephemeral signing public key @return pk if signing key is not in a valid manifest @par Thread Safety May be called concurrently */ PublicKey getMasterKey(PublicKey const& pk) const; /** Returns master key's current manifest sequence. @return sequence corresponding to Master public key if configured or std::nullopt otherwise */ std::optional getSequence(PublicKey const& pk) const; /** Returns domain claimed by a given public key @return domain corresponding to Master public key if present, otherwise std::nullopt */ std::optional getDomain(PublicKey const& pk) const; /** Returns manifest corresponding to a given public key @return manifest corresponding to Master public key if present, otherwise std::nullopt */ std::optional getManifest(PublicKey const& pk) const; /** Returns `true` if master key has been revoked in a manifest. @param pk Master public key @par Thread Safety May be called concurrently */ bool revoked(PublicKey const& pk) const; /** Add manifest to cache. @param m Manifest to add @return `ManifestDisposition::accepted` if successful, or `stale` or `invalid` otherwise @par Thread Safety May be called concurrently */ ManifestDisposition applyManifest(Manifest m); /** Populate manifest cache with manifests in database and config. @param dbCon Database connection with dbTable @param dbTable Database table @param configManifest Base64 encoded manifest for local node's validator keys @param configRevocation Base64 encoded validator key revocation from the config @par Thread Safety May be called concurrently */ bool load( DatabaseCon& dbCon, std::string const& dbTable, std::string const& configManifest, std::vector const& configRevocation); /** Populate manifest cache with manifests in database. @param dbCon Database connection with dbTable @param dbTable Database table @par Thread Safety May be called concurrently */ void load(DatabaseCon& dbCon, std::string const& dbTable); /** Save cached manifests to database. @param dbCon Database connection with `ValidatorManifests` table @param isTrusted Function that returns true if manifest is trusted @par Thread Safety May be called concurrently */ void save( DatabaseCon& dbCon, std::string const& dbTable, std::function const& isTrusted); /** Invokes the callback once for every populated manifest. @note Do not call ManifestCache member functions from within the callback. This can re-lock the mutex from the same thread, which is UB. @note Do not write ManifestCache member variables from within the callback. This can lead to data races. @param f Function called for each manifest @par Thread Safety May be called concurrently */ template void for_each_manifest(Function&& f) const { std::shared_lock lock{mutex_}; for (auto const& [_, manifest] : map_) { (void)_; f(manifest); } } /** Invokes the callback once for every populated manifest. @note Do not call ManifestCache member functions from within the callback. This can re-lock the mutex from the same thread, which is UB. @note Do not write ManifestCache member variables from within the callback. This can lead to data races. @param pf Pre-function called with the maximum number of times f will be called (useful for memory allocations) @param f Function called for each manifest @par Thread Safety May be called concurrently */ template void for_each_manifest(PreFun&& pf, EachFun&& f) const { std::shared_lock lock{mutex_}; pf(map_.size()); for (auto const& [_, manifest] : map_) { (void)_; f(manifest); } } }; } // namespace xrpl