feat: vendor validator-keys with external signing and validator-list signing

This commit is contained in:
Denis Angell
2026-09-13 19:52:40 -04:00
parent 9403736199
commit ff5aa537e1
20 changed files with 5068 additions and 35 deletions

View File

@@ -121,6 +121,6 @@ CheckOptions:
readability-identifier-naming.PublicMemberSuffix: ""
readability-identifier-naming.GlobalFunctionIgnoredRegexp: "^(to_string|hash_append|tuple_hash)$"
HeaderFilterRegex: '^.*/(tests?|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
HeaderFilterRegex: '^.*/(tests?|tools|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
ExcludeHeaderFilterRegex: '^.*/protocol_autogen/.*\.(h|hpp)$'
WarningsAsErrors: "*"

View File

@@ -67,9 +67,10 @@ words:
- Britto
- Btrfs
- Buildx
- canonicality
- canonicalised
- canonicality
- cctools
- CGNAT
- changespq
- checkme
- choco
@@ -137,9 +138,11 @@ words:
- gpgkey
- Hinnant
- hotwallet
- hvssbqmgz
- hwaddress
- hwrap
- ifndef
- Iiwib
- inequation
- insuf
- insuff
@@ -152,6 +155,7 @@ words:
- jemalloc
- jlog
- jtnofill
- keyfile
- keylet
- keylets
- keyvadb
@@ -177,11 +181,10 @@ words:
- mathbunnyru
- mcmodel
- MEMORYSTATUSEX
- MPTAMM
- MPTDEX
- Merkle
- misprediction
- missingok
- MPTAMM
- mptbalance
- MPTDEX
- mptflags
@@ -256,8 +259,8 @@ words:
- replayer
- repodata
- repomd
- rerandomize
- rerandomization
- rerandomize
- rerandomized
- rerandomizes
- rerere
@@ -280,8 +283,8 @@ words:
- rustup
- sahyadri
- Satoshi
- scons
- Schnorr
- scons
- secp
- sendq
- seqit
@@ -311,6 +314,7 @@ words:
- stobject
- stpath
- stpathset
- STRINGIZE
- sttx
- stvar
- stvector
@@ -346,6 +350,7 @@ words:
- unfindable
- unflatten
- unfund
- ungated
- unimpair
- unroutable
- unscalable
@@ -385,5 +390,3 @@ words:
- xxhash
- xxhasher
- zstdio
- CGNAT
- ungated

View File

