Files
rippled/include/xrpl/server/Manifest.h

443 lines
12 KiB
C++

#pragma once
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <optional>
#include <shared_mutex>
#include <string>
namespace xrpl {
/*
Validator key manifests
-----------------------
Suppose the secret keys installed on an XRPL validator are compromised. Not
only do you have to generate and install new key pairs on each validator,
EVERY xrpld 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 xrpld 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 an xrpld 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<PublicKey> 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<PublicKey> 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<Blob>
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<Manifest>
deserializeManifest(Slice s, beast::Journal journal);
inline std::optional<Manifest>
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<std::is_same<T, char>::value || std::is_same<T, unsigned char>::value>>
std::optional<Manifest>
deserializeManifest(
std::vector<T> 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<ValidatorToken>
loadValidatorToken(
std::vector<std::string> 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<PublicKey, Manifest> map_;
/** Master public keys stored by current ephemeral public key. */
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
std::atomic<std::uint32_t> 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<PublicKey>
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<std::uint32_t>
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<std::string>
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<std::string>
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<std::string> 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<bool(PublicKey const&)> 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 <class Function>
void
for_each_manifest(Function&& f) const
{
std::shared_lock const 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 <class PreFun, class EachFun>
void
for_each_manifest(PreFun&& pf, EachFun&& f) const
{
std::shared_lock const lock{mutex_};
pf(map_.size());
for (auto const& [_, manifest] : map_)
{
(void)_;
f(manifest);
}
}
};
} // namespace xrpl