Files
rippled/src/tests/tools/validator-keys/SigningKeys.cpp

530 lines
19 KiB
C++

#include <tools/validator-keys/SigningKeys.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/tokens.h>
#include <xrpl/server/Manifest.h>
#include <gtest/gtest.h>
#include <Fixtures.h>
#include <array>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <map>
#include <string>
#include <utility>
namespace xrpl::tools::test {
namespace {
constexpr std::array<KeyType, 2> kKeyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
constexpr std::uint32_t kMaxSequence = std::numeric_limits<std::uint32_t>::max();
std::string const kBadManifest = "Manifest is not properly signed";
std::string const kRevoked = "Validator keys have been revoked.";
std::string const kExhausted =
"Maximum number of tokens have already been generated.\n"
"Revoke validator keys if previous token has been compromised.";
class SigningKeysTest : public ::testing::Test
{
protected:
TempDir dir_;
std::filesystem::path keyFile_{dir_.file("validator_keys.json")};
void
writeKeyFile(json::Value const& jv)
{
writeFile(keyFile_, jv.toStyledString());
}
std::string
loadError(json::Value const& jv)
{
writeKeyFile(jv);
return errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); });
}
json::Value
keyFileJson()
{
json::Reader reader;
json::Value jv;
reader.parse(readFile(keyFile_), jv);
return jv;
}
[[nodiscard]] std::string
invalidField(std::string const& field) const
{
return "Key file '" + keyFile_.string() + "' contains invalid \"" + field + "\" field";
}
static json::Value
baseKeyFile(SecretKey const& secret)
{
json::Value jv;
jv["key_type"] = "ed25519";
jv["secret_key"] = toBase58(TokenType::NodePrivate, secret);
jv["token_sequence"] = 1;
jv["revoked"] = false;
return jv;
}
};
// The manifest of a token or a revocation, parsed and checked against the
// master key that made it.
Manifest
manifestOf(std::string const& base64, SigningKeys const& keys)
{
auto m = required(deserializeManifest(base64Decode(base64)));
EXPECT_TRUE(m.verify());
EXPECT_EQ(m.masterKey, keys.publicKey());
return m;
}
} // namespace
TEST_F(SigningKeysTest, key_file_round_trip)
{
for (auto const keyType : kKeyTypes)
{
SigningKeys const keys(keyType);
keys.writeToFile(keyFile_);
EXPECT_TRUE(std::filesystem::exists(keyFile_));
EXPECT_FALSE(std::filesystem::exists(keyFile_.string() + ".tmp"));
auto const perms = std::filesystem::status(keyFile_).permissions();
EXPECT_EQ(
perms & (std::filesystem::perms::group_all | std::filesystem::perms::others_all),
std::filesystem::perms::none);
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(keyFile_));
}
// A token, a domain and a pending external token all survive the round trip
SigningKeys keys(KeyType::Ed25519);
keys.domain("validator.example.com");
keys.createToken();
SigningKeys const signer(KeyType::Ed25519);
keys.startToken(KeyType::Ed25519, signer.publicKey());
keys.writeToFile(keyFile_);
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(keyFile_));
// So does a pending token with a generated signing key
SigningKeys other(KeyType::Secp256k1);
SigningKeys const before = other;
other.startToken(KeyType::Secp256k1);
other.writeToFile(keyFile_);
EXPECT_TRUE(other == SigningKeys::makeSigningKeys(keyFile_));
EXPECT_FALSE(other == keys);
EXPECT_FALSE(other == before);
// The same master key with a pending token of the other kind
SigningKeys external = before;
external.startToken(KeyType::Secp256k1, signer.publicKey());
EXPECT_FALSE(other == external);
}
TEST_F(SigningKeysTest, write_to_file_errors)
{
SigningKeys const keys(KeyType::Ed25519);
auto const nested = dir_.file("a/b/c/validator_keys.json");
keys.writeToFile(nested);
EXPECT_TRUE(keys == SigningKeys::makeSigningKeys(nested));
// The parent path is a file
auto const blocked = std::filesystem::path(keyFile_.string() + "/keys.json");
keys.writeToFile(keyFile_);
EXPECT_EQ(
errorOf([&] { keys.writeToFile(blocked); }),
"Cannot create directory: " + blocked.parent_path().string());
// The target is a directory
auto const directory = std::filesystem::path(dir_.file("dir"));
std::filesystem::create_directory(directory);
EXPECT_EQ(
errorOf([&] { keys.writeToFile(directory); }),
"Cannot write key file: " + directory.string());
// The directory cannot be written to
auto const sealed = std::filesystem::path(dir_.file("sealed"));
std::filesystem::create_directory(sealed);
std::filesystem::permissions(
sealed, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec);
auto const inSealed = sealed / "keys.json";
EXPECT_EQ(
errorOf([&] { keys.writeToFile(inSealed); }),
"Cannot write key file: " + inSealed.string());
std::filesystem::permissions(sealed, std::filesystem::perms::owner_all);
// The temporary beside the key file is a symlink
auto const linked = std::filesystem::path(dir_.file("linked.json"));
std::filesystem::create_symlink(dir_.file("elsewhere.json"), linked.string() + ".tmp");
EXPECT_EQ(
errorOf([&] { keys.writeToFile(linked); }),
"Refusing to write through a symlink: " + linked.string() + ".tmp");
}
TEST_F(SigningKeysTest, key_file_fields)
{
EXPECT_EQ(
errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); }),
"Failed to open key file: " + keyFile_.string());
writeFile(keyFile_, "{{}");
EXPECT_EQ(
errorOf([&] { SigningKeys::makeSigningKeys(keyFile_); }),
"Unable to parse json key file: " + keyFile_.string());
json::Value jv;
jv["dummy"] = "field";
for (auto const* field : {"key_type", "secret_key", "token_sequence", "revoked"})
{
EXPECT_EQ(
loadError(jv),
"Key file '" + keyFile_.string() + "' is missing \"" + field + "\" field");
jv[field] = "dummy";
}
EXPECT_EQ(loadError(jv), invalidField("key_type"));
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
jv["key_type"] = "ed25519";
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
jv["token_sequence"] = -1;
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
jv["token_sequence"] = true;
EXPECT_EQ(loadError(jv), invalidField("token_sequence"));
jv["token_sequence"] = json::UInt(kMaxSequence);
EXPECT_EQ(loadError(jv), invalidField("revoked"));
jv["revoked"] = false;
EXPECT_EQ(loadError(jv), invalidField("secret_key"));
jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second);
EXPECT_EQ(loadError(jv), "");
// Optional fields with the wrong type or value
for (auto const* field :
{"domain", "manifest", "pending_token_secret", "pending_signing_key", "pending_key_type"})
{
auto bad = baseKeyFile(kp.second);
bad[field] = 1;
if (field == std::string("pending_key_type"))
{
bad["pending_token_secret"] = toBase58(TokenType::NodePrivate, kp.second);
}
else if (std::string(field).starts_with("pending_"))
{
bad["pending_key_type"] = "ed25519";
}
EXPECT_EQ(loadError(bad), invalidField(field)) << field;
}
for (auto const* field : {"manifest", "pending_token_secret", "pending_signing_key"})
{
auto bad = baseKeyFile(kp.second);
bad[field] = "not valid";
bad["pending_key_type"] = "ed25519";
EXPECT_EQ(loadError(bad), invalidField(field)) << field;
}
{
auto bad = baseKeyFile(kp.second);
bad["manifest"] = "";
EXPECT_EQ(loadError(bad), invalidField("manifest"));
}
{
auto bad = baseKeyFile(kp.second);
bad["domain"] = "-bad.example";
EXPECT_EQ(
loadError(bad), "The domain field must use the '[host.][subdomain.]domain.tld' format");
}
{
// Pending fields need a key type and exclude each other
auto bad = baseKeyFile(kp.second);
bad["pending_token_secret"] = toBase58(TokenType::NodePrivate, kp.second);
EXPECT_EQ(
loadError(bad),
"Key file '" + keyFile_.string() + "' is missing \"pending_key_type\" field");
bad["pending_key_type"] = "dummy";
EXPECT_EQ(loadError(bad), invalidField("pending_key_type"));
bad["pending_key_type"] = "ed25519";
bad["pending_signing_key"] = toBase58(TokenType::NodePublic, kp.first);
EXPECT_EQ(
loadError(bad),
"Key file '" + keyFile_.string() +
"' has both \"pending_token_secret\" and \"pending_signing_key\"");
}
}
TEST_F(SigningKeysTest, external_key_file_fields)
{
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
json::Value jv;
jv["key_type"] = "ed25519";
jv["secret_key"] = "external";
jv["token_sequence"] = 0;
jv["revoked"] = false;
EXPECT_EQ(
loadError(jv), "Key file '" + keyFile_.string() + "' is missing \"public_key\" field");
jv["public_key"] = "dummy public";
EXPECT_EQ(loadError(jv), invalidField("public_key"));
jv["public_key"] = toBase58(TokenType::NodePublic, kp.first);
jv["key_type"] = "secp256k1";
EXPECT_EQ(
loadError(jv),
"Key file '" + keyFile_.string() +
"' has a \"key_type\" that does not match \"public_key\"");
jv["key_type"] = "ed25519";
EXPECT_EQ(loadError(jv), "");
auto const keys = SigningKeys::makeSigningKeys(keyFile_);
EXPECT_FALSE(keys.hasSecret());
EXPECT_EQ(keys.publicKey(), kp.first);
EXPECT_TRUE(keys == SigningKeys(KeyType::Ed25519, kp.first));
}
TEST_F(SigningKeysTest, create_token)
{
for (auto const keyType : kKeyTypes)
{
SigningKeys keys(keyType);
std::uint32_t sequence = 0;
for (auto const tokenKeyType : kKeyTypes)
{
auto const token = keys.createToken(tokenKeyType);
auto const m = manifestOf(token.manifest, keys);
EXPECT_EQ(m.sequence, ++sequence);
EXPECT_EQ(keys.sequence(), sequence);
ASSERT_TRUE(m.signingKey);
EXPECT_EQ(
required(m.signingKey), derivePublicKey(tokenKeyType, token.validationSecret));
EXPECT_EQ(base64Encode(keys.manifest().data(), keys.manifest().size()), token.manifest);
}
}
auto const kp = generateKeyPair(KeyType::Ed25519, randomSeed());
{
SigningKeys keys(KeyType::Ed25519, kp.second, kMaxSequence - 1);
EXPECT_EQ(errorOf([&] { keys.createToken(); }), kExhausted);
}
{
// A key migrated from a publisher whose manifests carried the list
// sequence continues from that sequence.
SigningKeys keys(KeyType::Ed25519, kp.second, 2026091301);
auto const m = manifestOf(keys.createToken(KeyType::Ed25519).manifest, keys);
EXPECT_EQ(m.sequence, 2026091302u);
}
{
SigningKeys keys(KeyType::Ed25519);
keys.revoke();
EXPECT_EQ(errorOf([&] { keys.createToken(); }), kRevoked);
}
{
SigningKeys keys(KeyType::Ed25519, kp.first);
EXPECT_EQ(
errorOf([&] { keys.createToken(); }), "This key file cannot be used to sign tokens.");
EXPECT_EQ(errorOf([&] { keys.revoke(); }), "This key file cannot be used to sign tokens.");
}
}
TEST_F(SigningKeysTest, token_with_domain)
{
SigningKeys keys(KeyType::Ed25519);
keys.domain("validator.example.com");
auto const m = manifestOf(keys.createToken().manifest, keys);
EXPECT_EQ(m.domain, "validator.example.com");
EXPECT_EQ(
keys.attestationData(),
"[domain-attestation-blob:validator.example.com:" +
toBase58(TokenType::NodePublic, keys.publicKey()) + "]");
for (auto const* bad : {"a.b", "-bad.example", "nodots"})
{
EXPECT_EQ(
errorOf([&] { keys.domain(bad); }),
"The domain field must use the '[host.][subdomain.]domain.tld' format")
<< bad;
}
keys.domain("");
EXPECT_TRUE(keys.domain().empty());
}
TEST_F(SigningKeysTest, revoke)
{
for (auto const keyType : kKeyTypes)
{
SigningKeys keys(keyType);
auto const m = manifestOf(keys.revoke(), keys);
EXPECT_TRUE(m.revoked());
EXPECT_FALSE(m.signingKey);
EXPECT_TRUE(keys.revoked());
// Revoking again is allowed
manifestOf(keys.revoke(), keys);
}
}
TEST_F(SigningKeysTest, sign)
{
std::map<KeyType, std::string> const expected{
{KeyType::Ed25519,
"2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F"
"237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939"
"C8F29B0DC759FD712300"},
{KeyType::Secp256k1,
"3045022100F142C27BF83D8D4541C7A4E759DE64A672"
"51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5"
"55438D64A229CD0B4BA2AE33402443B20409"}};
std::string const data = "data to sign";
for (auto const keyType : kKeyTypes)
{
auto const sk = generateSecretKey(keyType, generateSeed("test"));
SigningKeys const keys(keyType, sk, 1);
EXPECT_EQ(keys.sign(data), expected.at(keyType));
EXPECT_EQ(keys.signHex(strHex(data)), expected.at(keyType));
auto const sig = required(strUnHex(keys.sign(data)));
EXPECT_TRUE(verify(keys.publicKey(), makeSlice(data), makeSlice(sig)));
SigningKeys const external(keyType, derivePublicKey(keyType, sk));
EXPECT_EQ(
errorOf([&] { (void)external.sign(data); }), "This key file cannot be used to sign.");
EXPECT_EQ(
errorOf([&] { (void)external.signHex(strHex(data)); }),
"This key file cannot be used to sign.");
}
SigningKeys const keys(KeyType::Ed25519);
EXPECT_EQ(errorOf([&] { (void)keys.signHex("zz"); }), "Could not decode hex string: zz");
}
TEST_F(SigningKeysTest, external_master)
{
for (auto const keyType : kKeyTypes)
{
// The signer stands in for the hardware holding the master key
SigningKeys const signer(keyType);
SigningKeys keys(keyType, signer.publicKey());
std::uint32_t sequence = 0;
for (auto const tokenKeyType : kKeyTypes)
{
auto const data = keys.startToken(tokenKeyType);
keys.writeToFile(keyFile_);
auto fileKeys = SigningKeys::makeSigningKeys(keyFile_);
EXPECT_TRUE(keys == fileKeys);
auto const finished = fileKeys.finishToken(required(strUnHex(signer.signHex(data))));
ASSERT_TRUE(finished.secret);
auto const m = manifestOf(finished.manifest, keys);
EXPECT_EQ(m.sequence, ++sequence);
EXPECT_EQ(fileKeys.sequence(), sequence);
EXPECT_EQ(
required(m.signingKey), derivePublicKey(tokenKeyType, required(finished.secret)));
// A signature over other bytes does not finish the token
EXPECT_EQ(
errorOf([&] { keys.finishToken(required(strUnHex(signer.sign("foo")))); }),
kBadManifest);
EXPECT_EQ(
errorOf(
[&] { keys.finishToken(required(strUnHex(signer.signHex(data))), Blob{}); }),
"The pending token's signing key is in this key file; pass one signature");
keys.finishToken(required(strUnHex(signer.signHex(data))));
}
// Nothing pending
EXPECT_EQ(errorOf([&] { keys.finishToken(Blob{}); }), "No pending token to finish");
// Revocation: the same bytes each time, so a signature can be kept and reused
auto const revocation = keys.startRevoke();
EXPECT_EQ(revocation, keys.startRevoke());
EXPECT_EQ(
errorOf([&] { keys.finishRevoke(required(strUnHex(signer.sign("foo")))); }),
kBadManifest);
EXPECT_FALSE(keys.revoked());
auto const sig = required(strUnHex(signer.signHex(revocation)));
manifestOf(keys.finishRevoke(sig), keys);
EXPECT_TRUE(keys.revoked());
manifestOf(keys.finishRevoke(sig), keys);
EXPECT_EQ(errorOf([&] { keys.startToken(); }), kRevoked);
EXPECT_EQ(errorOf([&] { keys.finishToken(sig); }), kRevoked);
}
SigningKeys exhausted(
KeyType::Ed25519, SigningKeys(KeyType::Ed25519).publicKey(), kMaxSequence - 1);
EXPECT_EQ(errorOf([&] { exhausted.startToken(); }), kExhausted);
}
TEST_F(SigningKeysTest, external_signing_key)
{
// The master key is in software here; the signing key is held elsewhere.
SigningKeys const signer(KeyType::Ed25519);
SigningKeys keys(KeyType::Ed25519);
EXPECT_EQ(
errorOf([&] { keys.startToken(KeyType::Ed25519, keys.publicKey()); }),
"The signing key must differ from the master key");
auto const data = keys.startToken(KeyType::Ed25519, signer.publicKey());
keys.writeToFile(keyFile_);
auto fileKeys = SigningKeys::makeSigningKeys(keyFile_);
EXPECT_TRUE(keys == fileKeys);
auto const masterSig = required(strUnHex(keys.signHex(data)));
auto const signingSig = required(strUnHex(signer.signHex(data)));
EXPECT_EQ(
errorOf([&] { fileKeys.finishToken(masterSig); }),
"The pending token's signing key is external; pass its signature too");
EXPECT_EQ(errorOf([&] { fileKeys.finishToken(masterSig, masterSig); }), kBadManifest);
auto const finished = fileKeys.finishToken(masterSig, signingSig);
EXPECT_FALSE(finished.secret);
auto const m = manifestOf(finished.manifest, keys);
EXPECT_EQ(m.sequence, 1u);
EXPECT_EQ(required(m.signingKey), signer.publicKey());
EXPECT_EQ(fileKeys.sequence(), 1u);
EXPECT_EQ(
errorOf([&] { fileKeys.finishToken(masterSig, signingSig); }),
"No pending token to finish");
}
TEST_F(SigningKeysTest, stored_manifest_is_checked)
{
SigningKeys keys(KeyType::Ed25519);
keys.createToken(KeyType::Ed25519);
keys.writeToFile(keyFile_);
auto const good = keyFileJson();
EXPECT_EQ(loadError(good), "");
// A token manifest on revoked keys
auto jv = good;
jv["revoked"] = true;
EXPECT_EQ(loadError(jv), kBadManifest);
// A revocation manifest on keys that are not revoked
SigningKeys revoked(KeyType::Ed25519);
revoked.revoke();
jv = good;
jv["manifest"] = strHex(makeSlice(revoked.manifest()));
EXPECT_EQ(loadError(jv), kBadManifest);
// A manifest of another key
jv = good;
jv["secret_key"] =
toBase58(TokenType::NodePrivate, generateKeyPair(KeyType::Ed25519, randomSeed()).second);
EXPECT_EQ(loadError(jv), kBadManifest);
}
} // namespace xrpl::tools::test