Files
rippled/src/ripple/app/misc/impl/ValidatorList.cpp
Nikolaos D. Bougalis d5f981f5fc Address issues identified by external review:
* RIPD-1617, RIPD-1619, RIPD-1621:
  Verify serialized public keys more strictly before
  using them.

* RIPD-1618:
    * Simplify the base58 decoder logic.
    * Reduce the complexity of the base58 encoder and
      eliminate a potential out-of-bounds memory access.
    * Improve type safety by using an `enum class` to
      enforce strict type checking for token types.

* RIPD-1616:
  Avoid calling `memcpy` with a null pointer even if the
  size is specified as zero, since it results in undefined
  behavior.

Acknowledgements:
Ripple thanks Guido Vranken for responsibly disclosing these
issues.

Bug Bounties and Responsible Disclosures:
We welcome reviews of the rippled code and urge researchers
to responsibly disclose any issues that they may find. For
more on Ripple's Bug Bounty program, please visit:
https://ripple.com/bug-bounty
2018-03-21 20:39:18 -07:00

601 lines
18 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2015 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/basics/Slice.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/json/json_reader.h>
#include <ripple/protocol/JsonFields.h>
#include <beast/core/detail/base64.hpp>
#include <boost/regex.hpp>
namespace ripple {
std::string
to_string(ListDisposition disposition)
{
switch (disposition)
{
case ListDisposition::accepted:
return "accepted";
case ListDisposition::same_sequence:
return "same_sequence";
case ListDisposition::unsupported_version:
return "unsupported_version";
case ListDisposition::untrusted:
return "untrusted";
case ListDisposition::stale:
return "stale";
case ListDisposition::invalid:
return "invalid";
}
return "unknown";
}
ValidatorList::ValidatorList (
ManifestCache& validatorManifests,
ManifestCache& publisherManifests,
TimeKeeper& timeKeeper,
beast::Journal j,
boost::optional<std::size_t> minimumQuorum)
: validatorManifests_ (validatorManifests)
, publisherManifests_ (publisherManifests)
, timeKeeper_ (timeKeeper)
, j_ (j)
, quorum_ (minimumQuorum.value_or(1)) // Genesis ledger quorum
, minimumQuorum_ (minimumQuorum)
{
}
ValidatorList::~ValidatorList()
{
}
bool
ValidatorList::load (
PublicKey const& localSigningKey,
std::vector<std::string> const& configKeys,
std::vector<std::string> const& publisherKeys)
{
static boost::regex const re (
"[[:space:]]*" // skip leading whitespace
"([[:alnum:]]+)" // node identity
"(?:" // begin optional comment block
"[[:space:]]+" // (skip all leading whitespace)
"(?:" // begin optional comment
"(.*[^[:space:]]+)" // the comment
"[[:space:]]*" // (skip all trailing whitespace)
")?" // end optional comment
")?" // end optional comment block
);
boost::unique_lock<boost::shared_mutex> read_lock{mutex_};
JLOG (j_.debug()) <<
"Loading configured trusted validator list publisher keys";
std::size_t count = 0;
for (auto key : publisherKeys)
{
JLOG (j_.trace()) <<
"Processing '" << key << "'";
auto const ret = strUnHex (key);
if (! ret.second || ! publicKeyType(makeSlice(ret.first)))
{
JLOG (j_.error()) <<
"Invalid validator list publisher key: " << key;
return false;
}
auto id = PublicKey(makeSlice(ret.first));
if (validatorManifests_.revoked (id))
{
JLOG (j_.warn()) <<
"Configured validator list publisher key is revoked: " << key;
continue;
}
if (publisherLists_.count(id))
{
JLOG (j_.warn()) <<
"Duplicate validator list publisher key: " << key;
continue;
}
publisherLists_[id].available = false;
++count;
}
JLOG (j_.debug()) <<
"Loaded " << count << " keys";
localPubKey_ = validatorManifests_.getMasterKey (localSigningKey);
// Treat local validator key as though it was listed in the config
if (localPubKey_.size())
keyListings_.insert ({ localPubKey_, 1 });
JLOG (j_.debug()) <<
"Loading configured validator keys";
count = 0;
PublicKey local;
for (auto const& n : configKeys)
{
JLOG (j_.trace()) <<
"Processing '" << n << "'";
boost::smatch match;
if (!boost::regex_match (n, match, re))
{
JLOG (j_.error()) <<
"Malformed entry: '" << n << "'";
return false;
}
auto const id = parseBase58<PublicKey>(
TokenType::NodePublic, match[1]);
if (!id)
{
JLOG (j_.error()) << "Invalid node identity: " << match[1];
return false;
}
// Skip local key which was already added
if (*id == localPubKey_ || *id == localSigningKey)
continue;
auto ret = keyListings_.insert ({*id, 1});
if (! ret.second)
{
JLOG (j_.warn()) << "Duplicate node identity: " << match[1];
continue;
}
auto it = publisherLists_.emplace(
std::piecewise_construct,
std::forward_as_tuple(local),
std::forward_as_tuple());
// Config listed keys never expire
if (it.second)
it.first->second.expiration = TimeKeeper::time_point::max();
it.first->second.list.emplace_back(std::move(*id));
it.first->second.available = true;
++count;
}
JLOG (j_.debug()) <<
"Loaded " << count << " entries";
return true;
}
ListDisposition
ValidatorList::applyList (
std::string const& manifest,
std::string const& blob,
std::string const& signature,
std::uint32_t version)
{
if (version != requiredListVersion)
return ListDisposition::unsupported_version;
boost::unique_lock<boost::shared_mutex> lock{mutex_};
Json::Value list;
PublicKey pubKey;
auto const result = verify (list, pubKey, manifest, blob, signature);
if (result != ListDisposition::accepted)
return result;
// Update publisher's list
Json::Value const& newList = list["validators"];
publisherLists_[pubKey].available = true;
publisherLists_[pubKey].sequence = list["sequence"].asUInt ();
publisherLists_[pubKey].expiration = TimeKeeper::time_point{
TimeKeeper::duration{list["expiration"].asUInt()}};
std::vector<PublicKey>& publisherList = publisherLists_[pubKey].list;
std::vector<PublicKey> oldList = publisherList;
publisherList.clear ();
publisherList.reserve (newList.size ());
std::vector<std::string> manifests;
for (auto const& val : newList)
{
if (val.isObject () &&
val.isMember ("validation_public_key") &&
val["validation_public_key"].isString ())
{
std::pair<Blob, bool> ret (strUnHex (
val["validation_public_key"].asString ()));
if (! ret.second || ! publicKeyType(makeSlice(ret.first)))
{
JLOG (j_.error()) <<
"Invalid node identity: " <<
val["validation_public_key"].asString ();
}
else
{
publisherList.push_back (
PublicKey(Slice{ ret.first.data (), ret.first.size() }));
}
if (val.isMember ("manifest") && val["manifest"].isString ())
manifests.push_back(val["manifest"].asString ());
}
}
// Update keyListings_ for added and removed keys
std::sort (
publisherList.begin (),
publisherList.end ());
auto iNew = publisherList.begin ();
auto iOld = oldList.begin ();
while (iNew != publisherList.end () ||
iOld != oldList.end ())
{
if (iOld == oldList.end () ||
(iNew != publisherList.end () &&
*iNew < *iOld))
{
// Increment list count for added keys
++keyListings_[*iNew];
++iNew;
}
else if (iNew == publisherList.end () ||
(iOld != oldList.end () && *iOld < *iNew))
{
// Decrement list count for removed keys
if (keyListings_[*iOld] <= 1)
keyListings_.erase (*iOld);
else
--keyListings_[*iOld];
++iOld;
}
else
{
++iNew;
++iOld;
}
}
if (publisherList.empty())
{
JLOG (j_.warn()) <<
"No validator keys included in valid list";
}
for (auto const& valManifest : manifests)
{
auto m = Manifest::make_Manifest (
beast::detail::base64_decode(valManifest));
if (! m || ! keyListings_.count (m->masterKey))
{
JLOG (j_.warn()) <<
"List for " << strHex(pubKey) <<
" contained untrusted validator manifest";
continue;
}
auto const result = validatorManifests_.applyManifest (std::move(*m));
if (result == ManifestDisposition::invalid)
{
JLOG (j_.warn()) <<
"List for " << strHex(pubKey) <<
" contained invalid validator manifest";
}
}
return ListDisposition::accepted;
}
ListDisposition
ValidatorList::verify (
Json::Value& list,
PublicKey& pubKey,
std::string const& manifest,
std::string const& blob,
std::string const& signature)
{
auto m = Manifest::make_Manifest (beast::detail::base64_decode(manifest));
if (! m || ! publisherLists_.count (m->masterKey))
return ListDisposition::untrusted;
pubKey = m->masterKey;
auto const revoked = m->revoked();
auto const result = publisherManifests_.applyManifest (
std::move(*m));
if (revoked && result == ManifestDisposition::accepted)
{
removePublisherList (pubKey);
publisherLists_.erase (pubKey);
}
if (revoked || result == ManifestDisposition::invalid)
return ListDisposition::untrusted;
auto const sig = strUnHex(signature);
auto const data = beast::detail::base64_decode (blob);
if (! sig.second ||
! ripple::verify (
publisherManifests_.getSigningKey(pubKey),
makeSlice(data),
makeSlice(sig.first)))
return ListDisposition::invalid;
Json::Reader r;
if (! r.parse (data, list))
return ListDisposition::invalid;
if (list.isMember("sequence") && list["sequence"].isInt() &&
list.isMember("expiration") && list["expiration"].isInt() &&
list.isMember("validators") && list["validators"].isArray())
{
auto const sequence = list["sequence"].asUInt();
auto const expiration = TimeKeeper::time_point{
TimeKeeper::duration{list["expiration"].asUInt()}};
if (sequence < publisherLists_[pubKey].sequence ||
expiration <= timeKeeper_.now())
return ListDisposition::stale;
else if (sequence == publisherLists_[pubKey].sequence)
return ListDisposition::same_sequence;
}
else
{
return ListDisposition::invalid;
}
return ListDisposition::accepted;
}
bool
ValidatorList::listed (
PublicKey const& identity) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
auto const pubKey = validatorManifests_.getMasterKey (identity);
return keyListings_.find (pubKey) != keyListings_.end ();
}
bool
ValidatorList::trusted (PublicKey const& identity) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
auto const pubKey = validatorManifests_.getMasterKey (identity);
return trustedKeys_.find (pubKey) != trustedKeys_.end();
}
boost::optional<PublicKey>
ValidatorList::getListedKey (
PublicKey const& identity) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
auto const pubKey = validatorManifests_.getMasterKey (identity);
if (keyListings_.find (pubKey) != keyListings_.end ())
return pubKey;
return boost::none;
}
boost::optional<PublicKey>
ValidatorList::getTrustedKey (PublicKey const& identity) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
auto const pubKey = validatorManifests_.getMasterKey (identity);
if (trustedKeys_.find (pubKey) != trustedKeys_.end())
return pubKey;
return boost::none;
}
bool
ValidatorList::trustedPublisher (PublicKey const& identity) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
return identity.size() && publisherLists_.count (identity);
}
PublicKey
ValidatorList::localPublicKey () const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
return localPubKey_;
}
bool
ValidatorList::removePublisherList (PublicKey const& publisherKey)
{
auto const iList = publisherLists_.find (publisherKey);
if (iList == publisherLists_.end ())
return false;
JLOG (j_.debug()) <<
"Removing validator list for revoked publisher " <<
toBase58(TokenType::NodePublic, publisherKey);
for (auto const& val : iList->second.list)
{
auto const& iVal = keyListings_.find (val);
if (iVal == keyListings_.end())
continue;
if (iVal->second <= 1)
keyListings_.erase (iVal);
else
--iVal->second;
}
iList->second.list.clear();
iList->second.available = false;
return true;
}
boost::optional<TimeKeeper::time_point>
ValidatorList::expires() const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
boost::optional<TimeKeeper::time_point> res{boost::none};
for (auto const& p : publisherLists_)
{
// Unfetched
if (p.second.expiration == TimeKeeper::time_point{})
return boost::none;
// Earliest
if (!res || p.second.expiration < *res)
res = p.second.expiration;
}
return res;
}
Json::Value
ValidatorList::getJson() const
{
Json::Value res(Json::objectValue);
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
res[jss::validation_quorum] = static_cast<Json::UInt>(quorum());
if (auto when = expires())
{
if (*when == TimeKeeper::time_point::max())
res[jss::validator_list_expires] = "never";
else
res[jss::validator_list_expires] = to_string(*when);
}
else
res[jss::validator_list_expires] = "unknown";
// Local static keys
PublicKey local;
Json::Value& jLocalStaticKeys =
(res[jss::local_static_keys] = Json::arrayValue);
auto it = publisherLists_.find(local);
if (it != publisherLists_.end())
{
for (auto const& key : it->second.list)
jLocalStaticKeys.append(
toBase58(TokenType::NodePublic, key));
}
// Publisher lists
Json::Value& jPublisherLists =
(res[jss::publisher_lists] = Json::arrayValue);
for (auto const& p : publisherLists_)
{
if(local == p.first)
continue;
Json::Value& curr = jPublisherLists.append(Json::objectValue);
curr[jss::pubkey_publisher] = strHex(p.first);
curr[jss::available] = p.second.available;
if(p.second.expiration != TimeKeeper::time_point{})
{
curr[jss::seq] = static_cast<Json::UInt>(p.second.sequence);
curr[jss::expiration] = to_string(p.second.expiration);
curr[jss::version] = requiredListVersion;
}
Json::Value& keys = (curr[jss::list] = Json::arrayValue);
for (auto const& key : p.second.list)
{
keys.append(toBase58(TokenType::NodePublic, key));
}
}
// Trusted validator keys
Json::Value& jValidatorKeys =
(res[jss::trusted_validator_keys] = Json::arrayValue);
for (auto const& k : trustedKeys_)
{
jValidatorKeys.append(toBase58(TokenType::NodePublic, k));
}
// signing keys
Json::Value& jSigningKeys = (res[jss::signing_keys] = Json::objectValue);
validatorManifests_.for_each_manifest(
[&jSigningKeys, this](Manifest const& manifest) {
auto it = keyListings_.find(manifest.masterKey);
if (it != keyListings_.end())
{
jSigningKeys[toBase58(
TokenType::NodePublic, manifest.masterKey)] =
toBase58(TokenType::NodePublic, manifest.signingKey);
}
});
return res;
}
void
ValidatorList::for_each_listed (
std::function<void(PublicKey const&, bool)> func) const
{
boost::shared_lock<boost::shared_mutex> read_lock{mutex_};
for (auto const& v : keyListings_)
func (v.first, trusted(v.first));
}
std::size_t
ValidatorList::calculateMinimumQuorum (
std::size_t nListedKeys, bool unlistedLocal)
{
// Only require 51% quorum for small number of validators to facilitate
// bootstrapping a network.
if (nListedKeys <= 6)
return nListedKeys/2 + 1;
// The number of listed validators is increased to preserve the safety
// guarantee for two unlisted validators using the same set of listed
// validators.
if (unlistedLocal)
++nListedKeys;
// Guarantee safety with up to 1/3 listed validators being malicious.
// This prioritizes safety (Byzantine fault tolerance) over liveness.
// It takes at least as many malicious nodes to split/fork the network as
// to stall the network.
// At 67%, the overlap of two quorums is 34%
// 67 + 67 - 100 = 34
// So under certain conditions, 34% of validators could vote for two
// different ledgers and split the network.
// Similarly 34% could prevent quorum from being met (by not voting) and
// stall the network.
// If/when the quorum is subsequently raised to/towards 80%, it becomes
// harder to split the network (more safe) and easier to stall it (less live).
return nListedKeys * 2/3 + 1;
}
} // ripple