@@ -61,9 +61,6 @@ ${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' include/xrpl/protocol/messages.h
${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' BUILD.md
${SED_COMMAND} -i 's/ripple.pb.h/xrpl.pb.h/' BUILD.md
# Restore the name of the validator keys repository.
${SED_COMMAND} -i 's@xrpl/validator-keys-tool@ripple/validator-keys-tool@' cmake/XrplValidatorKeys.cmake
# Ensure the name of the binary and config remain 'rippled' for now.
${SED_COMMAND} -i -E 's/xrpld(-example)?\.cfg/rippled\1.cfg/g' cmake/XrplInstall.cmake
if grep -q '"xrpld"' cmake/XrplCore.cmake; then

View File

@@ -1,6 +1,6 @@
option(
validator_keys
"Enables building of validator-keys tool as a separate target (imported via FetchContent)"
"Enables building of the validator-keys tool as a separate target"
OFF
)
@@ -9,33 +9,17 @@ if(validator_keys)
# having pulled this in first.
include(GNUInstallDirs)
# Pinned to an exact commit, not a branch: the tool ships inside our
# packages, so the same xrpld version must always package the same
# validator-keys. Bump this deliberately.
set(validator_keys_commit "4c0fb75eec9601c711645998c904507e87e910ae")
message(STATUS "Using ValidatorKeys commit: ${validator_keys_commit}")
FetchContent_Declare(
validator_keys
GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git
GIT_TAG "${validator_keys_commit}"
)
FetchContent_MakeAvailable(validator_keys)
# The tool's own CMakeLists excludes the target from 'all' when it is built
# as a subproject. Undo that, so validator_keys=ON really does build it.
add_subdirectory(src/tools/validator-keys)
set_target_properties(
validator-keys
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
EXCLUDE_FROM_ALL OFF
EXCLUDE_FROM_DEFAULT_BUILD OFF
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
# We ship this binary, so like xrpld it must not keep the Nix store's ELF
# loader, or it cannot run on the target distro at all.
patch_nix_binary(validator-keys)
configure_file(
"${validator_keys_SOURCE_DIR}/LICENSE"
"${CMAKE_SOURCE_DIR}/src/tools/validator-keys/LICENSE"
"${CMAKE_BINARY_DIR}/validator-keys-LICENSE"
COPYONLY
)

View File

@@ -73,9 +73,10 @@ artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that
same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs
are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix.
`validator-keys` is fetched from an exact commit pinned in
`validator-keys` is built from the source in
[`src/tools/validator-keys`](../src/tools/validator-keys), enabled by
[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given
`xrpld` version always packages the same tool; bump that commit deliberately.
`xrpld` version always packages the tool from the same tree.
### Locally (mirrors CI)

View File

@@ -98,8 +98,8 @@ def check_binaries(build_dir: Path) -> None:
# No package goes out without the attribution.
notice = build_dir / "validator-keys-LICENSE"
assert notice.is_file(), (
f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it out of the "
"fetched validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON."
f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it from "
"src/tools/validator-keys, so reconfigure with -Dvalidator_keys=ON."
)
# Catches a binary still pointing at the Nix store's ELF loader, since

View File

@@ -0,0 +1,23 @@
# The validator-keys tool: validator and publisher key files, manifests,
# tokens, revocations and validator-list signing. Built only when the
# validator_keys option is ON (see cmake/XrplValidatorKeys.cmake).
add_executable(validator-keys)
target_sources(
validator-keys
PRIVATE
ListSigning.cpp
SigningKeys.cpp
ValidatorKeysTool.cpp
# Unit tests run with `validator-keys --unittest`.
test/ListSigning_test.cpp
test/SigningKeys_test.cpp
test/ValidatorKeysTool_test.cpp
)
target_include_directories(
validator-keys
PRIVATE $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src>
)
target_link_libraries(
validator-keys
PRIVATE Xrpl::boost Xrpl::opts Xrpl::libs xrpl.libxrpl
)

View File

@@ -0,0 +1,77 @@
The accompanying files under various copyrights.
Copyright (c) 2016 Ripple Labs Inc.
Permission to use, copy, modify, and 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.
The accompanying files incorporate work covered by the following copyright
and previous license notice:
Copyright (c) 2011 Arthur Britto, David Schwartz, Jed McCaleb,
Vinnie Falco, Bob Way, Eric Lombrozo, Nikolaos D. Bougalis, Howard Hinnant
Some code from Raw Material Software, Ltd., provided under the terms of the
ISC License. See the corresponding source files for more details.
Copyright (c) 2013 - Raw Material Software Ltd.
Please visit http://www.juce.com
Some code from ASIO examples:
// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Some code from Bitcoin:
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2011 The Bitcoin developers
// Distributed under the MIT/X11 software license, see the accompanying
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
Some code from Tom Wu:
This software is covered under the following copyright:
/*
* Copyright (c) 2003-2005 Tom Wu
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
* EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
* WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
*
* IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
* RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF
* THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* In addition, the following condition applies:
*
* All redistributions must retain an intact copy of this copyright notice
* and disclaimer.
*/
Address all questions regarding this license to:
Tom Wu
tjw@cs.Stanford.EDU

View File

@@ -0,0 +1,466 @@
#include <tools/validator-keys/ListSigning.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/protocol/jss.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <set>
namespace xrpl {
namespace {
// A version 2 list document holds at most this many blobs.
constexpr std::size_t kMaxBlobs = 5;
[[nodiscard]] std::string
readFile(boost::filesystem::path const& file)
{
std::ifstream in(file.c_str(), std::ios::in | std::ios::binary);
if (!in)
throw std::runtime_error("Failed to open file: " + file.string());
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
// The base64 lines of a config-style block, without its section header or
// comment lines.
[[nodiscard]] std::vector<std::string>
base64Lines(std::string const& text)
{
std::vector<std::string> lines;
std::vector<std::string> raw;
boost::split(raw, text, boost::is_any_of("\n"));
for (auto line : raw)
{
boost::trim(line);
if (line.empty() || line.front() == '#' || line.front() == '[')
continue;
lines.push_back(line);
}
return lines;
}
[[nodiscard]] std::optional<PublicKey>
parseHexKey(std::string const& hex)
{
auto const bytes = strUnHex(hex);
if (!bytes)
return std::nullopt;
auto const slice = makeSlice(*bytes);
if (!publicKeyType(slice))
return std::nullopt;
return PublicKey(slice);
}
[[nodiscard]] std::optional<Manifest>
parseManifest(std::string const& base64)
{
auto m = deserializeManifest(base64Decode(base64));
if (!m || !m->verify())
return std::nullopt;
return m;
}
[[nodiscard]] std::optional<std::uint32_t>
uintField(json::Value const& obj, char const* name)
{
if (!obj.isMember(name) || !obj[name].isIntegral() || obj[name].asInt() < 0)
return std::nullopt;
return obj[name].asUInt();
}
} // namespace
ValidatorToken
loadTokenFile(boost::filesystem::path const& tokenFile)
{
auto const token = loadValidatorToken(base64Lines(readFile(tokenFile)));
if (!token)
throw std::runtime_error("Not a validator token: " + tokenFile.string());
return *token;
}
Manifest
loadManifestFile(boost::filesystem::path const& manifestFile)
{
auto const lines = base64Lines(readFile(manifestFile));
std::string base64;
for (auto const& line : lines)
base64 += line;
auto m = parseManifest(base64);
if (!m)
throw std::runtime_error("Not a valid manifest: " + manifestFile.string());
return std::move(*m);
}
std::string
canonicalJson(std::string const& text)
{
// Reject malformed text before whitespace is moved.
{
json::Reader reader;
json::Value parsed;
if (!reader.parse(text, parsed) || !parsed.isObject())
throw std::runtime_error("Not a JSON object");
}
std::string out;
out.reserve(text.size());
bool inString = false;
bool escaped = false;
for (char const c : text)
{
if (inString)
{
out += c;
if (escaped)
escaped = false;
else if (c == '\\')
escaped = true;
else if (c == '"')
inString = false;
continue;
}
switch (c)
{
case ' ':
case '\t':
case '\n':
case '\r':
break;
case '"':
inString = true;
out += c;
break;
case ',':
out += ", ";
break;
case ':':
out += ": ";
break;
default:
out += c;
}
}
return out;
}
UnsignedList
parseUnsignedList(std::string const& text)
{
UnsignedList list;
list.canonical = canonicalJson(text);
json::Reader reader;
json::Value jv;
reader.parse(list.canonical, jv);
auto const sequence = uintField(jv, jss::sequence);
if (!sequence || *sequence == 0)
throw std::runtime_error("\"sequence\" must be a positive integer");
list.sequence = *sequence;
auto const expiration = uintField(jv, jss::expiration);
if (!expiration)
throw std::runtime_error("\"expiration\" must be an unsigned integer");
list.expiration = *expiration;
if (jv.isMember(jss::effective))
{
auto const effective = uintField(jv, jss::effective);
if (!effective)
throw std::runtime_error("\"effective\" must be an unsigned integer");
if (*effective >= list.expiration)
throw std::runtime_error("\"effective\" must be earlier than \"expiration\"");
list.effective = effective;
}
if (!jv.isMember(jss::validators) || !jv[jss::validators].isArray() ||
jv[jss::validators].size() == 0)
throw std::runtime_error("\"validators\" must be a non-empty array");
for (auto const& entry : jv[jss::validators])
{
if (!entry.isObject() || !entry.isMember(jss::validation_public_key) ||
!entry[jss::validation_public_key].isString())
throw std::runtime_error("every validator needs a \"validation_public_key\" string");
auto const key = parseHexKey(entry[jss::validation_public_key].asString());
if (!key)
throw std::runtime_error(
"\"validation_public_key\" is not a hex public key: " +
entry[jss::validation_public_key].asString());
if (entry.isMember(jss::manifest))
{
if (!entry[jss::manifest].isString())
throw std::runtime_error(
"\"manifest\" must be a base64 string for " +
entry[jss::validation_public_key].asString());
auto const m = parseManifest(entry[jss::manifest].asString());
if (!m)
throw std::runtime_error(
"\"manifest\" does not verify for " +
entry[jss::validation_public_key].asString());
if (m->masterKey != *key)
throw std::runtime_error(
"\"manifest\" belongs to another key than " +
entry[jss::validation_public_key].asString());
}
list.validators.push_back(*key);
}
return list;
}
UnsignedList
loadUnsignedList(boost::filesystem::path const& file)
{
return parseUnsignedList(readFile(file));
}
std::string
signList(UnsignedList const& list, PublicKey const& signingKey, SecretKey const& signingSecret)
{
return strHex(sign(signingKey, signingSecret, makeSlice(list.canonical)));
}
json::Value
makeSignedList(
std::string const& manifestBase64,
PublicKey const& masterKey,
UnsignedList const& list,
std::string const& signatureHex,
unsigned version,
std::optional<json::Value> const& append)
{
auto const blob = base64Encode(list.canonical);
if (version == 1)
{
if (append)
throw std::runtime_error("A version 1 list holds one blob; use version 2 to append");
json::Value jv(json::ValueType::Object);
jv[jss::blob] = blob;
jv[jss::manifest] = manifestBase64;
jv[jss::public_key] = strHex(masterKey);
jv[jss::signature] = signatureHex;
jv[jss::version] = 1;
return jv;
}
if (version != 2)
throw std::runtime_error("Unsupported list version");
json::Value jv(json::ValueType::Object);
if (append)
{
auto const& existing = *append;
if (!existing.isObject() || !existing.isMember(jss::version) ||
!existing[jss::version].isIntegral() || existing[jss::version].asUInt() != 2 ||
!existing.isMember(jss::blobs_v2) || !existing[jss::blobs_v2].isArray())
throw std::runtime_error("The list to append to is not a version 2 list");
if (!existing.isMember(jss::public_key) || !existing[jss::public_key].isString() ||
!boost::iequals(existing[jss::public_key].asString(), strHex(masterKey)))
throw std::runtime_error("The list to append to belongs to another master key");
if (existing[jss::blobs_v2].size() >= kMaxBlobs)
throw std::runtime_error(
"The list to append to already holds " + std::to_string(kMaxBlobs) + " blobs");
jv = existing;
}
else
{
jv[jss::blobs_v2] = json::Value(json::ValueType::Array);
}
jv[jss::manifest] = manifestBase64;
jv[jss::public_key] = strHex(masterKey);
jv[jss::version] = 2;
json::Value entry(json::ValueType::Object);
entry[jss::blob] = blob;
entry[jss::signature] = signatureHex;
jv[jss::blobs_v2].append(entry);
return jv;
}
std::uint32_t
rippleEpochNow()
{
using namespace std::chrono;
auto const since1970 = duration_cast<seconds>(system_clock::now().time_since_epoch());
return static_cast<std::uint32_t>((since1970 - kEpochOffset).count());
}
ListVerification
verifyList(
json::Value const& list,
std::optional<UnsignedList> const& expectedRoster,
std::optional<PublicKey> const& expectedKey,
std::uint32_t now)
{
ListVerification result;
result.report = json::Value(json::ValueType::Object);
auto fail = [&result](std::string const& error) {
result.ok = false;
result.errors.push_back(error);
};
if (!list.isObject())
{
fail("the list is not a JSON object");
return result;
}
auto const version = uintField(list, jss::version);
if (!version || (*version != 1 && *version != 2))
{
fail("\"version\" must be 1 or 2");
return result;
}
result.report[jss::version] = *version;
if (!list.isMember(jss::public_key) || !list[jss::public_key].isString() ||
!list.isMember(jss::manifest) || !list[jss::manifest].isString())
{
fail("\"public_key\" and \"manifest\" must be strings");
return result;
}
auto const manifest = parseManifest(list[jss::manifest].asString());
if (!manifest)
{
fail("\"manifest\" does not deserialize and verify");
return result;
}
result.report[jss::public_key] = strHex(manifest->masterKey);
result.report["manifest_sequence"] = manifest->sequence;
if (manifest->revoked() || !manifest->signingKey)
{
fail("the publisher's master key is revoked");
return result;
}
result.report["signing_key"] = strHex(*manifest->signingKey);
{
auto const declared = parseHexKey(list[jss::public_key].asString());
if (!declared || *declared != manifest->masterKey)
fail("\"public_key\" is not the manifest's master key");
}
if (expectedKey && *expectedKey != manifest->masterKey)
fail("the master key is not the expected key");
// The blobs of either version as (blob, signature) pairs.
std::vector<std::pair<std::string, std::string>> blobs;
if (*version == 1)
{
if (!list.isMember(jss::blob) || !list[jss::blob].isString() ||
!list.isMember(jss::signature) || !list[jss::signature].isString() ||
list.isMember(jss::blobs_v2))
{
fail("a version 1 list needs \"blob\" and \"signature\" and no \"blobs_v2\"");
return result;
}
blobs.emplace_back(list[jss::blob].asString(), list[jss::signature].asString());
}
else
{
if (!list.isMember(jss::blobs_v2) || !list[jss::blobs_v2].isArray() ||
list[jss::blobs_v2].size() == 0 || list[jss::blobs_v2].size() > kMaxBlobs ||
list.isMember(jss::blob) || list.isMember(jss::signature))
{
fail(
"a version 2 list needs 1 to " + std::to_string(kMaxBlobs) +
" \"blobs_v2\" entries and no top-level \"blob\"");
return result;
}
for (auto const& entry : list[jss::blobs_v2])
{
if (!entry.isObject() || !entry.isMember(jss::blob) || !entry[jss::blob].isString() ||
!entry.isMember(jss::signature) || !entry[jss::signature].isString())
{
fail("every \"blobs_v2\" entry needs \"blob\" and \"signature\"");
return result;
}
if (entry.isMember(jss::manifest))
{
if (!entry[jss::manifest].isString())
{
fail("a \"blobs_v2\" entry's \"manifest\" must be a string");
return result;
}
auto const m = parseManifest(entry[jss::manifest].asString());
if (!m || m->masterKey != manifest->masterKey)
fail("a \"blobs_v2\" entry's \"manifest\" is not this publisher's");
}
blobs.emplace_back(entry[jss::blob].asString(), entry[jss::signature].asString());
}
}
result.report["blobs"] = json::Value(json::ValueType::Array);
std::size_t index = 0;
for (auto const& [blob, signature] : blobs)
{
auto const where = "blob " + std::to_string(index++);
json::Value entry(json::ValueType::Object);
auto const sig = strUnHex(signature);
auto const data = base64Decode(blob);
if (!sig || !verify(*manifest->signingKey, makeSlice(data), makeSlice(*sig)))
fail(where + ": the signature does not verify under the signing key");
std::optional<UnsignedList> parsed;
try
{
parsed = parseUnsignedList(data);
}
catch (std::runtime_error const& e)
{
fail(where + ": " + e.what());
}
if (parsed)
{
entry[jss::sequence] = parsed->sequence;
if (parsed->effective)
entry[jss::effective] = *parsed->effective;
entry[jss::expiration] = parsed->expiration;
entry[jss::validators] = json::UInt(parsed->validators.size());
entry["expired"] = parsed->expiration <= now;
if (parsed->expiration <= now)
fail(where + ": expired");
if (expectedRoster)
{
std::set<PublicKey> const have(
parsed->validators.begin(), parsed->validators.end());
std::set<PublicKey> const want(
expectedRoster->validators.begin(), expectedRoster->validators.end());
if (have != want)
fail(where + ": the validators differ from the expected list");
}
}
result.report["blobs"].append(entry);
}
result.report["ok"] = result.ok;
result.report["errors"] = json::Value(json::ValueType::Array);
for (auto const& e : result.errors)
result.report["errors"].append(e);
return result;
}
} // namespace xrpl

View File

@@ -0,0 +1,149 @@
#pragma once
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/server/Manifest.h>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace boost {
namespace filesystem {
class path;
}
} // namespace boost
namespace xrpl {
/**
* Reads a token from a file holding the [validator_token] block as
* `create_token` prints it. The section header and `#` comment lines are
* ignored and the base64 lines are joined.
*
* @throws std::runtime_error if the file cannot be read or is not a token
*/
ValidatorToken
loadTokenFile(boost::filesystem::path const& tokenFile);
/**
* Reads a base64 manifest from a file. Comment lines and line breaks are
* ignored.
*
* @throws std::runtime_error if the file cannot be read or the manifest does
* not deserialize and verify
*/
Manifest
loadManifestFile(boost::filesystem::path const& manifestFile);
/**
* A validator list before it is signed: the canonical bytes the signing key
* signs, and the fields checked before signing.
*/
struct UnsignedList
{
// Canonical JSON text: compact, `, ` and `: ` separators, key order as
// written.
std::string canonical;
std::uint32_t sequence = 0;
std::optional<std::uint32_t> effective;
std::uint32_t expiration = 0;
// Master keys of the listed validators.
std::vector<PublicKey> validators;
};
/**
* Returns the canonical form of a JSON document: whitespace outside strings
* removed, one space after each `,` and `:`, key order preserved.
*
* @throws std::runtime_error if the text is not a JSON document
*/
std::string
canonicalJson(std::string const& text);
/**
* Parses an unsigned list and checks its fields: `sequence` and `expiration`
* are unsigned integers, `effective` if present is earlier than `expiration`,
* and every entry of `validators` has a `validation_public_key` that is a hex
* public key and, if present, a `manifest` for that key.
*
* @throws std::runtime_error naming the first failed check
*/
UnsignedList
parseUnsignedList(std::string const& text);
/**
* Reads and parses an unsigned list file.
*
* @throws std::runtime_error if the file cannot be read or fails a check
*/
UnsignedList
loadUnsignedList(boost::filesystem::path const& file);
/**
* Returns the hex signature of the list's canonical bytes.
*/
std::string
signList(UnsignedList const& list, PublicKey const& signingKey, SecretKey const& signingSecret);
/**
* Builds the document a publisher serves.
*
* Version 1 is `{blob, manifest, public_key, signature, version}`. Version 2
* carries the blob and signature inside `blobs_v2`; when @p append is given it
* must be a version 2 document for the same master key and the new blob is
* added to it.
*
* @throws std::runtime_error if @p append is not a version 2 document for
* @p masterKey or already holds the maximum number of blobs
*/
json::Value
makeSignedList(
std::string const& manifestBase64,
PublicKey const& masterKey,
UnsignedList const& list,
std::string const& signatureHex,
unsigned version,
std::optional<json::Value> const& append);
/**
* Seconds since the XRP Ledger epoch, now.
*/
std::uint32_t
rippleEpochNow();
/**
* The outcome of checking a published list.
*/
struct ListVerification
{
bool ok = true;
std::vector<std::string> errors;
// What was found: master key, signing key, manifest sequence, version,
// and one entry per blob.
json::Value report;
};
/**
* Checks a published list the way a server does before trusting it: the
* manifest verifies and names the `public_key`, every blob's signature
* verifies under the manifest's signing key, every blob parses, and none has
* expired at @p now. Every listed validator with a manifest must own it.
*
* @param list The document as served
* @param expectedRoster When set, every blob must list exactly these master
* keys
* @param expectedKey When set, the manifest's master key must be this key
* @param now Seconds since the XRP Ledger epoch
*/
ListVerification
verifyList(
json::Value const& list,
std::optional<UnsignedList> const& expectedRoster,
std::optional<PublicKey> const& expectedKey,
std::uint32_t now);
} // namespace xrpl

View File

@@ -0,0 +1,22 @@
# validator-keys
Command-line tool for the keys behind an XRP Ledger validator or a validator-list
publisher: the key file, the manifest that delegates from the master key to a
signing key, the `[validator_token]` for `xrpld.cfg`, key revocation, domain
attestation, and signing and verifying validator lists.
It is built as a separate target of this repository and ships in the `xrpld`
packages as `/usr/bin/validator-keys`.
## Build
Configure with `-Dvalidator_keys=ON` and build the `validator-keys` target:
```
cmake --build . --target validator-keys
./validator-keys --unittest
```
## Guide
[Validator Keys Tool Guide](doc/validator-keys-tool-guide.md)

View File

@@ -0,0 +1,524 @@
#include <tools/validator-keys/SigningKeys.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/regex.hpp>
#include <fstream>
namespace xrpl {
std::string
tokenToBase64(ValidatorToken const& token)
{
json::Value jv;
jv["validation_secret_key"] = strHex(token.validationSecret);
jv["manifest"] = token.manifest;
return xrpl::base64Encode(to_string(jv));
}
SigningKeys::SigningKeys(KeyType const& keyType)
: keyType_(keyType)
, keys_(generateKeyPair(keyType_, randomSeed()))
, tokenSequence_(0)
, revoked_(false)
{
}
SigningKeys::SigningKeys(
KeyType const& keyType,
SecretKey const& secretKey,
std::uint32_t tokenSequence,
bool revoked)
: keyType_(keyType)
, keys_({derivePublicKey(keyType_, secretKey), secretKey})
, tokenSequence_(tokenSequence)
, revoked_(revoked)
{
}
SigningKeys::SigningKeys(
KeyType const& keyType,
PublicKey const& publicKey,
std::uint32_t tokenSequence,
bool revoked)
: keyType_(keyType), keys_(publicKey), tokenSequence_(tokenSequence), revoked_(revoked)
{
}
SigningKeys
SigningKeys::make_SigningKeys(boost::filesystem::path const& keyFile)
{
std::ifstream ifsKeys(keyFile.c_str(), std::ios::in);
if (!ifsKeys)
throw std::runtime_error("Failed to open key file: " + keyFile.string());
json::Reader reader;
json::Value jKeys;
if (!reader.parse(ifsKeys, jKeys))
{
throw std::runtime_error("Unable to parse json key file: " + keyFile.string());
}
static std::array<std::string, 4> const requiredFields{
{"key_type", "secret_key", "token_sequence", "revoked"}};
for (auto field : requiredFields)
{
if (!jKeys.isMember(field))
{
throw std::runtime_error(
"Key file '" + keyFile.string() + "' is missing \"" + field + "\" field");
}
}
auto const invalidField = [&keyFile, &jKeys](std::string const& field) {
return std::runtime_error(
"Key file '" + keyFile.string() + "' contains invalid \"" + field +
"\" field: " + jKeys[field].toStyledString());
};
auto const keyType = keyTypeFromString(jKeys["key_type"].asString());
if (!keyType)
throw invalidField("key_type");
auto const secret =
parseBase58<SecretKey>(TokenType::NodePrivate, jKeys["secret_key"].asString());
auto const pubKey = [&]() -> std::optional<PublicKey> {
if (jKeys["secret_key"].asString() == "external")
{
if (!jKeys.isMember("public_key"))
{
throw std::runtime_error(
"Key file '" + keyFile.string() + "' is missing \"public_key\" field");
}
auto const pubKey =
parseBase58<PublicKey>(TokenType::NodePublic, jKeys["public_key"].asString());
if (!pubKey)
throw invalidField("public_key");
return pubKey;
}
if (!secret)
throw invalidField("secret_key");
return std::nullopt;
}();
std::uint32_t tokenSequence;
try
{
if (!jKeys["token_sequence"].isIntegral())
throw std::runtime_error("");
tokenSequence = jKeys["token_sequence"].asUInt();
}
catch (std::runtime_error&)
{
throw invalidField("token_sequence");
}
if (!jKeys["revoked"].isBool())
throw invalidField("revoked");
SigningKeys vk = [&]() {
if (secret)
return SigningKeys(*keyType, *secret, tokenSequence, jKeys["revoked"].asBool());
if (*keyType != *publicKeyType(*pubKey))
throw std::runtime_error(
"Key file '" + keyFile.string() +
"' has a \"key_type\" that does not match \"public_key\"");
return SigningKeys(*keyType, *pubKey, tokenSequence, jKeys["revoked"].asBool());
}();
if (jKeys.isMember("domain"))
{
if (!jKeys["domain"].isString())
throw invalidField("domain");
vk.domain(jKeys["domain"].asString());
}
if (jKeys.isMember("manifest"))
{
if (!jKeys["manifest"].isString())
throw invalidField("manifest");
auto ret = strUnHex(jKeys["manifest"].asString());
if (!ret || ret->size() == 0)
throw invalidField("manifest");
vk.manifest_.clear();
vk.manifest_.reserve(ret->size());
std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_));
}
if (jKeys.isMember("pending_token_secret"))
{
if (!jKeys["pending_token_secret"].isString())
throw invalidField("pending_token_secret");
vk.pendingTokenSecret_ = parseBase58<SecretKey>(
TokenType::NodePrivate, jKeys["pending_token_secret"].asString());
if (!vk.pendingTokenSecret_)
throw invalidField("pending_token_secret");
}
if (jKeys.isMember("pending_signing_key"))
{
if (!jKeys["pending_signing_key"].isString())
throw invalidField("pending_signing_key");
vk.pendingSigningKey_ =
parseBase58<PublicKey>(TokenType::NodePublic, jKeys["pending_signing_key"].asString());
if (!vk.pendingSigningKey_)
throw invalidField("pending_signing_key");
}
if (jKeys.isMember("pending_key_type"))
{
auto const pendingKeyType = keyTypeFromString(jKeys["pending_key_type"].asString());
if (!pendingKeyType)
throw invalidField("pending_key_type");
vk.pendingKeyType_ = pendingKeyType;
}
return vk;
}
void
SigningKeys::writeToFile(boost::filesystem::path const& keyFile) const
{
using namespace boost::filesystem;
json::Value jv;
jv["key_type"] = to_string(keyType_);
jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey);
jv["secret_key"] =
keys_.secretKey ? toBase58(TokenType::NodePrivate, *keys_.secretKey) : "external";
jv["token_sequence"] = json::UInt(tokenSequence_);
jv["revoked"] = revoked_;
if (!domain_.empty())
jv["domain"] = domain_;
if (!manifest_.empty())
jv["manifest"] = strHex(makeSlice(manifest_));
if (pendingTokenSecret_)
jv["pending_token_secret"] = toBase58(TokenType::NodePrivate, *pendingTokenSecret_);
if (pendingSigningKey_)
jv["pending_signing_key"] = toBase58(TokenType::NodePublic, *pendingSigningKey_);
if (pendingKeyType_)
jv["pending_key_type"] = to_string(*pendingKeyType_);
if (!keyFile.parent_path().empty())
{
boost::system::error_code ec;
if (!exists(keyFile.parent_path()))
boost::filesystem::create_directories(keyFile.parent_path(), ec);
if (ec || !is_directory(keyFile.parent_path()))
throw std::runtime_error("Cannot create directory: " + keyFile.parent_path().string());
}
std::ofstream o(keyFile.string(), std::ios_base::trunc);
if (o.fail())
throw std::runtime_error("Cannot open key file: " + keyFile.string());
o << jv.toStyledString();
}
void
SigningKeys::verifyManifest() const
{
STObject st(sfGeneric);
SerialIter sit(manifest_.data(), manifest_.size());
st.set(sit);
auto fail = []() { throw std::runtime_error("Manifest is not properly signed"); };
auto const tpk = get<PublicKey>(st, sfSigningPubKey);
if (revoked() && tpk)
fail();
if (!revoked() && (!tpk || !verify(st, HashPrefix::Manifest, *tpk)))
fail();
auto const pk = get<PublicKey>(st, sfPublicKey);
if (!pk || !verify(st, HashPrefix::Manifest, *pk, sfMasterSignature))
fail();
}
namespace {
[[nodiscard]] STObject
generatePartialManifest(
std::uint32_t sequence,
PublicKey const& masterPubKey,
PublicKey const& signingPubKey,
std::string const& domain)
{
STObject st(sfGeneric);
st[sfSequence] = sequence;
st[sfPublicKey] = masterPubKey;
st[sfSigningPubKey] = signingPubKey;
if (!domain.empty())
st[sfDomain] = makeSlice(domain);
return st;
}
[[nodiscard]] STObject
generatePartialRevocation(PublicKey const& masterPubKey)
{
STObject st(sfGeneric);
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
st[sfPublicKey] = masterPubKey;
return st;
}
// The bytes both the signing key and the master key sign.
[[nodiscard]] std::string
signingData(STObject const& st)
{
Serializer s;
s.add32(HashPrefix::Manifest);
st.addWithoutSigningFields(s);
return strHex(s.peekData());
}
} // namespace
std::optional<ValidatorToken>
SigningKeys::createValidatorToken(KeyType const& keyType)
{
if (revoked() || std::numeric_limits<std::uint32_t>::max() - 1 <= tokenSequence_)
return std::nullopt;
if (!keys_.secretKey)
throw std::runtime_error("This key file cannot be used to sign tokens.");
++tokenSequence_;
auto const tokenSecret = generateSecretKey(keyType, randomSeed());
auto const tokenPublic = derivePublicKey(keyType, tokenSecret);
STObject st = generatePartialManifest(tokenSequence_, keys_.publicKey, tokenPublic, domain_);
xrpl::sign(st, HashPrefix::Manifest, keyType, tokenSecret);
xrpl::sign(st, HashPrefix::Manifest, keyType_, *keys_.secretKey, sfMasterSignature);
setManifest(st);
return ValidatorToken{xrpl::base64Encode(manifest_.data(), manifest_.size()), tokenSecret};
}
std::optional<std::string>
SigningKeys::startValidatorToken(
KeyType const& keyType,
std::optional<PublicKey> const& externalSigningKey) const
{
if (revoked() || std::numeric_limits<std::uint32_t>::max() - 1 <= tokenSequence_)
return std::nullopt;
clearPending();
// The next manifest carries the next sequence, but the sequence is not
// consumed until the signature comes back.
if (externalSigningKey)
{
pendingSigningKey_ = externalSigningKey;
pendingKeyType_ = publicKeyType(*externalSigningKey);
return signingData(generatePartialManifest(
tokenSequence_ + 1, keys_.publicKey, *externalSigningKey, domain_));
}
auto const tokenSecret = generateSecretKey(keyType, randomSeed());
auto const tokenPublic = derivePublicKey(keyType, tokenSecret);
pendingTokenSecret_ = tokenSecret;
pendingKeyType_ = keyType;
return signingData(
generatePartialManifest(tokenSequence_ + 1, keys_.publicKey, tokenPublic, domain_));
}
std::optional<ValidatorToken>
SigningKeys::finishToken(Blob const& masterSig)
{
if (revoked())
return std::nullopt;
if (!pendingTokenSecret_ || !pendingKeyType_)
throw std::runtime_error("No pending token to finish");
++tokenSequence_;
auto const tokenSecret = *pendingTokenSecret_;
auto const tokenPublic = derivePublicKey(*pendingKeyType_, tokenSecret);
STObject st = generatePartialManifest(tokenSequence_, keys_.publicKey, tokenPublic, domain_);
xrpl::sign(st, HashPrefix::Manifest, *pendingKeyType_, tokenSecret);
st[sfMasterSignature] = makeSlice(masterSig);
setManifest(st);
return ValidatorToken{xrpl::base64Encode(manifest_.data(), manifest_.size()), tokenSecret};
}
std::optional<std::string>
SigningKeys::finishExternalToken(Blob const& masterSig, Blob const& signingSig)
{
if (revoked())
return std::nullopt;
if (!pendingSigningKey_)
throw std::runtime_error("No pending token with an external signing key to finish");
++tokenSequence_;
STObject st =
generatePartialManifest(tokenSequence_, keys_.publicKey, *pendingSigningKey_, domain_);
st[sfSignature] = makeSlice(signingSig);
st[sfMasterSignature] = makeSlice(masterSig);
setManifest(st);
return xrpl::base64Encode(manifest_.data(), manifest_.size());
}
std::string
SigningKeys::revoke()
{
if (!keys_.secretKey)
throw std::runtime_error("This key file cannot be used to sign tokens.");
revoked_ = true;
STObject st = generatePartialRevocation(keys_.publicKey);
xrpl::sign(st, HashPrefix::Manifest, keyType_, *keys_.secretKey, sfMasterSignature);
setManifest(st);
return xrpl::base64Encode(manifest_.data(), manifest_.size());
}
std::string
SigningKeys::startRevoke() const
{
clearPending();
return signingData(generatePartialRevocation(keys_.publicKey));
}
std::string
SigningKeys::finishRevoke(Blob const& masterSig)
{
revoked_ = true;
STObject st = generatePartialRevocation(keys_.publicKey);
st[sfMasterSignature] = makeSlice(masterSig);
setManifest(st);
return xrpl::base64Encode(manifest_.data(), manifest_.size());
}
void
SigningKeys::setManifest(STObject const& st)
{
Serializer s;
st.add(s);
manifest_.clear();
manifest_.reserve(s.size());
std::copy(s.begin(), s.end(), std::back_inserter(manifest_));
verifyManifest();
clearPending();
}
void
SigningKeys::clearPending() const
{
pendingTokenSecret_.reset();
pendingSigningKey_.reset();
pendingKeyType_.reset();
}
std::string
SigningKeys::sign(std::string const& data) const
{
if (!keys_.secretKey)
throw std::runtime_error("This key file cannot be used to sign.");
return strHex(xrpl::sign(keys_.publicKey, *keys_.secretKey, makeSlice(data)));
}
std::string
SigningKeys::signHex(std::string data) const
{
if (!keys_.secretKey)
throw std::runtime_error("This key file cannot be used to sign.");
boost::algorithm::trim(data);
auto const blob = strUnHex(data);
if (!blob)
throw std::runtime_error("Could not decode hex string: " + data);
return strHex(xrpl::sign(keys_.publicKey, *keys_.secretKey, makeSlice(*blob)));
}
void
SigningKeys::domain(std::string d)
{
if (!d.empty())
{
// A valid domain for a validator must be at least 4 characters
// long, should contain at least one . and should not be longer
// that 128 characters.
if (d.size() < 4 || d.size() > 128)
throw std::runtime_error("The domain must be between 4 and 128 characters long.");
// This regular expression should do a decent job of weeding out
// obviously wrong domain names but it isn't perfect. It does not
// really support IDNs. If this turns out to be an issue, a more
// thorough regex can be used or this check can just be removed.
static boost::regex const re(
"^" // Beginning of line
"(" // Hostname or domain name
"(?!-)" // - must not begin with '-'
"[a-zA-Z0-9-]{1,63}" // - only alphanumeric and '-'
"(?<!-)" // - must not end with '-'
"\\." // segment separator
")+" // 1 or more segments
"[A-Za-z]{2,63}" // TLD
"$" // End of line
,
boost::regex_constants::optimize);
if (!boost::regex_match(d, re))
throw std::runtime_error(
"The domain field must use the '[host.][subdomain.]domain.tld' "
"format");
}
domain_ = std::move(d);
}
} // namespace xrpl

View File

@@ -0,0 +1,318 @@
#pragma once
#include <xrpl/basics/Blob.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/server/Manifest.h>
#include <algorithm>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace boost {
namespace filesystem {
class path;
}
} // namespace boost
namespace xrpl {
/**
* Returns the token as the base64 JSON object written to [validator_token].
*/
std::string
tokenToBase64(ValidatorToken const& token);
/**
* The master key of a validator or a validator-list publisher, as stored in
* the key file, with the manifest, token and revocation operations that the
* master key signs.
*
* The secret key is optional. When it is absent the key file was created with
* `create_external` and every master signature comes from an external signer:
* the `start*` methods return the bytes to sign as hex and the `finish*`
* methods take the signature back.
*/
class SigningKeys
{
private:
struct Keys
{
PublicKey publicKey;
// Unset when the master key is held by an external signer.
std::optional<SecretKey> secretKey;
Keys() = delete;
Keys(std::pair<PublicKey, SecretKey> const& p) : publicKey(p.first), secretKey(p.second)
{
}
Keys(PublicKey const& pub) : publicKey(pub), secretKey(std::nullopt)
{
}
};
KeyType const keyType_;
Keys const keys_;
std::vector<std::uint8_t> manifest_;
std::uint32_t tokenSequence_;
bool revoked_;
std::string domain_;
// A token started with `startValidatorToken` and not yet finished. Only
// one of the two is set: the software signing key generated for the
// token, or the external signing key the token will delegate to.
mutable std::optional<SecretKey> pendingTokenSecret_;
mutable std::optional<PublicKey> pendingSigningKey_;
mutable std::optional<KeyType> pendingKeyType_;
public:
explicit SigningKeys(KeyType const& keyType);
SigningKeys(
KeyType const& keyType,
SecretKey const& secretKey,
std::uint32_t tokenSequence,
bool revoked = false);
/**
* Creates keys whose secret is held by an external signer.
*
* The key file is written with `"secret_key": "external"`.
*/
SigningKeys(
KeyType const& keyType,
PublicKey const& publicKey,
std::uint32_t tokenSequence = 0,
bool revoked = false);
/**
* Returns SigningKeys constructed from a JSON key file.
*
* @param keyFile Path to JSON key file
*
* @throws std::runtime_error if file content is invalid
*/
static SigningKeys
make_SigningKeys(boost::filesystem::path const& keyFile);
~SigningKeys() = default;
SigningKeys(SigningKeys const&) = default;
SigningKeys&
operator=(SigningKeys const&) = delete;
inline bool
operator==(SigningKeys const& rhs) const
{
return revoked_ == rhs.revoked_ && keyType_ == rhs.keyType_ &&
tokenSequence_ == rhs.tokenSequence_ && keys_.publicKey == rhs.keys_.publicKey &&
keys_.secretKey.has_value() == rhs.keys_.secretKey.has_value() &&
(!keys_.secretKey ||
std::equal(
keys_.secretKey->begin(), keys_.secretKey->end(), rhs.keys_.secretKey->begin()));
}
/**
* Writes the keys to a JSON key file.
*
* @param keyFile Path to file to write
*
* @note Overwrites an existing key file
*
* @throws std::runtime_error if unable to create the parent directory
*/
void
writeToFile(boost::filesystem::path const& keyFile) const;
/**
* Returns a validator token for the next sequence.
*
* @param keyType Key type of the token's signing key
*
* @return The token, or nullopt if the keys are revoked or the sequence is
* exhausted
*
* @throws std::runtime_error if the master key is external
*/
std::optional<ValidatorToken>
createValidatorToken(KeyType const& keyType = KeyType::Secp256k1);
/**
* Starts a token whose master signature comes from an external signer.
*
* When @p externalSigningKey is set, the token delegates to that key and
* its signature must also come from the external signer, so the returned
* bytes are signed twice: once by the signing key and once by the master
* key. Otherwise a software signing key is generated and kept pending in
* the key file until `finishToken`.
*
* @param keyType Key type of a generated signing key; ignored when
* @p externalSigningKey is set
* @param externalSigningKey Signing key held by the external signer
*
* @return The hex bytes to sign, or nullopt if the keys are revoked or
* the sequence is exhausted
*/
std::optional<std::string>
startValidatorToken(
KeyType const& keyType = KeyType::Secp256k1,
std::optional<PublicKey> const& externalSigningKey = std::nullopt) const;
/**
* Finishes a token started with a generated signing key.
*
* @param masterSig Master signature over the bytes `startValidatorToken`
* returned
*
* @return The token, or nullopt if the keys are revoked
*
* @throws std::runtime_error if no such token is pending or the
* signature does not verify
*/
std::optional<ValidatorToken>
finishToken(Blob const& masterSig);
/**
* Finishes a token started with an external signing key.
*
* @param masterSig Master signature over the bytes `startValidatorToken`
* returned
* @param signingSig Signing-key signature over the same bytes
*
* @return The base64 manifest, or nullopt if the keys are revoked
*
* @throws std::runtime_error if no such token is pending or a signature
* does not verify
*/
std::optional<std::string>
finishExternalToken(Blob const& masterSig, Blob const& signingSig);
/**
* Revokes the keys.
*
* @return The base64 revocation manifest
*
* @throws std::runtime_error if the master key is external
*/
std::string
revoke();
/**
* Starts a revocation whose master signature comes from an external
* signer.
*
* @return The hex bytes to sign
*/
std::string
startRevoke() const;
/**
* Finishes a revocation.
*
* @param masterSig Master signature over the bytes `startRevoke` returned
*
* @return The base64 revocation manifest
*
* @throws std::runtime_error if the signature does not verify
*/
std::string
finishRevoke(Blob const& masterSig);
/**
* Signs a string with the master key.
*
* @param data String to sign
*
* @return The hex signature
*
* @throws std::runtime_error if the master key is external
*/
std::string
sign(std::string const& data) const;
/**
* Signs hex-encoded bytes with the master key.
*
* @param data Hex string; decoded to raw bytes before signing
*
* @return The hex signature
*
* @throws std::runtime_error if the master key is external
*/
std::string
signHex(std::string data) const;
/**
* Returns the public key.
*/
PublicKey const&
publicKey() const
{
return keys_.publicKey;
}
/**
* Returns true if the keys are revoked.
*/
bool
revoked() const
{
return revoked_;
}
/**
* Returns the domain associated with this key, if any.
*/
std::string const&
domain() const
{
return domain_;
}
/**
* Sets the domain associated with this key.
*/
void
domain(std::string d);
/**
* Checks the stored manifest.
*
* @throws std::runtime_error if the manifest is malformed or not signed
* correctly
*/
void
verifyManifest() const;
/**
* Returns the last manifest generated, if available.
*/
std::vector<std::uint8_t>
manifest() const
{
if (!manifest_.empty())
verifyManifest();
return manifest_;
}
/**
* Returns the sequence number of the last manifest generated.
*/
std::uint32_t
sequence() const
{
return tokenSequence_;
}
private:
void
setManifest(STObject const& st);
void
clearPending() const;
};
} // namespace xrpl

View File

@@ -0,0 +1,912 @@
#include <tools/validator-keys/ValidatorKeysTool.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/beast/core/SemanticVersion.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/unit_test/global_suites.h>
#include <xrpl/beast/unit_test/reporter.h>
#include <xrpl/json/json_reader.h>
#include <boost/filesystem.hpp>
#include <boost/format.hpp>
#include <boost/preprocessor/stringize.hpp>
#include <boost/program_options.hpp>
#include <tools/validator-keys/ListSigning.h>
#include <tools/validator-keys/SigningKeys.h>
#include <fstream>
#include <iostream>
//------------------------------------------------------------------------------
// The build version number. You must edit this for each release
// and follow the format described at http://semver.org/
//--------------------------------------------------------------------------
char const* const versionString =
"0.4.0"
#if defined(DEBUG) || defined(SANITIZER)
"+"
#ifdef DEBUG
"DEBUG"
#ifdef SANITIZER
"."
#endif
#endif
#ifdef SANITIZER
BOOST_PP_STRINGIZE(SANITIZER)
#endif
#endif
//--------------------------------------------------------------------------
;
static int
runUnitTests()
{
using namespace beast::unit_test;
// Report on stderr: the tool tests capture stdout to check command output,
// and a failure reported into that capture would never be seen.
reporter r(std::cerr);
bool const anyFailed = r.runEach(globalSuites());
if (anyFailed)
return EXIT_FAILURE; // LCOV_EXCL_LINE
return EXIT_SUCCESS;
}
namespace {
/**
* Parses a public key given as base58, hex or base64.
*
* @throws std::runtime_error if none of the encodings yields a public key
*/
xrpl::PublicKey
parsePublicKey(std::string const& data)
{
using namespace xrpl;
if (auto const unBase58 = parseBase58<PublicKey>(TokenType::NodePublic, data))
return *unBase58;
if (auto const unHex = strUnHex(data))
{
auto const slice = makeSlice(*unHex);
if (publicKeyType(slice))
return PublicKey(slice);
}
{
auto const unBase64 = base64Decode(data);
auto const slice = makeSlice(unBase64);
if (publicKeyType(slice))
return PublicKey(slice);
}
throw std::runtime_error("Unable to parse public key: " + data);
}
/**
* Decodes a signature given as hex or base64. There is no structural way to
* check it other than trying to use it, so if the decoding succeeds, proceed.
*/
xrpl::Blob
decodeSignature(std::string const& data)
{
using namespace xrpl;
if (auto const unHex = strUnHex(data))
{
return *unHex;
}
// base64Decode decodes as far as it can and returns partial data for
// invalid input, so re-encode the result and require a round trip.
if (auto const unBase64 = base64Decode(data); base64Encode(unBase64) == data)
{
return Blob(unBase64.begin(), unBase64.end());
}
throw std::runtime_error("Invalid master signature");
}
// Writes a config block in 72-character lines, to the file when one is given
// (readable by the owner only, since a token holds a secret) or to stdout.
void
emitBlock(
std::string const& section,
std::string const& publicKey,
std::string const& body,
std::optional<boost::filesystem::path> const& outFile)
{
std::ostringstream block;
block << "# validator public key: " << publicKey << "\n\n";
block << "[" << section << "]\n";
auto const len = 72;
for (std::size_t i = 0; i < body.size(); i += len)
block << body.substr(i, len) << "\n";
if (!outFile)
{
std::cout << "Update xrpld.cfg file with these values and restart xrpld:\n\n";
std::cout << block.str() << std::endl;
return;
}
using namespace boost::filesystem;
std::ofstream o(outFile->string(), std::ios_base::trunc);
if (o.fail())
throw std::runtime_error("Cannot open output file: " + outFile->string());
o << block.str();
o.close();
permissions(*outFile, owner_read | owner_write);
std::cout << "[" << section << "] written to " << outFile->string() << "\n\n";
}
void
emitJson(json::Value const& jv, std::optional<boost::filesystem::path> const& outFile)
{
if (!outFile)
{
std::cout << jv.toStyledString() << std::endl;
return;
}
std::ofstream o(outFile->string(), std::ios_base::trunc);
if (o.fail())
throw std::runtime_error("Cannot open output file: " + outFile->string());
o << jv.toStyledString();
std::cout << "Written to " << outFile->string() << "\n";
}
json::Value
readJsonFile(boost::filesystem::path const& file)
{
std::ifstream in(file.c_str(), std::ios::in);
if (!in)
throw std::runtime_error("Failed to open file: " + file.string());
json::Reader reader;
json::Value jv;
if (!reader.parse(in, jv))
throw std::runtime_error("Not a JSON document: " + file.string());
return jv;
}
} // namespace
void
createKeyFile(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (exists(keyFile))
throw std::runtime_error("Refusing to overwrite existing key file: " + keyFile.string());
SigningKeys const keys(KeyType::Ed25519);
keys.writeToFile(keyFile);
std::cout << "Validator keys stored in " << keyFile.string()
<< "\n\nThis file should be stored securely and not shared.\n\n";
}
void
createExternal(std::string const& data, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (exists(keyFile))
throw std::runtime_error("Refusing to overwrite existing key file: " + keyFile.string());
auto const publicKey = parsePublicKey(data);
SigningKeys const keys(*publicKeyType(publicKey), publicKey);
keys.writeToFile(keyFile);
std::cout << "Validator keys stored in " << keyFile.string()
<< "\n\nThis file should be stored securely and not shared.\n\n";
}
void
createToken(ToolOptions const& options)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(options.keyFile);
if (keys.revoked())
throw std::runtime_error("Validator keys have been revoked.");
auto const token = keys.createValidatorToken(options.tokenKeyType);
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Update key file with new token sequence
keys.writeToFile(options.keyFile);
emitBlock(
"validator_token",
toBase58(TokenType::NodePublic, keys.publicKey()),
tokenToBase64(*token),
options.outFile);
}
void
startToken(ToolOptions const& options)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(options.keyFile);
if (keys.revoked())
throw std::runtime_error("Validator keys have been revoked.");
auto const token = keys.startValidatorToken(options.tokenKeyType, options.signingKey);
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Update key file with the pending token
keys.writeToFile(options.keyFile);
std::cout << *token << std::endl;
std::cout << std::endl;
}
void
finishToken(std::vector<std::string> const& signatures, ToolOptions const& options)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(options.keyFile);
if (keys.revoked())
throw std::runtime_error("Validator keys have been revoked.");
auto const masterSig = decodeSignature(signatures.at(0));
if (signatures.size() == 2)
{
auto const signingSig = decodeSignature(signatures.at(1));
auto const manifest = keys.finishExternalToken(masterSig, signingSig);
if (!manifest)
throw std::runtime_error("Validator keys have been revoked.");
keys.writeToFile(options.keyFile);
emitBlock(
"validator_manifest",
toBase58(TokenType::NodePublic, keys.publicKey()),
*manifest,
options.outFile);
return;
}
auto const token = keys.finishToken(masterSig);
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Update key file with new token sequence
keys.writeToFile(options.keyFile);
emitBlock(
"validator_token",
toBase58(TokenType::NodePublic, keys.publicKey()),
tokenToBase64(*token),
options.outFile);
}
void
createRevocation(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have already been revoked!\n\n";
else
std::cout << "WARNING: This will revoke your validator keys!\n\n";
auto const revocation = keys.revoke();
// Update key file with new token sequence
keys.writeToFile(keyFile);
emitBlock(
"validator_key_revocation",
toBase58(TokenType::NodePublic, keys.publicKey()),
revocation,
std::nullopt);
}
void
startRevocation(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
std::cerr << "WARNING: Validator keys have already been revoked!\n\n";
else
std::cerr << "WARNING: This will revoke your validator keys!\n\n";
auto const revocation = keys.startRevoke();
// Update key file with new token sequence
keys.writeToFile(keyFile);
std::cout << revocation << std::endl;
std::cout << std::endl;
}
void
finishRevocation(std::string const& data, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have already been revoked!\n\n";
else
std::cout << "WARNING: This will revoke your validator keys!\n\n";
auto const masterSig = decodeSignature(data);
auto const revocation = keys.finishRevoke(masterSig);
// Update key file with new token sequence
keys.writeToFile(keyFile);
emitBlock(
"validator_key_revocation",
toBase58(TokenType::NodePublic, keys.publicKey()),
revocation,
std::nullopt);
}
void
attestDomain(xrpl::SigningKeys const& keys)
{
using namespace xrpl;
if (keys.domain().empty())
{
std::cout << "No attestation is necessary if no domain is specified!\n";
std::cout << "If you have an attestation in your xrpl-ledger.toml\n";
std::cout << "you should remove it at this time.\n";
return;
}
std::cout << "The domain attestation for validator "
<< toBase58(TokenType::NodePublic, keys.publicKey()) << " is:\n\n";
std::cout << "attestation=\""
<< keys.sign(
"[domain-attestation-blob:" + keys.domain() + ":" +
toBase58(TokenType::NodePublic, keys.publicKey()) + "]")
<< "\"\n\n";
std::cout << "You should include it in your xrp-ledger.toml file in the\n";
std::cout << "section for this validator.\n";
}
void
attestDomain(boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
throw std::runtime_error("Operation error: The specified master key has been revoked!");
attestDomain(keys);
}
void
setDomain(std::string const& domain, ToolOptions const& options)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(options.keyFile);
if (keys.revoked())
throw std::runtime_error("Operation error: The specified master key has been revoked!");
if (domain == keys.domain())
{
if (domain.empty())
std::cout << "The domain name was already cleared!\n";
else
std::cout << "The domain name was already set.\n";
return;
}
// Set the domain and generate a new token
keys.domain(domain);
auto const token = keys.createValidatorToken(options.tokenKeyType);
if (!token)
throw std::runtime_error(
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.");
// Flush to disk
keys.writeToFile(options.keyFile);
if (domain.empty())
std::cout << "The domain name has been cleared.\n";
else
std::cout << "The domain name has been set to: " << domain << "\n\n";
attestDomain(keys);
std::cout << "\n";
std::cout << "You also need to update the xrpld.cfg file to add a new\n";
std::cout << "validator token and restart xrpld:\n\n";
emitBlock(
"validator_token",
toBase58(TokenType::NodePublic, keys.publicKey()),
tokenToBase64(*token),
options.outFile);
}
void
signData(std::string const& data, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (data.empty())
throw std::runtime_error("Syntax error: Must specify data string to sign");
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have been revoked!\n\n";
std::cout << keys.sign(data) << std::endl;
std::cout << std::endl;
}
void
signHexData(std::string const& data, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
if (data.empty())
throw std::runtime_error("Syntax error: Must specify data string to sign");
auto keys = SigningKeys::make_SigningKeys(keyFile);
if (keys.revoked())
std::cout << "WARNING: Validator keys have been revoked!\n\n";
std::cout << keys.signHex(data) << std::endl;
std::cout << std::endl;
}
void
generateManifest(std::string const& type, boost::filesystem::path const& keyFile)
{
using namespace xrpl;
auto keys = SigningKeys::make_SigningKeys(keyFile);
auto const m = keys.manifest();
if (m.empty())
{
std::cout << "The last manifest generated is unavailable. You can\n";
std::cout << "generate a new one.\n\n";
return;
}
if (type == "base64")
{
std::cout << "Manifest #" << keys.sequence() << " (Base64):\n";
std::cout << base64Encode(m.data(), m.size()) << "\n\n";
return;
}
if (type == "hex")
{
std::cout << "Manifest #" << keys.sequence() << " (Hex):\n";
std::cout << strHex(makeSlice(m)) << "\n\n";
return;
}
std::cout << "Unknown encoding '" << type << "'\n";
}
void
signListFile(boost::filesystem::path const& unsignedList, ToolOptions const& options)
{
using namespace xrpl;
if (!options.tokenFile)
throw std::runtime_error("sign_list needs --token-file");
auto const token = loadTokenFile(*options.tokenFile);
auto const manifest = deserializeManifest(base64Decode(token.manifest));
if (!manifest || !manifest->verify() || manifest->revoked() || !manifest->signingKey)
throw std::runtime_error("The token's manifest is not valid");
auto const keyType = publicKeyType(*manifest->signingKey);
if (!keyType || derivePublicKey(*keyType, token.validationSecret) != *manifest->signingKey)
throw std::runtime_error("The token's secret does not match its manifest");
auto const list = loadUnsignedList(unsignedList);
auto const signature = signList(list, *manifest->signingKey, token.validationSecret);
std::optional<json::Value> append;
if (options.appendFile)
append = readJsonFile(*options.appendFile);
emitJson(
makeSignedList(
token.manifest, manifest->masterKey, list, signature, options.listVersion, append),
options.outFile);
}
void
startSignList(boost::filesystem::path const& unsignedList, ToolOptions const& options)
{
using namespace xrpl;
if (!options.manifestFile)
throw std::runtime_error("start_sign_list needs --manifest-file");
// The manifest names the signing key; the bytes to sign are the list.
auto const manifest = loadManifestFile(*options.manifestFile);
if (manifest.revoked() || !manifest.signingKey)
throw std::runtime_error("The manifest is revoked");
auto const list = loadUnsignedList(unsignedList);
std::cout << strHex(makeSlice(list.canonical)) << std::endl;
}
void
finishSignList(
std::string const& signature,
boost::filesystem::path const& unsignedList,
ToolOptions const& options)
{
using namespace xrpl;
if (!options.manifestFile)
throw std::runtime_error("finish_sign_list needs --manifest-file");
auto const manifest = loadManifestFile(*options.manifestFile);
if (manifest.revoked() || !manifest.signingKey)
throw std::runtime_error("The manifest is revoked");
auto const list = loadUnsignedList(unsignedList);
auto const sig = decodeSignature(signature);
if (!verify(*manifest.signingKey, makeSlice(list.canonical), makeSlice(sig)))
throw std::runtime_error("The signature does not verify under the manifest's signing key");
std::optional<json::Value> append;
if (options.appendFile)
append = readJsonFile(*options.appendFile);
emitJson(
makeSignedList(
base64Encode(manifest.serialized),
manifest.masterKey,
list,
strHex(sig),
options.listVersion,
append),
options.outFile);
}
int
verifyListFile(boost::filesystem::path const& list, ToolOptions const& options)
{
using namespace xrpl;
std::optional<UnsignedList> roster;
if (options.validatorsFile)
roster = loadUnsignedList(*options.validatorsFile);
auto const result =
verifyList(readJsonFile(list), roster, options.expectedKey, rippleEpochNow());
std::cout << result.report.toStyledString() << std::endl;
return result.ok ? EXIT_SUCCESS : EXIT_FAILURE;
}
int
runCommand(
std::string const& command,
std::vector<std::string> const& args,
ToolOptions const& options)
{
using namespace std;
// Minimum and maximum number of positional arguments per command.
static map<string, pair<size_t, size_t>> const commandArgs = {
{"create_keys", {0, 0}},
{"create_token", {0, 0}},
{"revoke_keys", {0, 0}},
{"set_domain", {1, 1}},
{"clear_domain", {0, 0}},
{"attest_domain", {0, 0}},
{"show_manifest", {1, 1}},
{"sign", {1, 1}},
{"sign_hex", {1, 1}},
{"create_external", {1, 1}},
{"start_token", {0, 0}},
{"finish_token", {1, 2}},
{"start_revoke_keys", {0, 0}},
{"finish_revoke_keys", {1, 1}},
{"sign_list", {1, 1}},
{"start_sign_list", {1, 1}},
{"finish_sign_list", {2, 2}},
{"verify_list", {1, 1}},
};
auto const iArgs = commandArgs.find(command);
if (iArgs == commandArgs.end())
throw std::runtime_error("Unknown command: " + command);
if (args.size() < iArgs->second.first || args.size() > iArgs->second.second)
throw std::runtime_error("Syntax error: Wrong number of arguments");
auto const& keyFile = options.keyFile;
if (command == "create_keys")
createKeyFile(keyFile);
else if (command == "create_token")
createToken(options);
else if (command == "revoke_keys")
createRevocation(keyFile);
else if (command == "set_domain")
setDomain(args[0], options);
else if (command == "clear_domain")
setDomain("", options);
else if (command == "attest_domain")
attestDomain(keyFile);
else if (command == "sign")
signData(args[0], keyFile);
else if (command == "sign_hex")
signHexData(args[0], keyFile);
else if (command == "show_manifest")
generateManifest(args[0], keyFile);
else if (command == "create_external")
createExternal(args[0], keyFile);
else if (command == "start_token")
startToken(options);
else if (command == "finish_token")
finishToken(args, options);
else if (command == "start_revoke_keys")
startRevocation(keyFile);
else if (command == "finish_revoke_keys")
finishRevocation(args[0], keyFile);
else if (command == "sign_list")
signListFile(args[0], options);
else if (command == "start_sign_list")
startSignList(args[0], options);
else if (command == "finish_sign_list")
finishSignList(args[0], args[1], options);
else if (command == "verify_list")
return verifyListFile(args[0], options);
return 0;
}
// LCOV_EXCL_START
static std::string
getEnvVar(char const* name)
{
std::string value;
auto const v = getenv(name);
if (v != nullptr)
value = v;
return value;
}
void
printHelp(boost::program_options::options_description const& desc)
{
std::cerr << "validator-keys [options] <command> [<argument> ...]\n"
<< desc << std::endl
<< "Commands: \n"
" create_keys Generate validator keys.\n"
" create_token Generate validator token.\n"
" revoke_keys Revoke validator keys.\n"
" sign <data> Sign string with validator "
"key.\n"
" sign_hex <data> Decode and sign hex string with "
"validator key.\n"
" show_manifest [hex|base64] Displays the last generated "
"manifest\n"
" set_domain <domain> Associate a domain with the "
"validator key.\n"
" clear_domain Disassociate a domain from a "
"validator key.\n"
" attest_domain Produce the attestation string "
"for a domain.\n"
"Commands for signing externally: \n"
" create_external <public key> Generate validator keys without "
"a secret.\n"
" start_token Generate a partial token for "
"external signing; --signing-key delegates to an external key.\n"
" finish_token <master sig> [<signing sig>]\n"
" Finish generating token with "
"external signature(s).\n"
" start_revoke_keys Generate a partial revocation "
"for external signing.\n"
" finish_revoke_keys <sig> Finish generating revocation "
"with external signature.\n"
"Commands for validator lists: \n"
" sign_list <unsigned list> Sign a list with --token-file.\n"
" start_sign_list <unsigned list>\n"
" Print the bytes to sign with an "
"external signing key; needs --manifest-file.\n"
" finish_sign_list <sig> <unsigned list>\n"
" Assemble the signed list from an "
"external signature; needs --manifest-file.\n"
" verify_list <list> Check a published list; "
"--validators and --expected-key add checks.\n";
}
// LCOV_EXCL_STOP
std::string const&
getVersionString()
{
static std::string const value = [] {
std::string const s = versionString;
beast::SemanticVersion v;
if (!v.parse(s) || v.print() != s)
throw std::logic_error(s + ": Bad version string"); // LCOV_EXCL_LINE
return s;
}();
return value;
}
int
main(int argc, char** argv)
{
namespace po = boost::program_options;
po::variables_map vm;
// Set up option parsing.
//
po::options_description general("General Options");
general.add_options()("help,h", "Display this message.")(
"keyfile", po::value<std::string>(), "Specify the key file.")(
"token-key-type",
po::value<std::string>(),
"Key type of a token's signing key: secp256k1 (default) or ed25519.")(
"signing-key",
po::value<std::string>(),
"External signing key a token delegates to (start_token).")(
"token-file", po::value<std::string>(), "File holding a [validator_token] block.")(
"manifest-file", po::value<std::string>(), "File holding a base64 manifest.")(
"out", po::value<std::string>(), "Write the token or signed list to this file.")(
"list-version", po::value<unsigned>(), "Signed list version: 1 (default) or 2.")(
"append", po::value<std::string>(), "Version 2 list to add the new blob to.")(
"validators",
po::value<std::string>(),
"Unsigned list whose validators a published list must carry (verify_list).")(
"expected-key",
po::value<std::string>(),
"Master key a published list must be signed under (verify_list).")(
"unittest,u", "Perform unit tests.")("version", "Display the build version.");
po::options_description hidden("Hidden options");
hidden.add_options()("command", po::value<std::string>(), "Command.")(
"arguments",
po::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "empty"),
"Arguments.");
po::positional_options_description p;
p.add("command", 1).add("arguments", -1);
po::options_description cmdline_options;
cmdline_options.add(general).add(hidden);
// Parse options, if no error.
try
{
po::store(
po::command_line_parser(argc, argv)
.options(cmdline_options) // Parse options.
.positional(p)
.run(),
vm);
po::notify(vm); // Invoke option notify functions.
}
// LCOV_EXCL_START
catch (std::exception const&)
{
std::cerr << "validator-keys: Incorrect command line syntax." << std::endl;
std::cerr << "Use '--help' for a list of options." << std::endl;
return EXIT_FAILURE;
}
// LCOV_EXCL_STOP
// Run the unit tests if requested.
// The unit tests will exit the application with an appropriate return code.
if (vm.count("unittest"))
return runUnitTests();
// LCOV_EXCL_START
if (vm.count("version"))
{
std::cout << "validator-keys version " << getVersionString() << std::endl;
return 0;
}
if (vm.count("help") || !vm.count("command"))
{
printHelp(general);
return EXIT_SUCCESS;
}
std::string const homeDir = getEnvVar("HOME");
std::string const defaultKeyFile =
(homeDir.empty() ? boost::filesystem::current_path().string() : homeDir) +
"/.ripple/validator-keys.json";
try
{
using namespace boost::filesystem;
ToolOptions options;
options.keyFile = vm.count("keyfile") ? vm["keyfile"].as<std::string>() : defaultKeyFile;
if (vm.count("token-key-type"))
{
auto const keyType = xrpl::keyTypeFromString(vm["token-key-type"].as<std::string>());
if (!keyType)
throw std::runtime_error(
"Unknown key type: " + vm["token-key-type"].as<std::string>());
options.tokenKeyType = *keyType;
}
if (vm.count("signing-key"))
options.signingKey = parsePublicKey(vm["signing-key"].as<std::string>());
if (vm.count("token-file"))
options.tokenFile = path(vm["token-file"].as<std::string>());
if (vm.count("manifest-file"))
options.manifestFile = path(vm["manifest-file"].as<std::string>());
if (vm.count("out"))
options.outFile = path(vm["out"].as<std::string>());
if (vm.count("list-version"))
options.listVersion = vm["list-version"].as<unsigned>();
if (vm.count("append"))
options.appendFile = path(vm["append"].as<std::string>());
if (vm.count("validators"))
options.validatorsFile = path(vm["validators"].as<std::string>());
if (vm.count("expected-key"))
options.expectedKey = parsePublicKey(vm["expected-key"].as<std::string>());
return runCommand(
vm["command"].as<std::string>(),
vm["arguments"].as<std::vector<std::string>>(),
options);
}
catch (std::exception const& e)
{
std::cerr << e.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
// LCOV_EXCL_STOP
}

