mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 02:25:52 +00:00
Add validator key revocations:
Allow manifest revoking validator keys to be stored in a separate [validator_key_revocation] config field, so the validator can run again with new keys and token.
This commit is contained in:
@@ -604,8 +604,19 @@
|
||||
#
|
||||
# This is an alternative to [validation_seed] that allows rippled to perform
|
||||
# validation without having to store the validator keys on the network
|
||||
# connected server. The field should contain a base64-encoded blob.
|
||||
# External tools are available for generating validator keys and tokens.
|
||||
# connected server. The field should contain a single token in the form of a
|
||||
# base64-encoded blob.
|
||||
# An external tool is available for generating validator keys and tokens.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [validator_key_revocation]
|
||||
#
|
||||
# If a validator's secret key has been compromised, a revocation must be
|
||||
# generated and added to this field. The revocation notifies peers that it is
|
||||
# no longer safe to trust the revoked key. The field should contain a single
|
||||
# revocation in the form of a base64-encoded blob.
|
||||
# An external tool is available for generating and revoking validator keys.
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
@@ -1135,7 +1135,8 @@ bool ApplicationImp::setup()
|
||||
}
|
||||
|
||||
if (!validatorManifests_->load (
|
||||
getWalletDB (), "ValidatorManifests", manifest))
|
||||
getWalletDB (), "ValidatorManifests", manifest,
|
||||
config().section (SECTION_VALIDATOR_KEY_REVOCATION).values ()))
|
||||
{
|
||||
JLOG(m_journal.fatal()) << "Invalid configured validator manifest.";
|
||||
return false;
|
||||
|
||||
@@ -52,18 +52,13 @@ namespace ripple {
|
||||
dynamically generates the signatureless form when it needs to verify
|
||||
the signature.
|
||||
|
||||
There are two stores of information within rippled related to manifests.
|
||||
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" is
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from rippled peers.
|
||||
|
||||
The other data store (which does not involve manifests per se) contains
|
||||
the set of active ephemeral validator keys. Keys are added to the set
|
||||
when a manifest is accepted, and removed when that manifest is obsoleted.
|
||||
|
||||
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,
|
||||
@@ -71,7 +66,9 @@ namespace ripple {
|
||||
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. Since no further manifests for this master key will be accepted
|
||||
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.
|
||||
*/
|
||||
@@ -239,13 +236,17 @@ public:
|
||||
@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::string const& configManifest,
|
||||
std::vector<std::string> const& configRevocation);
|
||||
|
||||
/** Populate manifest cache with manifests in database.
|
||||
|
||||
|
||||
@@ -42,15 +42,26 @@ Manifest::make_Manifest (std::string s)
|
||||
SerialIter sit (s.data (), s.size ());
|
||||
st.set (sit);
|
||||
auto const opt_pk = get<PublicKey>(st, sfPublicKey);
|
||||
auto const opt_spk = get<PublicKey>(st, sfSigningPubKey);
|
||||
auto const opt_seq = get (st, sfSequence);
|
||||
auto const opt_sig = get (st, sfSignature);
|
||||
auto const opt_msig = get (st, sfMasterSignature);
|
||||
if (!opt_pk || !opt_spk || !opt_seq || !opt_sig || !opt_msig)
|
||||
{
|
||||
if (!opt_pk || !opt_seq || !opt_msig)
|
||||
return boost::none;
|
||||
|
||||
// Signing key and signature are not required for
|
||||
// master key revocations
|
||||
if (*opt_seq != std::numeric_limits<std::uint32_t>::max ())
|
||||
{
|
||||
auto const opt_spk = get<PublicKey>(st, sfSigningPubKey);
|
||||
auto const opt_sig = get (st, sfSignature);
|
||||
if (!opt_spk || !opt_sig)
|
||||
{
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
return Manifest (std::move (s), *opt_pk, *opt_spk, *opt_seq);
|
||||
}
|
||||
return Manifest (std::move (s), *opt_pk, *opt_spk, *opt_seq);
|
||||
|
||||
return Manifest (std::move (s), *opt_pk, PublicKey(), *opt_seq);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
@@ -103,7 +114,10 @@ bool Manifest::verify () const
|
||||
STObject st (sfGeneric);
|
||||
SerialIter sit (serialized.data (), serialized.size ());
|
||||
st.set (sit);
|
||||
if (! ripple::verify (st, HashPrefix::manifest, signingKey))
|
||||
|
||||
// Signing key and signature are not required for
|
||||
// master key revocations
|
||||
if (! revoked () && ! ripple::verify (st, HashPrefix::manifest, signingKey))
|
||||
return false;
|
||||
|
||||
return ripple::verify (
|
||||
@@ -359,7 +373,8 @@ ManifestCache::load (
|
||||
bool
|
||||
ManifestCache::load (
|
||||
DatabaseCon& dbCon, std::string const& dbTable,
|
||||
std::string const& configManifest)
|
||||
std::string const& configManifest,
|
||||
std::vector<std::string> const& configRevocation)
|
||||
{
|
||||
load (dbCon, dbTable);
|
||||
|
||||
@@ -369,7 +384,7 @@ ManifestCache::load (
|
||||
beast::detail::base64_decode(configManifest));
|
||||
if (! mo)
|
||||
{
|
||||
JLOG (j_.error()) << "Malformed manifest in config";
|
||||
JLOG (j_.error()) << "Malformed validator_token in config";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -387,6 +402,30 @@ ManifestCache::load (
|
||||
}
|
||||
}
|
||||
|
||||
if (! configRevocation.empty())
|
||||
{
|
||||
std::string revocationStr;
|
||||
revocationStr.reserve (
|
||||
std::accumulate (configRevocation.cbegin(), configRevocation.cend(), std::size_t(0),
|
||||
[] (std::size_t init, std::string const& s)
|
||||
{
|
||||
return init + s.size();
|
||||
}));
|
||||
|
||||
for (auto const& line : configRevocation)
|
||||
revocationStr += beast::rfc2616::trim(line);
|
||||
|
||||
auto mo = Manifest::make_Manifest (
|
||||
beast::detail::base64_decode(revocationStr));
|
||||
|
||||
if (! mo || ! mo->revoked() ||
|
||||
applyManifest (std::move(*mo)) == ManifestDisposition::invalid)
|
||||
{
|
||||
JLOG (j_.error()) << "Invalid validator key revocation in config";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -404,7 +443,9 @@ void ManifestCache::save (
|
||||
"INSERT INTO " + dbTable + " (RawData) VALUES (:rawData);";
|
||||
for (auto const& v : map_)
|
||||
{
|
||||
if (! isTrusted (v.second.masterKey))
|
||||
// Save all revocation manifests,
|
||||
// but only save trusted non-revocation manifests.
|
||||
if (! v.second.revoked() && ! isTrusted (v.second.masterKey))
|
||||
{
|
||||
|
||||
JLOG(j_.info())
|
||||
|
||||
@@ -63,6 +63,7 @@ struct ConfigSection
|
||||
#define SECTION_VALIDATION_SEED "validation_seed"
|
||||
#define SECTION_WEBSOCKET_PING_FREQ "websocket_ping_frequency"
|
||||
#define SECTION_VALIDATOR_KEYS "validator_keys"
|
||||
#define SECTION_VALIDATOR_KEY_REVOCATION "validator_key_revocation"
|
||||
#define SECTION_VALIDATOR_LIST_KEYS "validator_list_keys"
|
||||
#define SECTION_VALIDATOR_LIST_SITES "validator_list_sites"
|
||||
#define SECTION_VALIDATORS "validators"
|
||||
|
||||
@@ -130,7 +130,7 @@ public:
|
||||
Manifest
|
||||
make_Manifest
|
||||
(SecretKey const& sk, KeyType type, SecretKey const& ssk, KeyType stype,
|
||||
int seq, bool broken = false)
|
||||
int seq, bool invalidSig = false)
|
||||
{
|
||||
auto const pk = derivePublicKey(type, sk);
|
||||
auto const spk = derivePublicKey(stype, ssk);
|
||||
@@ -143,15 +143,11 @@ public:
|
||||
sign(st, HashPrefix::manifest, stype, ssk);
|
||||
BEAST_EXPECT(verify(st, HashPrefix::manifest, spk));
|
||||
|
||||
sign(st, HashPrefix::manifest, type, sk, sfMasterSignature);
|
||||
BEAST_EXPECT(verify(
|
||||
sign(st, HashPrefix::manifest, type,
|
||||
invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||
BEAST_EXPECT(invalidSig ^ verify(
|
||||
st, HashPrefix::manifest, pk, sfMasterSignature));
|
||||
|
||||
if (broken)
|
||||
{
|
||||
set(st, sfSequence, seq + 1);
|
||||
}
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
@@ -162,6 +158,28 @@ public:
|
||||
return *Manifest::make_Manifest(std::move(m)); // Silence compiler warning.
|
||||
}
|
||||
|
||||
std::string
|
||||
makeRevocation
|
||||
(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
||||
{
|
||||
auto const pk = derivePublicKey(type, sk);
|
||||
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = std::numeric_limits<std::uint32_t>::max ();
|
||||
st[sfPublicKey] = pk;
|
||||
|
||||
sign(st, HashPrefix::manifest, type,
|
||||
invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||
BEAST_EXPECT(invalidSig ^ verify(
|
||||
st, HashPrefix::manifest, pk, sfMasterSignature));
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
|
||||
return beast::detail::base64_encode (std::string(
|
||||
static_cast<char const*> (s.data()), s.size()));
|
||||
}
|
||||
|
||||
Manifest
|
||||
clone (Manifest const& m)
|
||||
{
|
||||
@@ -174,8 +192,6 @@ public:
|
||||
|
||||
std::string const dbName("ManifestCacheTestDB");
|
||||
{
|
||||
// create a database, save the manifest to the db and reload and
|
||||
// check that the manifest caches are the same
|
||||
DatabaseCon::Setup setup;
|
||||
setup.dataDir = getDatabasePath ();
|
||||
DatabaseCon dbCon(setup, dbName, WalletDBInit, WalletDBCount);
|
||||
@@ -209,6 +225,7 @@ public:
|
||||
|
||||
{
|
||||
// save should not store untrusted master keys to db
|
||||
// except for revocations
|
||||
m.save (dbCon, "ValidatorManifests",
|
||||
[&unl](PublicKey const& pubKey)
|
||||
{
|
||||
@@ -218,9 +235,13 @@ public:
|
||||
ManifestCache loaded;
|
||||
|
||||
loaded.load (dbCon, "ValidatorManifests");
|
||||
for (auto const& man : inManifests)
|
||||
BEAST_EXPECT(
|
||||
loaded.getSigningKey (man->masterKey) == man->masterKey);
|
||||
|
||||
// check that all loaded manifests are revocations
|
||||
std::vector<Manifest const*> const loadedManifests (
|
||||
sort (getPopulatedManifests (loaded)));
|
||||
|
||||
for (auto const& man : loadedManifests)
|
||||
BEAST_EXPECT(man->revoked());
|
||||
}
|
||||
{
|
||||
// save should store all trusted master keys to db
|
||||
@@ -241,6 +262,7 @@ public:
|
||||
ManifestCache loaded;
|
||||
loaded.load (dbCon, "ValidatorManifests");
|
||||
|
||||
// check that the manifest caches are the same
|
||||
std::vector<Manifest const*> const loadedManifests (
|
||||
sort (getPopulatedManifests (loaded)));
|
||||
|
||||
@@ -259,11 +281,12 @@ public:
|
||||
}
|
||||
{
|
||||
// load config manifest
|
||||
std::string const badManifest = "bad manifest";
|
||||
|
||||
ManifestCache loaded;
|
||||
std::vector<std::string> const emptyRevocation;
|
||||
|
||||
std::string const badManifest = "bad manifest";
|
||||
BEAST_EXPECT(! loaded.load (
|
||||
dbCon, "ValidatorManifests", badManifest));
|
||||
dbCon, "ValidatorManifests", badManifest, emptyRevocation));
|
||||
|
||||
auto const sk = randomSecretKey();
|
||||
auto const pk = derivePublicKey(KeyType::ed25519, sk);
|
||||
@@ -273,7 +296,40 @@ public:
|
||||
makeManifestString (pk, sk, kp.first, kp.second, 0);
|
||||
|
||||
BEAST_EXPECT(loaded.load (
|
||||
dbCon, "ValidatorManifests", cfgManifest));
|
||||
dbCon, "ValidatorManifests", cfgManifest, emptyRevocation));
|
||||
}
|
||||
{
|
||||
// load config revocation
|
||||
ManifestCache loaded;
|
||||
std::string const emptyManifest;
|
||||
|
||||
std::vector<std::string> const badRevocation = { "bad revocation" };
|
||||
BEAST_EXPECT(! loaded.load (
|
||||
dbCon, "ValidatorManifests", emptyManifest, badRevocation));
|
||||
|
||||
auto const sk = randomSecretKey();
|
||||
auto const keyType = KeyType::ed25519;
|
||||
auto const pk = derivePublicKey(keyType, sk);
|
||||
auto const kp = randomKeyPair(KeyType::secp256k1);
|
||||
std::vector<std::string> const nonRevocation =
|
||||
{ makeManifestString (pk, sk, kp.first, kp.second, 0) };
|
||||
|
||||
BEAST_EXPECT(! loaded.load (
|
||||
dbCon, "ValidatorManifests", emptyManifest, nonRevocation));
|
||||
BEAST_EXPECT(! loaded.revoked(pk));
|
||||
|
||||
std::vector<std::string> const badSigRevocation =
|
||||
{ makeRevocation (sk, keyType, true /* invalidSig */) };
|
||||
BEAST_EXPECT(! loaded.load (
|
||||
dbCon, "ValidatorManifests", emptyManifest, badSigRevocation));
|
||||
BEAST_EXPECT(! loaded.revoked(pk));
|
||||
|
||||
std::vector<std::string> const cfgRevocation =
|
||||
{ makeRevocation (sk, keyType) };
|
||||
BEAST_EXPECT(loaded.load (
|
||||
dbCon, "ValidatorManifests", emptyManifest, cfgRevocation));
|
||||
|
||||
BEAST_EXPECT(loaded.revoked(pk));
|
||||
}
|
||||
}
|
||||
boost::filesystem::remove (getDatabasePath () /
|
||||
@@ -434,7 +490,7 @@ public:
|
||||
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 1);
|
||||
auto const s_b2 = make_Manifest (
|
||||
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 2,
|
||||
true); // broken
|
||||
true); // invalidSig
|
||||
auto const fake = s_b1.serialized + '\0';
|
||||
|
||||
// applyManifest should accept new manifests with
|
||||
|
||||
Reference in New Issue
Block a user