diff --git a/doc/rippled-example.cfg b/doc/rippled-example.cfg index ec12cb30d6..d3a3293c29 100644 --- a/doc/rippled-example.cfg +++ b/doc/rippled-example.cfg @@ -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. # # # diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 7b1f431e4f..db55d161ed 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -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; diff --git a/src/ripple/app/misc/Manifest.h b/src/ripple/app/misc/Manifest.h index 4abc2665d0..766ef8bcc4 100644 --- a/src/ripple/app/misc/Manifest.h +++ b/src/ripple/app/misc/Manifest.h @@ -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 const& configRevocation); /** Populate manifest cache with manifests in database. diff --git a/src/ripple/app/misc/impl/Manifest.cpp b/src/ripple/app/misc/impl/Manifest.cpp index 171f5ea105..64ce9d8dbc 100644 --- a/src/ripple/app/misc/impl/Manifest.cpp +++ b/src/ripple/app/misc/impl/Manifest.cpp @@ -42,15 +42,26 @@ Manifest::make_Manifest (std::string s) SerialIter sit (s.data (), s.size ()); st.set (sit); auto const opt_pk = get(st, sfPublicKey); - auto const opt_spk = get(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::max ()) + { + auto const opt_spk = get(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 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()) diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index 44d239d33e..638a53403a 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -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" diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index 09cd98e793..899c09fa73 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -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::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 (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 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 const loadedManifests ( sort (getPopulatedManifests (loaded))); @@ -259,11 +281,12 @@ public: } { // load config manifest - std::string const badManifest = "bad manifest"; - ManifestCache loaded; + std::vector 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 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 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 const badSigRevocation = + { makeRevocation (sk, keyType, true /* invalidSig */) }; + BEAST_EXPECT(! loaded.load ( + dbCon, "ValidatorManifests", emptyManifest, badSigRevocation)); + BEAST_EXPECT(! loaded.revoked(pk)); + + std::vector 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