View File

@@ -0,0 +1,97 @@
#pragma once
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <boost/filesystem/path.hpp>
#include <optional>
#include <string>
#include <vector>
std::string const&
getVersionString();
/**
* The command-line options every command may read.
*/
struct ToolOptions
{
// The master key file.
boost::filesystem::path keyFile;
// Key type of a token's signing key.
xrpl::KeyType tokenKeyType = xrpl::KeyType::Secp256k1;
// External signing key a token delegates to.
std::optional<xrpl::PublicKey> signingKey;
// File holding a [validator_token] block.
std::optional<boost::filesystem::path> tokenFile;
// File holding a base64 manifest.
std::optional<boost::filesystem::path> manifestFile;
// File to write a token or a signed list to instead of stdout.
std::optional<boost::filesystem::path> outFile;
// Version of the signed list document.
unsigned listVersion = 1;
// Version 2 list to add a blob to.
std::optional<boost::filesystem::path> appendFile;
// Unsigned list whose validators a published list must carry.
std::optional<boost::filesystem::path> validatorsFile;
// Master key a published list must be signed under.
std::optional<xrpl::PublicKey> expectedKey;
};
void
createKeyFile(boost::filesystem::path const& keyFile);
void
createToken(ToolOptions const& options);
void
createRevocation(boost::filesystem::path const& keyFile);
/*****************************************/
/* External signing support */
void
createExternal(std::string const& data, boost::filesystem::path const& keyFile);
void
startToken(ToolOptions const& options);
void
finishToken(std::vector<std::string> const& signatures, ToolOptions const& options);
void
startRevocation(boost::filesystem::path const& keyFile);
void
finishRevocation(std::string const& data, boost::filesystem::path const& keyFile);
/*****************************************/
/* Validator lists */
void
signListFile(boost::filesystem::path const& unsignedList, ToolOptions const& options);
void
startSignList(boost::filesystem::path const& unsignedList, ToolOptions const& options);
void
finishSignList(
std::string const& signature,
boost::filesystem::path const& unsignedList,
ToolOptions const& options);
int
verifyListFile(boost::filesystem::path const& list, ToolOptions const& options);
/*****************************************/
void
signData(std::string const& data, boost::filesystem::path const& keyFile);
void
signHexData(std::string const& data, boost::filesystem::path const& keyFile);
int
runCommand(
std::string const& command,
std::vector<std::string> const& args,
ToolOptions const& options);

