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:
wilsonianb
2017-01-24 08:38:29 -08:00
committed by seelabs
parent a8cf5e0a5c
commit b4a16b165b
6 changed files with 149 additions and 38 deletions

View File

@@ -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.
#
#
#

View File

@@ -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;

View File

@@ -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.

View File

@@ -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())

View File

@@ -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"

View File

@@ -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