mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +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
|
# This is an alternative to [validation_seed] that allows rippled to perform
|
||||||
# validation without having to store the validator keys on the network
|
# validation without having to store the validator keys on the network
|
||||||
# connected server. The field should contain a base64-encoded blob.
|
# connected server. The field should contain a single token in the form of a
|
||||||
# External tools are available for generating validator keys and tokens.
|
# 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 (
|
if (!validatorManifests_->load (
|
||||||
getWalletDB (), "ValidatorManifests", manifest))
|
getWalletDB (), "ValidatorManifests", manifest,
|
||||||
|
config().section (SECTION_VALIDATOR_KEY_REVOCATION).values ()))
|
||||||
{
|
{
|
||||||
JLOG(m_journal.fatal()) << "Invalid configured validator manifest.";
|
JLOG(m_journal.fatal()) << "Invalid configured validator manifest.";
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -52,18 +52,13 @@ namespace ripple {
|
|||||||
dynamically generates the signatureless form when it needs to verify
|
dynamically generates the signatureless form when it needs to verify
|
||||||
the signature.
|
the signature.
|
||||||
|
|
||||||
There are two stores of information within rippled related to manifests.
|
|
||||||
An instance of ManifestCache stores, for each trusted validator, (a) its
|
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
|
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
|
seen for that validator, if any. On startup, the [validator_token] config
|
||||||
entry (which contains the manifest for this validator) is decoded and
|
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.
|
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,
|
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),
|
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,
|
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
|
old ephemeral key and stores the new one. If the master key itself gets
|
||||||
compromised, a manifest with sequence number 0xFFFFFFFF will supersede a
|
compromised, a manifest with sequence number 0xFFFFFFFF will supersede a
|
||||||
prior manifest and discard any existing ephemeral key without storing 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
|
(since no higher sequence number is possible), and no signing key is on
|
||||||
record, no validations will be accepted from the compromised validator.
|
record, no validations will be accepted from the compromised validator.
|
||||||
*/
|
*/
|
||||||
@@ -239,13 +236,17 @@ public:
|
|||||||
@param configManifest Base64 encoded manifest for local node's
|
@param configManifest Base64 encoded manifest for local node's
|
||||||
validator keys
|
validator keys
|
||||||
|
|
||||||
|
@param configRevocation Base64 encoded validator key revocation
|
||||||
|
from the config
|
||||||
|
|
||||||
@par Thread Safety
|
@par Thread Safety
|
||||||
|
|
||||||
May be called concurrently
|
May be called concurrently
|
||||||
*/
|
*/
|
||||||
bool load (
|
bool load (
|
||||||
DatabaseCon& dbCon, std::string const& dbTable,
|
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.
|
/** Populate manifest cache with manifests in database.
|
||||||
|
|
||||||
|
|||||||
@@ -42,15 +42,26 @@ Manifest::make_Manifest (std::string s)
|
|||||||
SerialIter sit (s.data (), s.size ());
|
SerialIter sit (s.data (), s.size ());
|
||||||
st.set (sit);
|
st.set (sit);
|
||||||
auto const opt_pk = get<PublicKey>(st, sfPublicKey);
|
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_seq = get (st, sfSequence);
|
||||||
auto const opt_sig = get (st, sfSignature);
|
|
||||||
auto const opt_msig = get (st, sfMasterSignature);
|
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;
|
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&)
|
catch (std::exception const&)
|
||||||
{
|
{
|
||||||
@@ -103,7 +114,10 @@ bool Manifest::verify () const
|
|||||||
STObject st (sfGeneric);
|
STObject st (sfGeneric);
|
||||||
SerialIter sit (serialized.data (), serialized.size ());
|
SerialIter sit (serialized.data (), serialized.size ());
|
||||||
st.set (sit);
|
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 false;
|
||||||
|
|
||||||
return ripple::verify (
|
return ripple::verify (
|
||||||
@@ -359,7 +373,8 @@ ManifestCache::load (
|
|||||||
bool
|
bool
|
||||||
ManifestCache::load (
|
ManifestCache::load (
|
||||||
DatabaseCon& dbCon, std::string const& dbTable,
|
DatabaseCon& dbCon, std::string const& dbTable,
|
||||||
std::string const& configManifest)
|
std::string const& configManifest,
|
||||||
|
std::vector<std::string> const& configRevocation)
|
||||||
{
|
{
|
||||||
load (dbCon, dbTable);
|
load (dbCon, dbTable);
|
||||||
|
|
||||||
@@ -369,7 +384,7 @@ ManifestCache::load (
|
|||||||
beast::detail::base64_decode(configManifest));
|
beast::detail::base64_decode(configManifest));
|
||||||
if (! mo)
|
if (! mo)
|
||||||
{
|
{
|
||||||
JLOG (j_.error()) << "Malformed manifest in config";
|
JLOG (j_.error()) << "Malformed validator_token in config";
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +443,9 @@ void ManifestCache::save (
|
|||||||
"INSERT INTO " + dbTable + " (RawData) VALUES (:rawData);";
|
"INSERT INTO " + dbTable + " (RawData) VALUES (:rawData);";
|
||||||
for (auto const& v : map_)
|
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())
|
JLOG(j_.info())
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ struct ConfigSection
|
|||||||
#define SECTION_VALIDATION_SEED "validation_seed"
|
#define SECTION_VALIDATION_SEED "validation_seed"
|
||||||
#define SECTION_WEBSOCKET_PING_FREQ "websocket_ping_frequency"
|
#define SECTION_WEBSOCKET_PING_FREQ "websocket_ping_frequency"
|
||||||
#define SECTION_VALIDATOR_KEYS "validator_keys"
|
#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_KEYS "validator_list_keys"
|
||||||
#define SECTION_VALIDATOR_LIST_SITES "validator_list_sites"
|
#define SECTION_VALIDATOR_LIST_SITES "validator_list_sites"
|
||||||
#define SECTION_VALIDATORS "validators"
|
#define SECTION_VALIDATORS "validators"
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public:
|
|||||||
Manifest
|
Manifest
|
||||||
make_Manifest
|
make_Manifest
|
||||||
(SecretKey const& sk, KeyType type, SecretKey const& ssk, KeyType stype,
|
(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 pk = derivePublicKey(type, sk);
|
||||||
auto const spk = derivePublicKey(stype, ssk);
|
auto const spk = derivePublicKey(stype, ssk);
|
||||||
@@ -143,15 +143,11 @@ public:
|
|||||||
sign(st, HashPrefix::manifest, stype, ssk);
|
sign(st, HashPrefix::manifest, stype, ssk);
|
||||||
BEAST_EXPECT(verify(st, HashPrefix::manifest, spk));
|
BEAST_EXPECT(verify(st, HashPrefix::manifest, spk));
|
||||||
|
|
||||||
sign(st, HashPrefix::manifest, type, sk, sfMasterSignature);
|
sign(st, HashPrefix::manifest, type,
|
||||||
BEAST_EXPECT(verify(
|
invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
||||||
|
BEAST_EXPECT(invalidSig ^ verify(
|
||||||
st, HashPrefix::manifest, pk, sfMasterSignature));
|
st, HashPrefix::manifest, pk, sfMasterSignature));
|
||||||
|
|
||||||
if (broken)
|
|
||||||
{
|
|
||||||
set(st, sfSequence, seq + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serializer s;
|
Serializer s;
|
||||||
st.add(s);
|
st.add(s);
|
||||||
|
|
||||||
@@ -162,6 +158,28 @@ public:
|
|||||||
return *Manifest::make_Manifest(std::move(m)); // Silence compiler warning.
|
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
|
Manifest
|
||||||
clone (Manifest const& m)
|
clone (Manifest const& m)
|
||||||
{
|
{
|
||||||
@@ -174,8 +192,6 @@ public:
|
|||||||
|
|
||||||
std::string const dbName("ManifestCacheTestDB");
|
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;
|
DatabaseCon::Setup setup;
|
||||||
setup.dataDir = getDatabasePath ();
|
setup.dataDir = getDatabasePath ();
|
||||||
DatabaseCon dbCon(setup, dbName, WalletDBInit, WalletDBCount);
|
DatabaseCon dbCon(setup, dbName, WalletDBInit, WalletDBCount);
|
||||||
@@ -209,6 +225,7 @@ public:
|
|||||||
|
|
||||||
{
|
{
|
||||||
// save should not store untrusted master keys to db
|
// save should not store untrusted master keys to db
|
||||||
|
// except for revocations
|
||||||
m.save (dbCon, "ValidatorManifests",
|
m.save (dbCon, "ValidatorManifests",
|
||||||
[&unl](PublicKey const& pubKey)
|
[&unl](PublicKey const& pubKey)
|
||||||
{
|
{
|
||||||
@@ -218,9 +235,13 @@ public:
|
|||||||
ManifestCache loaded;
|
ManifestCache loaded;
|
||||||
|
|
||||||
loaded.load (dbCon, "ValidatorManifests");
|
loaded.load (dbCon, "ValidatorManifests");
|
||||||
for (auto const& man : inManifests)
|
|
||||||
BEAST_EXPECT(
|
// check that all loaded manifests are revocations
|
||||||
loaded.getSigningKey (man->masterKey) == man->masterKey);
|
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
|
// save should store all trusted master keys to db
|
||||||
@@ -241,6 +262,7 @@ public:
|
|||||||
ManifestCache loaded;
|
ManifestCache loaded;
|
||||||
loaded.load (dbCon, "ValidatorManifests");
|
loaded.load (dbCon, "ValidatorManifests");
|
||||||
|
|
||||||
|
// check that the manifest caches are the same
|
||||||
std::vector<Manifest const*> const loadedManifests (
|
std::vector<Manifest const*> const loadedManifests (
|
||||||
sort (getPopulatedManifests (loaded)));
|
sort (getPopulatedManifests (loaded)));
|
||||||
|
|
||||||
@@ -259,11 +281,12 @@ public:
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
// load config manifest
|
// load config manifest
|
||||||
std::string const badManifest = "bad manifest";
|
|
||||||
|
|
||||||
ManifestCache loaded;
|
ManifestCache loaded;
|
||||||
|
std::vector<std::string> const emptyRevocation;
|
||||||
|
|
||||||
|
std::string const badManifest = "bad manifest";
|
||||||
BEAST_EXPECT(! loaded.load (
|
BEAST_EXPECT(! loaded.load (
|
||||||
dbCon, "ValidatorManifests", badManifest));
|
dbCon, "ValidatorManifests", badManifest, emptyRevocation));
|
||||||
|
|
||||||
auto const sk = randomSecretKey();
|
auto const sk = randomSecretKey();
|
||||||
auto const pk = derivePublicKey(KeyType::ed25519, sk);
|
auto const pk = derivePublicKey(KeyType::ed25519, sk);
|
||||||
@@ -273,7 +296,40 @@ public:
|
|||||||
makeManifestString (pk, sk, kp.first, kp.second, 0);
|
makeManifestString (pk, sk, kp.first, kp.second, 0);
|
||||||
|
|
||||||
BEAST_EXPECT(loaded.load (
|
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 () /
|
boost::filesystem::remove (getDatabasePath () /
|
||||||
@@ -434,7 +490,7 @@ public:
|
|||||||
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 1);
|
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 1);
|
||||||
auto const s_b2 = make_Manifest (
|
auto const s_b2 = make_Manifest (
|
||||||
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 2,
|
sk_b, KeyType::ed25519, kp_b.second, KeyType::secp256k1, 2,
|
||||||
true); // broken
|
true); // invalidSig
|
||||||
auto const fake = s_b1.serialized + '\0';
|
auto const fake = s_b1.serialized + '\0';
|
||||||
|
|
||||||
// applyManifest should accept new manifests with
|
// applyManifest should accept new manifests with
|
||||||
|
|||||||
Reference in New Issue
Block a user