View File

@@ -0,0 +1,226 @@
# Validator Keys Tool Guide
This guide explains how to set up a validator so its public key does not have to
change if the rippled config and/or server are compromised.
A validator uses a public/private key pair. The validator is identified by the
public key. The private key should be tightly controlled. It is used to:
- sign tokens authorizing a rippled server to run as the validator identified
by this public key.
- sign revocations indicating that the private key has been compromised and
the validator public key should no longer be trusted.
Each new token invalidates all previous tokens for the validator public key.
The current token needs to be present in the rippled config file.
Servers that trust the validator will adapt automatically when the token
changes.
## Validator Keys
When first setting up a validator, use the `validator-keys` tool to generate
its key pair:
```
$ validator-keys create_keys
```
Sample output:
```
Validator keys stored in /home/ubuntu/.ripple/validator-keys.json
```
Keep the key file in a secure but recoverable location, such as an encrypted
USB flash drive. Do not modify its contents.
## Validator Token
After first creating the [validator keys](#validator-keys) or if the previous
token has been compromised, use the `validator-keys` tool to create a new
validator token:
```
$ validator-keys create_token
```
Sample output:
```
Update xrpld.cfg file with these values:
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
[validator_token]
eyJ2YWxpZGF0aW9uX3NlY3J|dF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT
QzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYT|kYWY2IiwibWFuaWZl
c3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE
hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG
bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2
hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1
NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj
VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ==
```
For a new validator, add the [validator_token] value to the rippled config file.
For a pre-existing validator, replace the old [validator_token] value with the
newly generated one. A valid config file may only contain one [validator_token]
value. After the config is updated, restart xrpld.
There is a hard limit of 4,294,967,293 tokens that can be generated for a given
validator key pair.
## Key Revocation
If a validator private key is compromised, the key must be revoked permanently.
To revoke the validator key, use the `validator-keys` tool to generate a
revocation, which indicates to other servers that the key is no longer valid:
```
$ validator-keys revoke_keys
```
Sample output:
```
WARNING: This will revoke your validator keys!
Update xrpld.cfg file with these values and restart xrpld:
# validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr
[validator_key_revocation]
JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr
jQ2KJSO/2tsjRhjO6q0QQHppslQsKNSXWxjGQNIEa6nPisBOKlDDcJVZAMP4QcIyNCadzgM=
```
Add the `[validator_key_revocation]` value to this validator's config and
restart xrpld. Rename the old key file and generate new [validator keys](#validator-keys) and
a corresponding [validator token](#validator-token).
## Signing
The `validator-keys` tool can be used to sign arbitrary data with the validator
key.
```
$ validator-keys sign "your data to sign"
```
Sample output:
```
B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D
```
## External Signing
The master key can live in a hardware signer instead of the key file. The tool
then produces the bytes to sign, the hardware signs them, and the tool
assembles the result.
One-time setup, with the hardware key's public key in base58 (`nHB...`), hex
(`ED...`) or base64:
```
$ validator-keys create_external <public key>
```
The key file is written with `"secret_key": "external"`.
To create a token, print the bytes to sign, sign them externally, and pass the
hex or base64 signature back:
```
$ validator-keys start_token
$ validator-keys finish_token <signature>
```
The output is the same `[validator_token]` block `create_token` prints. A
revocation works the same way with `start_revoke_keys` and
`finish_revoke_keys <signature>`.
When the signing key is also held by the hardware, name it when starting the
token. The bytes are then signed twice, once by each key, and the result is a
manifest without a secret:
```
$ validator-keys start_token --signing-key <signing public key>
$ validator-keys finish_token <master signature> <signing signature>
```
For testing without a hardware signer, a second key file can stand in for it:
```
$ validator-keys --keyfile other-keys.json sign_hex <bytes>
```
## Validator List Publishers
A publisher's keys are a master key and a signing key bound to it by a
manifest, exactly like a validator's. The token `create_token` prints holds
that manifest and the signing key, so a publisher's setup is:
```
$ validator-keys create_keys
$ validator-keys create_token --token-key-type ed25519 --out publisher-token.txt
```
`--token-key-type ed25519` matches what hardware signers support. `--out`
writes the token to a file readable only by its owner instead of printing it.
The manifest's sequence is `token_sequence` in the key file. A server keeps
the highest sequence it has seen for a master key, so a key migrated from
another tool must start `token_sequence` above the sequence of the manifest
currently published.
### Signing a list
The unsigned list is a JSON file with the fields a server reads:
```json
{
"sequence": 2026091301,
"expiration": 843955200,
"validators": [
{ "validation_public_key": "ED...", "manifest": "<base64 manifest>" }
]
}
```
`sequence` must rise with every published list, `expiration` and the optional
`effective` are seconds since the XRP Ledger epoch, and each validator's
`manifest` must belong to its `validation_public_key`. Whitespace is removed
and one space is placed after each `,` and `:` before signing.
```
$ validator-keys sign_list unsigned.json --token-file publisher-token.txt --out vl.json
```
`vl.json` is the version 1 document a server fetches: `blob`, `manifest`,
`public_key`, `signature`, `version`. `--list-version 2` writes the blob into
`blobs_v2` instead, and `--append <existing.json>` adds it to a version 2
document that already holds up to four blobs, so a list can be published
alongside the one it will replace.
When the signing key is held by a hardware signer, the list is signed in two
steps against the manifest from `finish_token`:
```
$ validator-keys start_sign_list unsigned.json --manifest-file manifest.txt
$ validator-keys finish_sign_list <signature> unsigned.json --manifest-file manifest.txt --out vl.json
```
### Checking a list
```
$ validator-keys verify_list vl.json --validators unsigned.json --expected-key <master public key>
```
The checks are the ones a server makes before trusting the list: the manifest
verifies and names `public_key`, every blob's signature verifies under the
manifest's signing key, every blob parses, and none has expired. `--validators`
requires every blob to list exactly the keys in the unsigned list, and
`--expected-key` requires the master key to be the one given. The result is
printed as JSON and the exit code is 0 only if every check passed.

View File

@@ -0,0 +1,60 @@
#pragma once
#include <xrpl/beast/unit_test.h>
#include <boost/filesystem.hpp>
#include <fstream>
namespace xrpl {
/**
* Write a key file dir and remove when done.
*/
class KeyFileGuard
{
private:
using path = boost::filesystem::path;
path subDir_;
beast::unit_test::Suite& test_;
auto
rmDir(path const& toRm)
{
if (is_directory(toRm))
remove_all(toRm);
else
test_.log << "Expected " << toRm.string() << " to be an existing directory."
<< std::endl;
};
public:
KeyFileGuard(beast::unit_test::Suite& test, std::string const& subDir)
: subDir_(subDir), test_(test)
{
using namespace boost::filesystem;
if (!exists(subDir_))
create_directory(subDir_);
else
// Cannot run the test. Someone created a file or directory
// where we want to put our directory
throw std::runtime_error("Cannot create directory: " + subDir_.string());
}
~KeyFileGuard()
{
try
{
using namespace boost::filesystem;
rmDir(subDir_);
}
catch (std::exception& e)
{
// if we throw here, just let it die.
test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl;
};
}
};
} // namespace xrpl

View File

@@ -0,0 +1,509 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/jss.h>
#include <tools/validator-keys/ListSigning.h>
#include <tools/validator-keys/SigningKeys.h>
#include <tools/validator-keys/test/KeyFileGuard.h>
#include <fstream>
namespace xrpl {
namespace tests {
class ListSigning_test : public beast::unit_test::Suite
{
private:
// A publisher: master keys and the token carrying its signing key.
struct Publisher
{
SigningKeys keys{KeyType::Ed25519};
ValidatorToken token;
Manifest manifest;
Publisher()
: token(*keys.createValidatorToken(KeyType::Ed25519))
, manifest(*deserializeManifest(base64Decode(token.manifest)))
{
}
};
// The unsigned list text a publisher's `prepare` step writes: one
// validator per token, each with its manifest.
static std::string
unsignedListText(
std::vector<ValidatorToken> const& validators,
std::uint32_t sequence,
std::uint32_t expiration,
std::optional<std::uint32_t> effective = std::nullopt)
{
std::string text = "{\n \"sequence\": " + std::to_string(sequence);
if (effective)
text += ",\n \"effective\": " + std::to_string(*effective);
text += ",\n \"expiration\": " + std::to_string(expiration) + ",\n \"validators\": [";
bool first = true;
for (auto const& v : validators)
{
auto const m = deserializeManifest(base64Decode(v.manifest));
text += first ? "\n" : ",\n";
first = false;
text += " {\"validation_public_key\": \"" + strHex(m->masterKey) +
"\", \"manifest\": \"" + v.manifest + "\"}";
}
text += "\n ]\n}\n";
return text;
}
static std::vector<ValidatorToken>
makeValidators(std::size_t count)
{
std::vector<ValidatorToken> validators;
for (std::size_t i = 0; i < count; ++i)
{
SigningKeys keys(KeyType::Ed25519);
validators.push_back(*keys.createValidatorToken(KeyType::Secp256k1));
}
return validators;
}
static json::Value
parse(std::string const& text)
{
json::Reader reader;
json::Value jv;
reader.parse(text, jv);
return jv;
}
void
testCanonicalJson()
{
testcase("Canonical JSON");
// Whitespace outside strings goes, one space follows each comma and
// colon, key order and string contents stay.
BEAST_EXPECT(
canonicalJson("{ \"b\" :1,\n\t\"a\": [ 1 , 2 ] , \"s\":\"x, y: z\" }") ==
"{\"b\": 1, \"a\": [1, 2], \"s\": \"x, y: z\"}");
BEAST_EXPECT(canonicalJson("{\"e\": \"a\\\"b\"}") == "{\"e\": \"a\\\"b\"}");
BEAST_EXPECT(canonicalJson("{\"sequence\": 1, \"x\": 2}") == "{\"sequence\": 1, \"x\": 2}");
try
{
canonicalJson("[1, 2]");
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(e.what() == std::string("Not a JSON object"));
}
try
{
canonicalJson("{\"a\": ");
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(e.what() == std::string("Not a JSON object"));
}
}
void
testParseUnsignedList()
{
testcase("Parse Unsigned List");
auto const validators = makeValidators(2);
{
auto const list = parseUnsignedList(unsignedListText(validators, 5, 1000, 500));
BEAST_EXPECT(list.sequence == 5);
BEAST_EXPECT(list.expiration == 1000);
BEAST_EXPECT(list.effective && *list.effective == 500);
BEAST_EXPECT(list.validators.size() == 2);
BEAST_EXPECT(
list.validators[0] ==
deserializeManifest(base64Decode(validators[0].manifest))->masterKey);
}
{
auto const list = parseUnsignedList(unsignedListText(validators, 5, 1000));
BEAST_EXPECT(!list.effective);
// The canonical text keeps the key order of the input.
BEAST_EXPECT(list.canonical.starts_with("{\"sequence\": 5, \"expiration\": 1000, "));
}
auto expectError = [this](std::string const& text, std::string const& expected) {
try
{
parseUnsignedList(text);
fail(expected);
}
catch (std::runtime_error const& e)
{
BEAST_EXPECTS(e.what() == expected, e.what());
}
};
expectError(
"{\"expiration\": 1, \"validators\": []}", "\"sequence\" must be a positive integer");
expectError(
"{\"sequence\": 0, \"expiration\": 1, \"validators\": []}",
"\"sequence\" must be a positive integer");
expectError(
"{\"sequence\": 1, \"validators\": []}", "\"expiration\" must be an unsigned integer");
expectError(
"{\"sequence\": 1, \"effective\": 1000, \"expiration\": 1000, \"validators\": []}",
"\"effective\" must be earlier than \"expiration\"");
expectError(
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": []}",
"\"validators\" must be a non-empty array");
expectError(
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": [{}]}",
"every validator needs a \"validation_public_key\" string");
expectError(
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": [{\"validation_public_key\": "
"\"ED00\"}]}",
"\"validation_public_key\" is not a hex public key: ED00");
{
// A manifest of another key.
auto const other = deserializeManifest(base64Decode(validators[1].manifest));
auto const text =
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": "
"[{\"validation_public_key\": \"" +
strHex(other->masterKey) + "\", \"manifest\": \"" + validators[0].manifest +
"\"}]}";
expectError(
text, "\"manifest\" belongs to another key than " + strHex(other->masterKey));
}
{
auto const key = deserializeManifest(base64Decode(validators[0].manifest))->masterKey;
auto const text =
"{\"sequence\": 1, \"expiration\": 1000, \"validators\": "
"[{\"validation_public_key\": \"" +
strHex(key) + "\", \"manifest\": \"AAAA\"}]}";
expectError(text, "\"manifest\" does not verify for " + strHex(key));
}
}
void
testSignAndVerify()
{
testcase("Sign and Verify");
Publisher const publisher;
auto const validators = makeValidators(3);
std::uint32_t const now = 1000;
auto const list = parseUnsignedList(unsignedListText(validators, 7, now + 100));
auto const signature =
signList(list, *publisher.manifest.signingKey, publisher.token.validationSecret);
// Version 1
auto const v1 = makeSignedList(
publisher.token.manifest,
publisher.manifest.masterKey,
list,
signature,
1,
std::nullopt);
BEAST_EXPECT(v1[jss::version].asUInt() == 1);
BEAST_EXPECT(v1[jss::public_key].asString() == strHex(publisher.manifest.masterKey));
BEAST_EXPECT(v1[jss::manifest].asString() == publisher.token.manifest);
BEAST_EXPECT(base64Decode(v1[jss::blob].asString()) == list.canonical);
BEAST_EXPECT(v1[jss::signature].asString() == signature);
{
auto const result = verifyList(v1, list, publisher.manifest.masterKey, now);
BEAST_EXPECTS(result.ok, to_string(result.report));
BEAST_EXPECT(result.report["blobs"].size() == 1);
BEAST_EXPECT(result.report["blobs"][0u][jss::sequence].asUInt() == 7);
BEAST_EXPECT(result.report["blobs"][0u][jss::validators].asUInt() == 3);
BEAST_EXPECT(!result.report["blobs"][0u]["expired"].asBool());
BEAST_EXPECT(result.report["manifest_sequence"].asUInt() == 1);
}
// Version 2, then a second blob appended
auto const v2 = makeSignedList(
publisher.token.manifest,
publisher.manifest.masterKey,
list,
signature,
2,
std::nullopt);
BEAST_EXPECT(v2[jss::version].asUInt() == 2);
BEAST_EXPECT(v2[jss::blobs_v2].size() == 1);
BEAST_EXPECT(!v2.isMember(jss::blob));
BEAST_EXPECT(verifyList(v2, list, std::nullopt, now).ok);
auto const later = parseUnsignedList(unsignedListText(validators, 8, now + 300, now + 200));
auto const laterSig =
signList(later, *publisher.manifest.signingKey, publisher.token.validationSecret);
auto const v2b = makeSignedList(
publisher.token.manifest, publisher.manifest.masterKey, later, laterSig, 2, v2);
BEAST_EXPECT(v2b[jss::blobs_v2].size() == 2);
{
auto const result = verifyList(v2b, std::nullopt, std::nullopt, now);
BEAST_EXPECTS(result.ok, to_string(result.report));
BEAST_EXPECT(result.report["blobs"][1u][jss::effective].asUInt() == now + 200);
}
// Append refuses the wrong shape, another publisher, and a full list
try
{
makeSignedList(
publisher.token.manifest, publisher.manifest.masterKey, list, signature, 1, v2);
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
e.what() ==
std::string("A version 1 list holds one blob; use version 2 to append"));
}
try
{
makeSignedList(
publisher.token.manifest, publisher.manifest.masterKey, list, signature, 2, v1);
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(e.what() == std::string("The list to append to is not a version 2 list"));
}
{
Publisher const other;
try
{
makeSignedList(
other.token.manifest, other.manifest.masterKey, list, signature, 2, v2);
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
e.what() == std::string("The list to append to belongs to another master key"));
}
}
{
auto full = v2;
while (full[jss::blobs_v2].size() < 5)
full[jss::blobs_v2].append(full[jss::blobs_v2][0u]);
try
{
makeSignedList(
publisher.token.manifest,
publisher.manifest.masterKey,
list,
signature,
2,
full);
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
e.what() == std::string("The list to append to already holds 5 blobs"));
}
}
}
void
testVerifyRejects()
{
testcase("Verify Rejects");
Publisher const publisher;
auto const validators = makeValidators(2);
std::uint32_t const now = 1000;
auto const list = parseUnsignedList(unsignedListText(validators, 7, now + 100));
auto const signature =
signList(list, *publisher.manifest.signingKey, publisher.token.validationSecret);
auto const good = makeSignedList(
publisher.token.manifest,
publisher.manifest.masterKey,
list,
signature,
1,
std::nullopt);
auto expectError = [this](
json::Value const& doc,
std::optional<UnsignedList> const& roster,
std::optional<PublicKey> const& key,
std::uint32_t at,
std::string const& expected) {
auto const result = verifyList(doc, roster, key, at);
BEAST_EXPECT(!result.ok);
bool found = false;
for (auto const& e : result.errors)
found = found || e == expected;
BEAST_EXPECTS(found, to_string(result.report));
};
// Tampered blob: the signature no longer matches
{
auto tampered = good;
auto text = list.canonical;
text.replace(text.find("\"sequence\": 7"), 13, "\"sequence\": 9");
tampered[jss::blob] = base64Encode(text);
expectError(
tampered,
std::nullopt,
std::nullopt,
now,
"blob 0: the signature does not verify under the signing key");
}
// Expired
expectError(good, std::nullopt, std::nullopt, now + 100, "blob 0: expired");
// Wrong manifest: another publisher's
{
Publisher const other;
auto wrong = good;
wrong[jss::manifest] = other.token.manifest;
expectError(
wrong,
std::nullopt,
std::nullopt,
now,
"\"public_key\" is not the manifest's master key");
}
// Not the expected key
{
Publisher const other;
expectError(
good,
std::nullopt,
other.manifest.masterKey,
now,
"the master key is not the expected key");
}
// Roster mismatch
{
auto const others = makeValidators(2);
auto const roster = parseUnsignedList(unsignedListText(others, 1, now + 100));
expectError(
good,
roster,
std::nullopt,
now,
"blob 0: the validators differ from the expected list");
}
// Structural
{
auto bad = good;
bad[jss::version] = 3;
expectError(bad, std::nullopt, std::nullopt, now, "\"version\" must be 1 or 2");
}
{
auto bad = good;
bad[jss::blobs_v2] = json::Value(json::ValueType::Array);
expectError(
bad,
std::nullopt,
std::nullopt,
now,
"a version 1 list needs \"blob\" and \"signature\" and no \"blobs_v2\"");
}
{
auto bad = good;
bad[jss::manifest] = "AAAA";
expectError(
bad,
std::nullopt,
std::nullopt,
now,
"\"manifest\" does not deserialize and verify");
}
}
void
testFiles()
{
testcase("Token and Manifest Files");
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
Publisher const publisher;
// A token file as `create_token` writes it: header, comment, 72-char lines
path const tokenFile = subdir / "token.txt";
{
std::ofstream o(tokenFile.string());
o << "# validator public key: "
<< toBase58(TokenType::NodePublic, publisher.keys.publicKey()) << "\n\n";
o << "[validator_token]\n";
auto const body = tokenToBase64(publisher.token);
for (std::size_t i = 0; i < body.size(); i += 72)
o << body.substr(i, 72) << "\n";
}
{
auto const token = loadTokenFile(tokenFile);
BEAST_EXPECT(token.manifest == publisher.token.manifest);
BEAST_EXPECT(
std::equal(
token.validationSecret.begin(),
token.validationSecret.end(),
publisher.token.validationSecret.begin()));
}
path const manifestFile = subdir / "manifest.txt";
{
std::ofstream o(manifestFile.string());
o << "# publisher manifest\n" << publisher.token.manifest << "\n";
}
{
auto const manifest = loadManifestFile(manifestFile);
BEAST_EXPECT(manifest.masterKey == publisher.manifest.masterKey);
BEAST_EXPECT(manifest.signingKey == publisher.manifest.signingKey);
}
try
{
loadTokenFile(manifestFile);
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(e.what() == "Not a validator token: " + manifestFile.string());
}
try
{
loadManifestFile(subdir / "missing.txt");
fail();
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(e.what() == "Failed to open file: " + (subdir / "missing.txt").string());
}
path const listFile = subdir / "unsigned.json";
{
std::ofstream o(listFile.string());
o << unsignedListText(makeValidators(1), 3, 5000);
}
auto const list = loadUnsignedList(listFile);
BEAST_EXPECT(list.sequence == 3 && list.validators.size() == 1);
}
public:
void
run() override
{
testCanonicalJson();
testParseUnsignedList();
testSignAndVerify();
testVerifyRejects();
testFiles();
}
};
BEAST_DEFINE_TESTSUITE(ListSigning, keys, xrpl);
} // namespace tests
} // namespace xrpl

