mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 23:38:08 +00:00
feat: vendor validator-keys with external signing and validator-list signing
This commit is contained in:
@@ -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: "*"
|
||||
|
||||
@@ -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
|
||||
|
||||
3
.github/scripts/rename/cmake.sh
vendored
3
.github/scripts/rename/cmake.sh
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
23
src/tools/validator-keys/CMakeLists.txt
Normal file
23
src/tools/validator-keys/CMakeLists.txt
Normal 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
|
||||
)
|
||||
77
src/tools/validator-keys/LICENSE
Normal file
77
src/tools/validator-keys/LICENSE
Normal 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
|
||||
466
src/tools/validator-keys/ListSigning.cpp
Normal file
466
src/tools/validator-keys/ListSigning.cpp
Normal 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
|
||||
149
src/tools/validator-keys/ListSigning.h
Normal file
149
src/tools/validator-keys/ListSigning.h
Normal 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
|
||||
22
src/tools/validator-keys/README.md
Normal file
22
src/tools/validator-keys/README.md
Normal 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)
|
||||
524
src/tools/validator-keys/SigningKeys.cpp
Normal file
524
src/tools/validator-keys/SigningKeys.cpp
Normal 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
|
||||
318
src/tools/validator-keys/SigningKeys.h
Normal file
318
src/tools/validator-keys/SigningKeys.h
Normal 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
|
||||
912
src/tools/validator-keys/ValidatorKeysTool.cpp
Normal file
912
src/tools/validator-keys/ValidatorKeysTool.cpp
Normal 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
|
||||
}
|
||||
97
src/tools/validator-keys/ValidatorKeysTool.h
Normal file
97
src/tools/validator-keys/ValidatorKeysTool.h
Normal 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);
|
||||
226
src/tools/validator-keys/doc/validator-keys-tool-guide.md
Normal file
226
src/tools/validator-keys/doc/validator-keys-tool-guide.md
Normal 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.
|
||||
60
src/tools/validator-keys/test/KeyFileGuard.h
Normal file
60
src/tools/validator-keys/test/KeyFileGuard.h
Normal 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
|
||||
509
src/tools/validator-keys/test/ListSigning_test.cpp
Normal file
509
src/tools/validator-keys/test/ListSigning_test.cpp
Normal 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
|
||||
742
src/tools/validator-keys/test/SigningKeys_test.cpp
Normal file
742
src/tools/validator-keys/test/SigningKeys_test.cpp
Normal 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
|
||||
923
src/tools/validator-keys/test/ValidatorKeysTool_test.cpp
Normal file
923
src/tools/validator-keys/test/ValidatorKeysTool_test.cpp
Normal 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
|
||||
Reference in New Issue
Block a user