View File

@@ -0,0 +1,742 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Sign.h>
#include <tools/validator-keys/SigningKeys.h>
#include <tools/validator-keys/test/KeyFileGuard.h>
namespace xrpl {
namespace tests {
class SigningKeys_test : public beast::unit_test::Suite
{
private:
void
testKeyFile(
boost::filesystem::path const& keyFile,
json::Value const& jv,
std::string const& expectedError)
{
{
std::ofstream o(keyFile.string(), std::ios_base::trunc);
o << jv.toStyledString();
o.close();
}
try
{
SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::runtime_error& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
}
std::array<KeyType, 2> const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
void
testMakeSigningKeys()
{
testcase("Make Validator Keys");
using namespace boost::filesystem;
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
for (auto const keyType : keyTypes)
{
SigningKeys const keys(keyType);
KeyFileGuard const g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
auto const keys2 = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == keys2);
}
{
// Require expected fields
KeyFileGuard g(*this, subdir.string());
auto expectedError = "Failed to open key file: " + keyFile.string();
std::string error;
try
{
SigningKeys::make_SigningKeys(keyFile);
fail();
}
catch (std::runtime_error& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
expectedError = "Unable to parse json key file: " + keyFile.string();
{
std::ofstream o(keyFile.string(), std::ios_base::trunc);
o << "{{}";
o.close();
}
try
{
SigningKeys::make_SigningKeys(keyFile);
fail();
}
catch (std::runtime_error& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
json::Value jv;
jv["dummy"] = "field";
expectedError = "Key file '" + keyFile.string() + "' is missing \"key_type\" field";
testKeyFile(keyFile, jv, expectedError);
jv["key_type"] = "dummy keytype";
expectedError = "Key file '" + keyFile.string() + "' is missing \"secret_key\" field";
testKeyFile(keyFile, jv, expectedError);
jv["secret_key"] = "dummy secret";
expectedError =
"Key file '" + keyFile.string() + "' is missing \"token_sequence\" field";
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = "dummy sequence";
expectedError = "Key file '" + keyFile.string() + "' is missing \"revoked\" field";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = "dummy revoked";
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"key_type\" field: " + jv["key_type"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
auto const keyType = KeyType::Ed25519;
jv["key_type"] = to_string(keyType);
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"secret_key\" field: " + jv["secret_key"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
SigningKeys const keys(keyType);
{
auto const kp = generateKeyPair(keyType, randomSeed());
jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second);
}
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = -1;
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = json::UInt(std::numeric_limits<std::uint32_t>::max());
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"revoked\" field: " + jv["revoked"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = false;
expectedError = "";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = true;
testKeyFile(keyFile, jv, expectedError);
}
}
void
testCreateValidatorToken()
{
testcase("Create Validator Token");
for (auto const keyType : keyTypes)
{
SigningKeys keys(keyType);
std::uint32_t sequence = 0;
for (auto const tokenKeyType : keyTypes)
{
auto const token = keys.createValidatorToken(tokenKeyType);
if (!BEAST_EXPECT(token))
continue;
auto const tokenPublicKey = derivePublicKey(tokenKeyType, token->validationSecret);
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(token->manifest);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == ++sequence);
auto const tpk = get<PublicKey>(st, sfSigningPubKey);
BEAST_EXPECT(tpk);
BEAST_EXPECT(*tpk == tokenPublicKey);
BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey));
auto const pk = get<PublicKey>(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
try
{
keys.verifyManifest();
}
catch (std::exception const& e)
{
fail(e.what());
}
}
}
auto const keyType = KeyType::Ed25519;
auto const kp = generateKeyPair(keyType, randomSeed());
auto keys = SigningKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
BEAST_EXPECT(!keys.createValidatorToken(keyType));
keys.revoke();
BEAST_EXPECT(!keys.createValidatorToken(keyType));
}
void
testRevoke()
{
testcase("Revoke");
for (auto const keyType : keyTypes)
{
SigningKeys keys(keyType);
auto const revocation = keys.revoke();
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(revocation);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == std::numeric_limits<std::uint32_t>::max());
auto const pk = get(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
try
{
keys.verifyManifest();
}
catch (std::exception const& e)
{
fail(e.what());
}
}
}
void
signWorker(
std::function<std::string(std::string const&)> modifyFunc,
std::function<std::string(SigningKeys const&, std::string const&)> signFunc)
{
std::string const rawdata = "data to sign";
std::string const data = modifyFunc(rawdata);
std::map<KeyType, std::string> expected(
{{KeyType::Ed25519,
"2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F"
"237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939"
"C8F29B0DC759FD712300"},
{KeyType::Secp256k1,
"3045022100F142C27BF83D8D4541C7A4E759DE64A672"
"51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5"
"55438D64A229CD0B4BA2AE33402443B20409"}});
for (auto const keyType : keyTypes)
{
auto const sk = generateSecretKey(keyType, generateSeed("test"));
SigningKeys keys(keyType, sk, 1);
{
SigningKeys pkOnly(keyType, derivePublicKey(keyType, sk));
try
{
signFunc(pkOnly, data);
fail();
}
catch (std::exception const& e)
{
using namespace std::string_literals;
BEAST_EXPECT(e.what() == "This key file cannot be used to sign."s);
}
}
auto const signature = signFunc(keys, data);
BEAST_EXPECT(expected[keyType] == signature);
auto const ret = strUnHex(signature);
BEAST_EXPECT(ret);
BEAST_EXPECT(ret->size());
BEAST_EXPECT(verify(keys.publicKey(), makeSlice(rawdata), makeSlice(*ret)));
}
}
void
testSign()
{
testcase("Sign");
signWorker(
[](auto const& data) { return data; },
[](auto const& keys, auto const& data) { return keys.sign(data); });
}
void
testSignHex()
{
testcase("Sign Hex");
signWorker(
[](auto const& data) { return strHex(data); },
[](auto const& keys, auto const& data) { return keys.signHex(data); });
}
void
testWriteToFile()
{
testcase("Write to File");
using namespace boost::filesystem;
auto const keyType = KeyType::Ed25519;
SigningKeys keys(keyType);
{
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
KeyFileGuard g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
{
auto fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
// Overwrite file with new sequence
keys.createValidatorToken(KeyType::Secp256k1);
keys.writeToFile(keyFile);
}
{
auto const fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
}
}
{
// Write to key file in current relative directory
path const keyFile = "test_validator_keys.json";
if (!exists(keyFile))
{
keys.writeToFile(keyFile);
remove(keyFile.string());
}
else
{
// Cannot run the test. Someone created a file
// where we want to put our key file
Throw<std::runtime_error>("Cannot create key file: " + keyFile.string());
}
}
{
// Create key file directory
path const subdir = "test_key_file";
path const keyFile = subdir / "directories/to/create/validator_keys.json";
KeyFileGuard g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
auto const fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
}
{
// Fail if file cannot be opened for write
path const subdir = "test_key_file";
KeyFileGuard g(*this, subdir.string());
path const badKeyFile = subdir / ".";
auto expectedError = "Cannot open key file: " + badKeyFile.string();
std::string error;
try
{
keys.writeToFile(badKeyFile);
fail();
}
catch (std::runtime_error& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
// Fail if parent directory is existing file
path const keyFile = subdir / "validator_keys.json";
keys.writeToFile(keyFile);
path const conflictingPath = keyFile / "validators_keys.json";
expectedError = "Cannot create directory: " + conflictingPath.parent_path().string();
try
{
keys.writeToFile(conflictingPath);
fail();
}
catch (std::runtime_error& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
}
}
////////////////////////////////////////////
// Tests related to using external keys
//
// These tests will use two SigningKeys objects,
// one with a secret key representing the external
// signing mechanism, and one only containing the
// public key from the first representing the real
// worker.
////////////////////////////////////////////
void
testExternalMakeValidatorKeys()
{
testcase("Make External Validator Keys");
using namespace boost::filesystem;
path const subdir = "test_key_file";
path const externalKeyFile = subdir / "validator_keys_external.json";
path const keyFile = subdir / "validator_keys.json";
for (auto const keyType : keyTypes)
{
SigningKeys const externalKeys(keyType);
KeyFileGuard const g(*this, subdir.string());
externalKeys.writeToFile(externalKeyFile);
BEAST_EXPECT(exists(externalKeyFile));
SigningKeys const keys(keyType, externalKeys.publicKey());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
auto const keys2 = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == keys2);
}
{
// Require expected fields
KeyFileGuard g(*this, subdir.string());
auto expectedError = "Failed to open key file: " + keyFile.string();
std::string error;
json::Value jv;
jv["key_type"] = "dummy keytype";
jv["secret_key"] = "external";
expectedError =
"Key file '" + keyFile.string() + "' is missing \"token_sequence\" field";
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = "dummy sequence";
expectedError = "Key file '" + keyFile.string() + "' is missing \"revoked\" field";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = "dummy revoked";
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"key_type\" field: " + jv["key_type"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
auto const keyType = KeyType::Ed25519;
jv["key_type"] = to_string(keyType);
expectedError = "Key file '" + keyFile.string() + "' is missing \"public_key\" field";
testKeyFile(keyFile, jv, expectedError);
jv["public_key"] = "dummy public";
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"public_key\" field: " + jv["public_key"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
SigningKeys const keys(keyType);
{
auto const kp = generateKeyPair(keyType, randomSeed());
jv["public_key"] = toBase58(TokenType::NodePublic, kp.first);
}
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = -1;
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"token_sequence\" field: " +
jv["token_sequence"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["token_sequence"] = json::UInt(std::numeric_limits<std::uint32_t>::max());
expectedError = "Key file '" + keyFile.string() +
"' contains invalid \"revoked\" field: " + jv["revoked"].toStyledString();
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = false;
expectedError = "";
testKeyFile(keyFile, jv, expectedError);
jv["revoked"] = true;
testKeyFile(keyFile, jv, expectedError);
}
}
void
testExternalCreateValidatorToken()
{
testcase("Create External Validator Token");
using namespace std::string_literals;
for (auto const keyType : keyTypes)
{
SigningKeys const externalKeys(keyType);
SigningKeys keys(keyType, externalKeys.publicKey());
std::uint32_t sequence = 0;
for (auto const tokenKeyType : keyTypes)
{
try
{
auto const token = keys.createValidatorToken(tokenKeyType);
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == "This key file cannot be used to sign tokens."s);
}
auto const start = keys.startValidatorToken(tokenKeyType);
if (!BEAST_EXPECT(start))
continue;
auto const sig = externalKeys.signHex(*start);
auto const sigBlob = strUnHex(sig);
if (!BEAST_EXPECT(sigBlob))
continue;
auto const token = keys.finishToken(*sigBlob);
if (!BEAST_EXPECT(token))
continue;
auto const tokenPublicKey = derivePublicKey(tokenKeyType, token->validationSecret);
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(token->manifest);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == ++sequence);
auto const tpk = get<PublicKey>(st, sfSigningPubKey);
BEAST_EXPECT(tpk);
BEAST_EXPECT(*tpk == tokenPublicKey);
BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey));
auto const pk = get<PublicKey>(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
try
{
keys.verifyManifest();
}
catch (std::exception const& e)
{
fail(e.what());
}
}
}
auto const keyType = KeyType::Ed25519;
auto const kp = generateKeyPair(keyType, randomSeed());
{
// The next sequence is the special "revoked" value
auto keys =
SigningKeys(keyType, kp.first, std::numeric_limits<std::uint32_t>::max() - 1);
BEAST_EXPECT(!keys.startValidatorToken(keyType));
}
{
// Key is revoked
auto keys = SigningKeys(keyType, kp.first, std::numeric_limits<std::uint32_t>::max());
BEAST_EXPECT(!keys.startValidatorToken(keyType));
}
}
void
testExternalRevoke()
{
testcase("External Revoke");
using namespace std::string_literals;
for (auto const keyType : keyTypes)
{
SigningKeys const externalKeys(keyType);
SigningKeys keys(keyType, externalKeys.publicKey());
try
{
auto const revocation = keys.revoke();
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == "This key file cannot be used to sign tokens."s);
}
auto const start = keys.startRevoke();
auto const sig = externalKeys.signHex(start);
auto const sigBlob = strUnHex(sig);
if (!BEAST_EXPECT(sigBlob))
continue;
auto const revocation = keys.finishRevoke(*sigBlob);
STObject st(sfGeneric);
auto const manifest = xrpl::base64Decode(revocation);
SerialIter sit(manifest.data(), manifest.size());
st.set(sit);
auto const seq = get(st, sfSequence);
BEAST_EXPECT(seq);
BEAST_EXPECT(*seq == std::numeric_limits<std::uint32_t>::max());
auto const pk = get(st, sfPublicKey);
BEAST_EXPECT(pk);
BEAST_EXPECT(*pk == keys.publicKey());
BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature));
try
{
keys.verifyManifest();
}
catch (std::exception const& e)
{
fail(e.what());
}
}
}
void
testExternalWriteToFile()
{
testcase("External Write to File");
using namespace boost::filesystem;
auto const keyType = KeyType::Ed25519;
SigningKeys const externalKeys(keyType);
SigningKeys keys(keyType, externalKeys.publicKey());
{
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
KeyFileGuard g(*this, subdir.string());
keys.writeToFile(keyFile);
BEAST_EXPECT(exists(keyFile));
{
auto const sigBlob = [&]() -> std::optional<Blob> {
auto fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
// Prepare to write new sequence
auto const start = keys.startValidatorToken(KeyType::Secp256k1);
if (!BEAST_EXPECT(start))
return std::nullopt;
// keys looks the same as the original file (though
// the pending fields have changed)
BEAST_EXPECT(keys == fileKeys);
keys.writeToFile(keyFile);
auto const sig = externalKeys.signHex(*start);
auto const sigBlob = strUnHex(sig);
return sigBlob;
}();
auto fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
if (!sigBlob)
return;
// Overwrite file with new sequence
auto const token = keys.finishToken(*sigBlob);
if (!BEAST_EXPECT(token))
return;
BEAST_EXPECT(keys != fileKeys);
keys.writeToFile(keyFile);
}
{
auto const fileKeys = SigningKeys::make_SigningKeys(keyFile);
BEAST_EXPECT(keys == fileKeys);
}
}
}
public:
void
run() override
{
testMakeSigningKeys();
testCreateValidatorToken();
testRevoke();
testSign();
testSignHex();
testWriteToFile();
// External
testExternalMakeValidatorKeys();
testExternalCreateValidatorToken();
testExternalRevoke();
testExternalWriteToFile();
}
};
BEAST_DEFINE_TESTSUITE(SigningKeys, keys, xrpl);
} // namespace tests
} // namespace xrpl

View File

@@ -0,0 +1,923 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/protocol/SecretKey.h>
#include <boost/algorithm/string.hpp>
#include <tools/validator-keys/ListSigning.h>
#include <tools/validator-keys/SigningKeys.h>
#include <tools/validator-keys/ValidatorKeysTool.h>
#include <tools/validator-keys/test/KeyFileGuard.h>
namespace xrpl {
namespace tests {
class ValidatorKeysTool_test : public beast::unit_test::Suite
{
private:
static ToolOptions
toolOptions(boost::filesystem::path const& keyFile)
{
ToolOptions options;
options.keyFile = keyFile;
return options;
}
// Allow a stream to be redirected. Destructor restores old streambuf.
class Redirect
{
public:
Redirect(std::ostream& stream, std::stringstream& sStream)
: stream_(stream), old_(stream_.rdbuf(sStream.rdbuf()))
{
}
virtual ~Redirect()
{
stream_.rdbuf(old_);
}
private:
std::ostream& stream_;
std::streambuf* const old_;
};
// Allow cout to be redirected. Destructor restores old cout streambuf.
class CoutRedirect : public Redirect
{
public:
CoutRedirect(std::stringstream& sStream) : Redirect(std::cout, sStream)
{
}
~CoutRedirect()
{
}
};
void
testCreateKeyFile()
{
testcase("Create Key File");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
std::string const expectedError =
"Refusing to overwrite existing key file: " + keyFile.string();
std::string error;
try
{
createKeyFile(keyFile);
fail();
}
catch (std::exception const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
}
void
testCreateToken()
{
testcase("Create Token");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto testToken = [this](path const& keyFile, std::string const& expectedError) {
try
{
createToken(toolOptions(keyFile));
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
testToken(keyFile, expectedError);
}
createKeyFile(keyFile);
{
std::string const expectedError = "";
testToken(keyFile, expectedError);
}
{
auto const keyType = KeyType::Ed25519;
auto const kp = generateKeyPair(keyType, randomSeed());
auto keys =
SigningKeys(keyType, kp.second, std::numeric_limits<std::uint32_t>::max() - 1);
keys.writeToFile(keyFile);
std::string const expectedError =
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.";
testToken(keyFile, expectedError);
}
{
createRevocation(keyFile);
std::string const expectedError = "Validator keys have been revoked.";
testToken(keyFile, expectedError);
}
}
void
testCreateRevocation()
{
testcase("Create Revocation");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto expectedError = "Failed to open key file: " + keyFile.string();
std::string error;
try
{
createRevocation(keyFile);
fail();
}
catch (std::runtime_error& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
createRevocation(keyFile);
createRevocation(keyFile);
}
void
testCreateKeyFileExternal()
{
testcase("Create Key File External");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
path const keyFile = subdir / "validator_keys.json";
// The externalKey will contain a secret key, and be used
// to simulate the actions of an actual external signing device
// or process. Note that it is const and not written to disk.
SigningKeys const externalKey(KeyType::Ed25519);
auto testCreate = [this, &subdir, &keyFile](
std::string pubKey, std::string const& expectedError) {
KeyFileGuard const g(*this, subdir.string());
try
{
createExternal(pubKey, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
// Test a few different ways to create the file, and remove the file in
// between
{
std::string const pubKey(strHex(externalKey.publicKey()));
std::string const expectedError;
testCreate(pubKey, expectedError);
}
{
auto const& key = externalKey.publicKey();
std::string const pubKey(base64Encode(key.data(), key.size()));
std::string const expectedError;
testCreate(pubKey, expectedError);
}
{
std::string badPubKey(strHex(externalKey.publicKey()));
badPubKey.insert(badPubKey.size() / 2, "n");
std::string const expectedError = "Unable to parse public key: " + badPubKey;
testCreate(badPubKey, expectedError);
}
{
std::string const badPubKey = "abcd";
std::string const expectedError = "Unable to parse public key: " + badPubKey;
testCreate(badPubKey, expectedError);
}
// Use one file for the remainder of the tests
KeyFileGuard const g(*this, subdir.string());
std::string const pubKey(toBase58(TokenType::NodePublic, externalKey.publicKey()));
createExternal(pubKey, keyFile);
BEAST_EXPECT(exists(keyFile));
std::string const expectedError =
"Refusing to overwrite existing key file: " + keyFile.string();
std::string error;
try
{
createExternal(pubKey, keyFile);
fail();
}
catch (std::exception const& e)
{
error = e.what();
}
BEAST_EXPECT(error == expectedError);
}
void
testCreateTokenExternal()
{
testcase("Create Token External");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
// The external key will contain a secret key, and be used
// to simulate the actions of an actual external signing device
// or process. Note that it is const.
KeyType const externalKeyType = KeyType::Ed25519;
SigningKeys const externalKey(externalKeyType);
std::string const pubKey(toBase58(TokenType::NodePublic, externalKey.publicKey()));
auto testStart = [this](path const& keyFile, std::string const& expectedError) {
std::stringstream capture;
CoutRedirect coutRedirect{capture};
try
{
startToken(toolOptions(keyFile));
BEAST_EXPECT(expectedError.empty());
return capture.str();
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
return std::string();
};
auto testFinish =
[this](std::string const& sig, path const& keyFile, std::string const& expectedError) {
try
{
finishToken({sig}, toolOptions(keyFile));
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
BEAST_EXPECT(testStart(keyFile, expectedError).empty());
}
createExternal(pubKey, keyFile);
std::string const noError = "";
{
auto const start = testStart(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.signHex(start);
testFinish(sig, keyFile, noError);
}
{
auto const start = testStart(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = [&]() {
auto sigBlob = strUnHex(externalKey.signHex(start));
if (BEAST_EXPECT(sigBlob))
return base64Encode(sigBlob->data(), sigBlob->size());
return base64Encode("fail");
}();
testFinish(sig, keyFile, noError);
}
{
std::string const expectedError = "Manifest is not properly signed";
auto const start = testStart(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.sign("foo");
testFinish(sig, keyFile, expectedError);
}
{
std::string const expectedError = "Invalid master signature";
auto const start = testStart(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = "bad signature";
testFinish(sig, keyFile, expectedError);
}
{
{
// Need to ensure any pending token is gone. Best
// way to do that is to generate one successfully
auto const start = testStart(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.signHex(start);
testFinish(sig, keyFile, noError);
}
std::string const expectedError = "No pending token to finish";
auto const sig = externalKey.sign("foo");
testFinish(sig, keyFile, expectedError);
}
{
auto keys = SigningKeys(
externalKeyType,
externalKey.publicKey(),
std::numeric_limits<std::uint32_t>::max() - 1);
keys.writeToFile(keyFile);
std::string const expectedError =
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.";
BEAST_EXPECT(testStart(keyFile, expectedError).empty());
}
{
// Create the file revoked
auto keys = SigningKeys(externalKeyType, externalKey.publicKey(), 42, true);
keys.writeToFile(keyFile);
std::string const expectedError = "Validator keys have been revoked.";
BEAST_EXPECT(testStart(keyFile, expectedError).empty());
}
}
void
testCreateRevocationExternal()
{
testcase("Create Revocation External");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
// The external key will contain a secret key, and be used
// to simulate the actions of an actual external signing device
// or process. Note that it is const.
SigningKeys const externalKey(KeyType::Ed25519);
std::string const pubKey(toBase58(TokenType::NodePublic, externalKey.publicKey()));
auto testStartRevoke = [this](
path const& keyFile,
std::string const& expectedError,
bool expectRevoked = true) {
std::stringstream capture;
std::stringstream errCapture;
CoutRedirect coutRedirect{capture};
Redirect cerrRedirect{std::cerr, errCapture};
try
{
startRevocation(keyFile);
BEAST_EXPECT(expectedError.empty());
if (expectRevoked)
BEAST_EXPECT(
errCapture.str() ==
"WARNING: Validator keys have already been "
"revoked!\n\n");
else
BEAST_EXPECT(
errCapture.str() == "WARNING: This will revoke your validator keys!\n\n");
return capture.str();
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
return std::string();
};
auto testFinishRevoke =
[this](std::string const& sig, path const& keyFile, std::string const& expectedError) {
try
{
finishRevocation(sig, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
std::string const noError = "";
{
auto const expectedError = "Failed to open key file: " + keyFile.string();
testStartRevoke(keyFile, expectedError);
}
createExternal(pubKey, keyFile);
BEAST_EXPECT(exists(keyFile));
{
auto const start = testStartRevoke(keyFile, noError, false);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.signHex(start);
testFinishRevoke(sig, keyFile, noError);
}
{
auto const start = testStartRevoke(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = [&]() {
auto sigBlob = strUnHex(externalKey.signHex(start));
if (BEAST_EXPECT(sigBlob))
return base64Encode(sigBlob->data(), sigBlob->size());
return base64Encode("fail");
}();
testFinishRevoke(sig, keyFile, noError);
}
{
// keys can be revoked multiple times
auto const start = testStartRevoke(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.signHex(start);
testFinishRevoke(sig, keyFile, noError);
}
{
std::string const expectedError = "Manifest is not properly signed";
auto const start = testStartRevoke(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.sign("foo");
testFinishRevoke(sig, keyFile, expectedError);
}
{
std::string const expectedError = "Invalid master signature";
auto const start = testStartRevoke(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = "bad signature";
testFinishRevoke(sig, keyFile, expectedError);
}
{
// Unlike tokens, which have a random key and a changing sequence,
// revocations are fixed, so as long as a valid signature has been
// generated, it can be reused. Same idea as how a signed revocation
// can be stored and released at any time.
// Generate a revocation successfully
auto const start = testStartRevoke(keyFile, noError);
BEAST_EXPECT(!start.empty());
auto const sig = externalKey.signHex(start);
testFinishRevoke(sig, keyFile, noError);
// Reuse the signature.
testFinishRevoke(sig, keyFile, noError);
}
}
void
testSign()
{
testcase("Sign");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
auto testSign =
[this](std::string const& data, path const& keyFile, std::string const& expectedError) {
try
{
signData(data, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
std::string const data = "data to sign";
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
testSign(data, keyFile, expectedError);
}
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
{
std::string const emptyData = "";
std::string const expectedError = "Syntax error: Must specify data string to sign";
testSign(emptyData, keyFile, expectedError);
}
{
std::string const expectedError = "";
testSign(data, keyFile, expectedError);
}
}
void
testHexSign()
{
testcase("Sign Hex");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
auto testSign =
[this](std::string const& data, path const& keyFile, std::string const& expectedError) {
try
{
signHexData(data, keyFile);
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
std::string const rawdata = "data to sign";
std::string const data = strHex(rawdata);
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
{
std::string const expectedError = "Failed to open key file: " + keyFile.string();
testSign(data, keyFile, expectedError);
}
createKeyFile(keyFile);
BEAST_EXPECT(exists(keyFile));
{
std::string const emptyData = "";
std::string const expectedError = "Syntax error: Must specify data string to sign";
testSign(emptyData, keyFile, expectedError);
}
{
std::string const expectedError = "";
testSign(data, keyFile, expectedError);
}
}
void
testRunCommand()
{
testcase("Run Command");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard g(*this, subdir.string());
path const keyFile = subdir / "validator_keys.json";
auto testCommand = [this](
std::string const& command,
std::vector<std::string> const& args,
path const& keyFile,
std::string const& expectedError) {
try
{
runCommand(command, args, toolOptions(keyFile));
BEAST_EXPECT(expectedError.empty());
}
catch (std::exception const& e)
{
BEAST_EXPECT(e.what() == expectedError);
}
};
std::vector<std::string> const noArgs;
std::vector<std::string> const oneArg = {"some data"};
std::vector<std::string> const oneHexArg = {strHex(oneArg[0])};
std::vector<std::string> const oneDomainArg = {"validator.example.com"};
std::vector<std::string> const twoArgs = {"data", "more data"};
std::string const noError = "";
std::string const argError = "Syntax error: Wrong number of arguments";
{
std::string const command = "unknown";
std::string const expectedError = "Unknown command: " + command;
testCommand(command, noArgs, keyFile, expectedError);
testCommand(command, oneArg, keyFile, expectedError);
testCommand(command, twoArgs, keyFile, expectedError);
}
{
std::string const command = "create_keys";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "create_token";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "set_domain";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneDomainArg, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "attest_domain";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "clear_domain";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "show_manifest";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneArg, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "revoke_keys";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "sign";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneArg, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "sign_hex";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneHexArg, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
// External signing functionality.
std::string const pkArg = [&]() {
SigningKeys const keys = SigningKeys::make_SigningKeys(keyFile);
return toBase58(TokenType::NodePublic, keys.publicKey());
}();
{
// Purposely shadow "keyFile" from the outer context
// to prevent reuse
path const keyFile = subdir / "validator_keys_ext.json";
// For the functions that expect a signature, don't pass in a
// valid signature. This is the error that is returned.
std::string const masterKeyError = "Invalid master signature";
{
std::string const command = "create_external";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, {pkArg}, keyFile, noError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "start_token";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
// One signature finishes a software-signed token, two finish a
// token whose signing key is external.
std::string const command = "finish_token";
std::vector<std::string> const threeArgs = {"a", "b", "c"};
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneArg, keyFile, masterKeyError);
testCommand(command, twoArgs, keyFile, masterKeyError);
testCommand(command, threeArgs, keyFile, argError);
}
{
std::stringstream ignore;
Redirect errRedirect(std::cerr, ignore);
std::string const command = "start_revoke_keys";
testCommand(command, noArgs, keyFile, noError);
testCommand(command, oneArg, keyFile, argError);
testCommand(command, twoArgs, keyFile, argError);
}
{
std::string const command = "finish_revoke_keys";
testCommand(command, noArgs, keyFile, argError);
testCommand(command, oneArg, keyFile, masterKeyError);
testCommand(command, twoArgs, keyFile, argError);
}
}
}
void
testListCommands()
{
testcase("List Commands");
std::stringstream coutCapture;
CoutRedirect coutRedirect{coutCapture};
using namespace boost::filesystem;
path const subdir = "test_key_file";
KeyFileGuard const g(*this, subdir.string());
auto run = [this](
std::string const& command,
std::vector<std::string> const& args,
ToolOptions const& options,
std::string const& expectedError) -> int {
try
{
auto const rc = runCommand(command, args, options);
BEAST_EXPECTS(expectedError.empty(), "expected: " + expectedError);
return rc;
}
catch (std::exception const& e)
{
BEAST_EXPECTS(e.what() == expectedError, e.what());
return -1;
}
};
// The publisher: a key file and a token carrying an ed25519 signing key
ToolOptions publisher;
publisher.keyFile = subdir / "publisher.json";
publisher.tokenKeyType = KeyType::Ed25519;
publisher.tokenFile = subdir / "publisher-token.txt";
publisher.outFile = publisher.tokenFile;
run("create_keys", {}, publisher, "");
run("create_token", {}, publisher, "");
BEAST_EXPECT(exists(*publisher.tokenFile));
publisher.outFile.reset();
// Two validators, each with a token whose manifest goes in the list
std::string validators;
for (int i = 0; i < 2; ++i)
{
SigningKeys keys(KeyType::Ed25519);
auto const token = keys.createValidatorToken(KeyType::Secp256k1);
validators += (i ? ", " : "") + std::string("{\"validation_public_key\": \"") +
strHex(keys.publicKey()) + "\", \"manifest\": \"" + token->manifest + "\"}";
}
auto const now = rippleEpochNow();
path const unsignedList = subdir / "unsigned.json";
{
std::ofstream o(unsignedList.string());
o << "{\"sequence\": 2026091301, \"expiration\": " << now + 3600
<< ", \"validators\": [" << validators << "]}\n";
}
// sign_list needs a token
{
ToolOptions noToken = publisher;
noToken.tokenFile.reset();
run("sign_list", {unsignedList.string()}, noToken, "sign_list needs --token-file");
}
// Sign, then verify with every check on
ToolOptions signer = publisher;
signer.outFile = subdir / "vl.json";
run("sign_list", {unsignedList.string()}, signer, "");
BEAST_EXPECT(exists(*signer.outFile));
ToolOptions verifier;
verifier.keyFile = publisher.keyFile;
verifier.validatorsFile = unsignedList;
verifier.expectedKey = SigningKeys::make_SigningKeys(publisher.keyFile).publicKey();
BEAST_EXPECT(run("verify_list", {signer.outFile->string()}, verifier, "") == 0);
// The wrong expected key fails verification
{
ToolOptions wrong = verifier;
wrong.expectedKey = SigningKeys(KeyType::Ed25519).publicKey();
BEAST_EXPECT(run("verify_list", {signer.outFile->string()}, wrong, "") == 1);
}
// Version 2, appended to itself once
ToolOptions v2 = signer;
v2.listVersion = 2;
v2.outFile = subdir / "vl2.json";
run("sign_list", {unsignedList.string()}, v2, "");
v2.appendFile = v2.outFile;
v2.outFile = subdir / "vl2b.json";
run("sign_list", {unsignedList.string()}, v2, "");
BEAST_EXPECT(run("verify_list", {v2.outFile->string()}, verifier, "") == 0);
// External signing key: the master delegates to a key it never holds,
// then the list is signed in two steps with that key.
SigningKeys const external(KeyType::Ed25519);
auto master = SigningKeys::make_SigningKeys(publisher.keyFile);
auto const toSign = master.startValidatorToken(KeyType::Ed25519, external.publicKey());
BEAST_EXPECT(toSign);
auto const manifest = master.finishExternalToken(
*strUnHex(master.signHex(*toSign)), *strUnHex(external.signHex(*toSign)));
BEAST_EXPECT(manifest);
master.writeToFile(publisher.keyFile);
ToolOptions hardware;
hardware.keyFile = publisher.keyFile;
hardware.manifestFile = subdir / "manifest.txt";
{
std::ofstream o(hardware.manifestFile->string());
o << *manifest << "\n";
}
run("start_sign_list",
{unsignedList.string()},
publisher,
"start_sign_list needs --manifest-file");
coutCapture.str("");
run("start_sign_list", {unsignedList.string()}, hardware, "");
auto bytes = coutCapture.str();
boost::algorithm::trim(bytes);
BEAST_EXPECT(!bytes.empty());
hardware.outFile = subdir / "vl-external.json";
run("finish_sign_list",
{master.signHex(bytes), unsignedList.string()},
hardware,
"The signature does not verify under the manifest's signing key");
run("finish_sign_list", {external.signHex(bytes), unsignedList.string()}, hardware, "");
BEAST_EXPECT(run("verify_list", {hardware.outFile->string()}, verifier, "") == 0);
}
public:
void
run() override
{
getVersionString();
testCreateKeyFile();
testCreateToken();
testCreateRevocation();
testCreateKeyFileExternal();
testCreateTokenExternal();
testCreateRevocationExternal();
testSign();
testHexSign();
testRunCommand();
testListCommands();
}
};
BEAST_DEFINE_TESTSUITE(ValidatorKeysTool, keys, xrpl);
} // namespace tests
} // namespace xrpl