From 8291db385bce3853cbdfd012e1f959dd2ac831a7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 12 Mar 2024 20:01:22 -0400 Subject: [PATCH] Add support for external signing * Also add a "sign_hex" command to sign non-printable data. * Include unit tests for external signing support functions. * Because this is a significant change, and this project is not updated often, increment the version number. * Resolves #48 --- src/ValidatorKeys.cpp | 313 +++++++++++++++-- src/ValidatorKeys.h | 83 ++++- src/ValidatorKeysTool.cpp | 240 ++++++++++++- src/ValidatorKeysTool.h | 24 ++ src/test/ValidatorKeysTool_test.cpp | 523 +++++++++++++++++++++++++++- src/test/ValidatorKeys_test.cpp | 422 +++++++++++++++++++++- 6 files changed, 1541 insertions(+), 64 deletions(-) diff --git a/src/ValidatorKeys.cpp b/src/ValidatorKeys.cpp index 0cd8a26..0ca3d6f 100644 --- a/src/ValidatorKeys.cpp +++ b/src/ValidatorKeys.cpp @@ -25,8 +25,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -45,9 +47,9 @@ ValidatorToken::toString() const ValidatorKeys::ValidatorKeys(KeyType const& keyType) : keyType_(keyType) + , keys_(generateKeyPair(keyType_, randomSeed())) , tokenSequence_(0) , revoked_(false) - , keys_(generateKeyPair(keyType_, randomSeed())) { } @@ -57,9 +59,21 @@ ValidatorKeys::ValidatorKeys( std::uint32_t tokenSequence, bool revoked) : keyType_(keyType) + , keys_({derivePublicKey(keyType_, secretKey), secretKey}) + , tokenSequence_(tokenSequence) + , revoked_(revoked) +{ +} + +ValidatorKeys::ValidatorKeys( + KeyType const& keyType, + PublicKey const& publicKey, + std::uint32_t tokenSequence, + bool revoked) + : keyType_(keyType) + , keys_(publicKey) , tokenSequence_(tokenSequence) , revoked_(revoked) - , keys_({derivePublicKey(keyType_, secretKey), secretKey}) { } @@ -105,13 +119,33 @@ ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile) auto const secret = parseBase58( TokenType::NodePrivate, jKeys["secret_key"].asString()); - if (!secret) - { - throw std::runtime_error( - "Key file '" + keyFile.string() + - "' contains invalid \"secret_key\" field: " + - jKeys["secret_key"].toStyledString()); - } + auto const pubKey = [&]() -> std::optional { + 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( + TokenType::NodePublic, jKeys["public_key"].asString()); + if (!pubKey) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"public_key\" field: " + + jKeys["public_key"].toStyledString()); + return pubKey; + } + else if (!secret) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"secret_key\" field: " + + jKeys["secret_key"].toStyledString()); + } + return std::nullopt; + }(); std::uint32_t tokenSequence; try @@ -135,8 +169,17 @@ ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile) "' contains invalid \"revoked\" field: " + jKeys["revoked"].toStyledString()); - ValidatorKeys vk( - *keyType, *secret, tokenSequence, jKeys["revoked"].asBool()); + ValidatorKeys vk = [&]() { + if (secret) + return ValidatorKeys( + *keyType, *secret, tokenSequence, jKeys["revoked"].asBool()); + else + { + assert(*keyType == *publicKeyType(*pubKey)); + return ValidatorKeys( + *keyType, *pubKey, tokenSequence, jKeys["revoked"].asBool()); + } + }(); if (jKeys.isMember("domain")) { @@ -170,6 +213,40 @@ ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile) std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_)); } + if (jKeys.isMember("pending_token_secret")) + { + if (!jKeys["pending_token_secret"].isString()) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"pending_token_secret\" field: " + + jKeys["pending_token_secret"].toStyledString()); + + vk.pendingTokenSecret_ = parseBase58( + TokenType::NodePrivate, jKeys["pending_token_secret"].asString()); + + if (!vk.pendingTokenSecret_) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"pending_token_secret\" field: " + + jKeys["pending_manifest"].toStyledString()); + } + } + + if (jKeys.isMember("pending_key_type")) + { + auto const keyType = + keyTypeFromString(jKeys["pending_key_type"].asString()); + if (!keyType) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"pending_key_type\" field: " + + jKeys["key_type"].toStyledString()); + } + vk.pendingKeyType_ = keyType; + } + return vk; } @@ -181,13 +258,20 @@ ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const Json::Value jv; jv["key_type"] = to_string(keyType_); jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey); - jv["secret_key"] = toBase58(TokenType::NodePrivate, keys_.secretKey); + 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 (pendingKeyType_) + jv["pending_key_type"] = to_string(*pendingKeyType_); if (!keyFile.parent_path().empty()) { @@ -207,6 +291,57 @@ ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const o << jv.toStyledString(); } +void +ValidatorKeys::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(st, sfSigningPubKey); + if (revoked() && tpk) + fail(); + + if (!revoked() && (!tpk || !verify(st, HashPrefix::manifest, *tpk))) + fail(); + + auto const pk = get(st, sfPublicKey); + if (!pk || !verify(st, HashPrefix::manifest, *pk, sfMasterSignature)) + fail(); +} + +// Helper functions +[[nodiscard]] STObject +generatePartialManifest( + 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::max(); + st[sfPublicKey] = masterPubKey; + + return st; +} + boost::optional ValidatorKeys::createValidatorToken(KeyType const& keyType) { @@ -214,29 +349,79 @@ ValidatorKeys::createValidatorToken(KeyType const& keyType) std::numeric_limits::max() - 1 <= tokenSequence_) return boost::none; + // Invalid secret key + 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(sfGeneric); - st[sfSequence] = tokenSequence_; - st[sfPublicKey] = keys_.publicKey; - st[sfSigningPubKey] = tokenPublic; - - if (!domain_.empty()) - st[sfDomain] = makeSlice(domain_); + STObject st = generatePartialManifest( + tokenSequence_, keys_.publicKey, tokenPublic, domain_); ripple::sign(st, HashPrefix::manifest, keyType, tokenSecret); ripple::sign( - st, HashPrefix::manifest, keyType_, keys_.secretKey, sfMasterSignature); + st, + HashPrefix::manifest, + keyType_, + *keys_.secretKey, + sfMasterSignature); + + setManifest(st); + + return ValidatorToken{ + ripple::base64_encode(manifest_.data(), manifest_.size()), tokenSecret}; +} + +boost::optional +ValidatorKeys::startValidatorToken(KeyType const& keyType) const +{ + if (revoked() || + std::numeric_limits::max() - 1 <= tokenSequence_) + return boost::none; + + auto const tokenSecret = generateSecretKey(keyType, randomSeed()); + auto const tokenPublic = derivePublicKey(keyType, tokenSecret); + + // Generate the next manifest with the next sequence number, but + // don't update until it's been signed + STObject st = generatePartialManifest( + tokenSequence_ + 1, keys_.publicKey, tokenPublic, domain_); Serializer s; - st.add(s); + s.add32(HashPrefix::manifest); + st.addWithoutSigningFields(s); - manifest_.clear(); - manifest_.reserve(s.size()); - std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); + pendingTokenSecret_ = tokenSecret; + pendingKeyType_ = keyType; + + return strHex(s.peekData()); +} + +boost::optional +ValidatorKeys::finishToken(Blob const& masterSig) +{ + if (revoked()) + return boost::none; + + 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_); + + ripple::sign(st, HashPrefix::manifest, *pendingKeyType_, tokenSecret); + st[sfMasterSignature] = makeSlice(masterSig); + + setManifest(st); return ValidatorToken{ ripple::base64_encode(manifest_.data(), manifest_.size()), tokenSecret}; @@ -245,15 +430,61 @@ ValidatorKeys::createValidatorToken(KeyType const& keyType) std::string ValidatorKeys::revoke() { + // Invalid secret key + if (!keys_.secretKey) + throw std::runtime_error( + "This key file cannot be used to sign tokens."); + revoked_ = true; - STObject st(sfGeneric); - st[sfSequence] = std::numeric_limits::max(); - st[sfPublicKey] = keys_.publicKey; + STObject st = generatePartialRevocation(keys_.publicKey); ripple::sign( - st, HashPrefix::manifest, keyType_, keys_.secretKey, sfMasterSignature); + st, + HashPrefix::manifest, + keyType_, + *keys_.secretKey, + sfMasterSignature); + + setManifest(st); + + return ripple::base64_encode(manifest_.data(), manifest_.size()); +} + +std::string +ValidatorKeys::startRevoke() const +{ + // Generate the revocation manifest, but + // don't update until it's been signed + STObject st = generatePartialRevocation(keys_.publicKey); + Serializer s; + s.add32(HashPrefix::manifest); + st.addWithoutSigningFields(s); + + pendingTokenSecret_.reset(); + pendingKeyType_.reset(); + + return strHex(s.peekData()); +} + +std::string +ValidatorKeys::finishRevoke(Blob const& masterSig) +{ + revoked_ = true; + + STObject st = generatePartialRevocation(keys_.publicKey); + + st[sfMasterSignature] = makeSlice(masterSig); + + setManifest(st); + + return ripple::base64_encode(manifest_.data(), manifest_.size()); +} + +void +ValidatorKeys::setManifest(STObject const& st) +{ Serializer s; st.add(s); @@ -261,14 +492,36 @@ ValidatorKeys::revoke() manifest_.reserve(s.size()); std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); - return ripple::base64_encode(manifest_.data(), manifest_.size()); + verifyManifest(); + + pendingTokenSecret_.reset(); + pendingKeyType_.reset(); } std::string ValidatorKeys::sign(std::string const& data) const { + // Invalid secret key + if (!keys_.secretKey) + throw std::runtime_error("This key file cannot be used to sign."); + + return strHex( + ripple::sign(keys_.publicKey, *keys_.secretKey, makeSlice(data))); +} + +std::string +ValidatorKeys::signHex(std::string data) const +{ + // Invalid secret key + 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( - ripple::sign(keys_.publicKey, keys_.secretKey, makeSlice(data))); + ripple::sign(keys_.publicKey, *keys_.secretKey, makeSlice(*blob))); } void diff --git a/src/ValidatorKeys.h b/src/ValidatorKeys.h index 327fdab..044efbf 100644 --- a/src/ValidatorKeys.h +++ b/src/ValidatorKeys.h @@ -46,26 +46,35 @@ struct ValidatorToken class ValidatorKeys { private: - KeyType keyType_; - // struct used to contain both public and secret keys struct Keys { PublicKey publicKey; - SecretKey secretKey; + // An unseated secretKey indicates that this object requires external + // signing + std::optional secretKey; Keys() = delete; - Keys(std::pair p) + Keys(std::pair 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 manifest_; std::uint32_t tokenSequence_; bool revoked_; std::string domain_; - Keys keys_; + // The pending fields are mutable so they can be updated + // in const functions without risking updating anything else. + // This may not be the best way to do this. + mutable std::optional pendingTokenSecret_; + mutable std::optional pendingKeyType_; public: explicit ValidatorKeys(KeyType const& keyType); @@ -73,7 +82,17 @@ class ValidatorKeys ValidatorKeys( KeyType const& keyType, SecretKey const& secretKey, - std::uint32_t sequence, + std::uint32_t tokenSequence, + bool revoked = false); + + /** Special case: Create only with a PublicKey, which implies + that the SecretKey is stored and used externally. The file + will be written with "secret_key: external" + */ + ValidatorKeys( + KeyType const& keyType, + PublicKey const& publicKey, + std::uint32_t tokenSequence = 0, bool revoked = false); /** Returns ValidatorKeys constructed from JSON file @@ -88,7 +107,7 @@ class ValidatorKeys ~ValidatorKeys() = default; ValidatorKeys(ValidatorKeys const&) = default; ValidatorKeys& - operator=(ValidatorKeys const&) = default; + operator=(ValidatorKeys const&) = delete; inline bool operator==(ValidatorKeys const& rhs) const @@ -117,6 +136,20 @@ class ValidatorKeys boost::optional createValidatorToken(KeyType const& keyType = KeyType::secp256k1); + /** Returns partial validator token for current sequence + + @param keyType Key type for the token keys + */ + boost::optional + startValidatorToken(KeyType const& keyType = KeyType::secp256k1) const; + + /** Returns validator token for current sequence + + @param masterSig Master signature + */ + boost::optional + finishToken(Blob const& masterSig); + /** Revokes validator keys @return base64-encoded key revocation @@ -124,6 +157,20 @@ class ValidatorKeys std::string revoke(); + /** Returns partial revocation token + + @param keyType Key type for the token keys + */ + std::string + startRevoke() const; + + /** Returns full revocation token + + @param masterSig Master signature + */ + std::string + finishRevoke(Blob const& masterSig); + /** Signs string with validator key @papam data String to sign @@ -133,6 +180,15 @@ class ValidatorKeys std::string sign(std::string const& data) const; + /** Signs hex-encoded string with validator key + + @papam data Hex string to sign. Will be decoded to raw bytes for signing. + + @return hex-encoded signature + */ + std::string + signHex(std::string data) const; + /** Returns the public key. */ PublicKey const& publicKey() const @@ -148,7 +204,7 @@ class ValidatorKeys } /** Returns the domain associated with this key, if any */ - std::string + std::string const& domain() const { return domain_; @@ -158,10 +214,17 @@ class ValidatorKeys void domain(std::string d); + // Throws if the manifest is malformed or not signed correctly. + void + verifyManifest() const; + /** Returns the last manifest we generated for this domain, if available. */ std::vector manifest() const { + if (!manifest_.empty()) + verifyManifest(); + return manifest_; } @@ -171,6 +234,10 @@ class ValidatorKeys { return tokenSequence_; } + +private: + void + setManifest(STObject const& st); }; } // namespace ripple diff --git a/src/ValidatorKeysTool.cpp b/src/ValidatorKeysTool.cpp index a12daea..c296558 100644 --- a/src/ValidatorKeysTool.cpp +++ b/src/ValidatorKeysTool.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -31,16 +32,12 @@ #include #include -#ifdef BOOST_MSVC -#include -#endif - //------------------------------------------------------------------------------ // 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.3.2" + "0.4.0" #if defined(DEBUG) || defined(SANITIZER) "+" @@ -86,6 +83,46 @@ createKeyFile(boost::filesystem::path const& keyFile) << "\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 ripple; + + if (exists(keyFile)) + throw std::runtime_error( + "Refusing to overwrite existing key file: " + keyFile.string()); + + auto const pkInfo = [&]() { + if (auto const unBase58 = + parseBase58(TokenType::NodePublic, data)) + // parseBase58 checks but does not return the key type, so it's safe + // to dereference the function result. + return std::make_pair(*publicKeyType(*unBase58), *unBase58); + + if (auto const unHex = strUnHex(data)) + { + auto const slice = makeSlice(*unHex); + if (auto const pkType = publicKeyType(slice)) + return std::make_pair(*pkType, PublicKey(slice)); + } + + { + auto const unBase64 = base64_decode(data); + auto const slice = makeSlice(unBase64); + if (auto const pkType = publicKeyType(slice)) + return std::make_pair(*pkType, PublicKey(slice)); + } + + throw std::runtime_error("Unable to parse public key: " + data); + }(); + + ValidatorKeys const keys(pkInfo.first, pkInfo.second); + 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(boost::filesystem::path const& keyFile) { @@ -120,6 +157,92 @@ createToken(boost::filesystem::path const& keyFile) std::cout << std::endl; } +void +startToken(boost::filesystem::path const& keyFile) +{ + using namespace ripple; + + auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); + + if (keys.revoked()) + throw std::runtime_error("Validator keys have been revoked."); + + auto const token = keys.startValidatorToken(); + + 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(keyFile); + + std::cout << *token << std::endl; + + std::cout << std::endl; +} + +/// Master signature input can be provided as hex- or base64-encoded. There is +/// no structural way to check that it is valid other than trying to use it, so +/// if the decoding succeeds, proceed. +ripple::Blob +decodeMasterSignature(std::string const& data) +{ + using namespace ripple; + if (auto const unHex = strUnHex(data)) + { + return *unHex; + } + + // base64_decode will decode as far as it can, and return partial data if + // the input is not valid. To ensure the input is valid, encode the result + // and check that they match. This is not the fastest possible way to check, + // but this app runs in human time scales, so it's ok. + if (auto const unBase64 = base64_decode(data); + base64_encode(unBase64) == data) + { + return Blob(unBase64.begin(), unBase64.end()); + } + + throw std::runtime_error("Invalid master signature"); +} + +void +finishToken(std::string const& data, boost::filesystem::path const& keyFile) +{ + using namespace ripple; + + auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); + + if (keys.revoked()) + throw std::runtime_error("Validator keys have been revoked."); + + auto const masterSig = decodeMasterSignature(data); + + 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(keyFile); + + std::cout + << "Update rippled.cfg file with these values and restart rippled:\n\n"; + std::cout << "# validator public key: " + << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; + std::cout << "[validator_token]\n"; + + auto const tokenStr = token->toString(); + auto const len = 72; + for (auto i = 0; i < tokenStr.size(); i += len) + std::cout << tokenStr.substr(i, len) << std::endl; + + std::cout << std::endl; +} + void createRevocation(boost::filesystem::path const& keyFile) { @@ -150,6 +273,62 @@ createRevocation(boost::filesystem::path const& keyFile) std::cout << std::endl; } +void +startRevocation(boost::filesystem::path const& keyFile) +{ + using namespace ripple; + + auto keys = ValidatorKeys::make_ValidatorKeys(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 ripple; + + auto keys = ValidatorKeys::make_ValidatorKeys(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 = decodeMasterSignature(data); + + auto const revocation = keys.finishRevoke(masterSig); + + // Update key file with new token sequence + keys.writeToFile(keyFile); + + std::cout + << "Update rippled.cfg file with these values and restart rippled:\n\n"; + std::cout << "# validator public key: " + << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; + std::cout << "[validator_key_revocation]\n"; + + auto const len = 72; + for (auto i = 0; i < revocation.size(); i += len) + std::cout << revocation.substr(i, len) << std::endl; + + std::cout << std::endl; +} + void attestDomain(ripple::ValidatorKeys const& keys) { @@ -261,6 +440,24 @@ signData(std::string const& data, boost::filesystem::path const& keyFile) std::cout << std::endl; } +void +signHexData(std::string const& data, boost::filesystem::path const& keyFile) +{ + using namespace ripple; + + if (data.empty()) + throw std::runtime_error( + "Syntax error: Must specify data string to sign"); + + auto keys = ValidatorKeys::make_ValidatorKeys(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, @@ -313,6 +510,12 @@ runCommand( {"attest_domain", 0}, {"show_manifest", 1}, {"sign", 1}, + {"sign_hex", 1}, + {"create_external", 1}, + {"start_token", 0}, + {"finish_token", 1}, + {"start_revoke_keys", 0}, + {"finish_revoke_keys", 1}, }; auto const iArgs = commandArgs.find(command); @@ -337,8 +540,20 @@ runCommand( 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(keyFile); + else if (command == "finish_token") + finishToken(args[0], keyFile); + else if (command == "start_revoke_keys") + startRevocation(keyFile); + else if (command == "finish_revoke_keys") + finishRevocation(args[0], keyFile); return 0; } @@ -369,6 +584,8 @@ printHelp(const boost::program_options::options_description& desc) " revoke_keys Revoke validator keys.\n" " sign Sign string with validator " "key.\n" + " sign_hex Decode and sign hex string with " + "validator key.\n" " show_manifest [hex|base64] Displays the last generated " "manifest\n" " set_domain Associate a domain with the " @@ -376,7 +593,18 @@ printHelp(const boost::program_options::options_description& desc) " clear_domain Disassociate a domain from a " "validator key.\n" " attest_domain Produce the attestation string " - "for a domain.\n"; + "for a domain.\n" + "Commands for signing externally: \n" + " create_external Generate validator keys without " + "a secret.\n" + " start_token Generate a partial token for " + "external signing.\n" + " finish_token Finish generating token with " + "external signature.\n" + " start_revoke_keys Generate a partial revocation " + "for external signing.\n" + " finish_revoke_keys Finish generating revocation " + "with external signature.\n"; } // LCOV_EXCL_STOP diff --git a/src/ValidatorKeysTool.h b/src/ValidatorKeysTool.h index d69ff99..244f986 100644 --- a/src/ValidatorKeysTool.h +++ b/src/ValidatorKeysTool.h @@ -39,9 +39,33 @@ createToken(boost::filesystem::path const& keyFile); void createRevocation(boost::filesystem::path const& keyFile); +/*****************************************/ +/* External signing support */ +void +createExternal(std::string const& data, boost::filesystem::path const& keyFile); + +void +startToken(boost::filesystem::path const& keyFile); + +void +finishToken(std::string const& data, boost::filesystem::path const& keyFile); + +void +startRevocation(boost::filesystem::path const& keyFile); + +void +finishRevocation( + std::string const& data, + boost::filesystem::path const& keyFile); + +/*****************************************/ + 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, diff --git a/src/test/ValidatorKeysTool_test.cpp b/src/test/ValidatorKeysTool_test.cpp index 8ff3aa3..261d352 100644 --- a/src/test/ValidatorKeysTool_test.cpp +++ b/src/test/ValidatorKeysTool_test.cpp @@ -17,11 +17,13 @@ */ //============================================================================== -#include #include #include #include +#include +#include + namespace ripple { namespace tests { @@ -29,24 +31,38 @@ namespace tests { class ValidatorKeysTool_test : public beast::unit_test::suite { private: - // Allow cout to be redirected. Destructor restores old cout streambuf. - class CoutRedirect + // Allow a stream to be redirected. Destructor restores old streambuf. + class Redirect { public: - CoutRedirect(std::stringstream& sStream) - : old_(std::cout.rdbuf(sStream.rdbuf())) + Redirect(std::ostream& stream, std::stringstream& sStream) + : stream_(stream), old_(stream_.rdbuf(sStream.rdbuf())) { } - ~CoutRedirect() + virtual ~Redirect() { - std::cout.rdbuf(old_); + 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() { @@ -70,6 +86,7 @@ class ValidatorKeysTool_test : public beast::unit_test::suite try { createKeyFile(keyFile); + fail(); } catch (std::exception const& e) { @@ -159,6 +176,7 @@ class ValidatorKeysTool_test : public beast::unit_test::suite try { createRevocation(keyFile); + fail(); } catch (std::runtime_error& e) { @@ -173,6 +191,360 @@ class ValidatorKeysTool_test : public beast::unit_test::suite 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. + ValidatorKeys 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(base64_encode(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; + ValidatorKeys 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(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, 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 base64_encode(sigBlob->data(), sigBlob->size()); + return base64_encode("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 = ValidatorKeys( + externalKeyType, + externalKey.publicKey(), + std::numeric_limits::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 = ValidatorKeys( + 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. + ValidatorKeys 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 base64_encode(sigBlob->data(), sigBlob->size()); + return base64_encode("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() { @@ -225,6 +597,59 @@ class ValidatorKeysTool_test : public beast::unit_test::suite } } + 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() { @@ -257,6 +682,8 @@ class ValidatorKeysTool_test : public beast::unit_test::suite std::vector const noArgs; std::vector const oneArg = {"some data"}; + std::vector const oneHexArg = {strHex(oneArg[0])}; + std::vector const oneDomainArg = {"validator.example.com"}; std::vector const twoArgs = {"data", "more data"}; std::string const noError = ""; std::string const argError = "Syntax error: Wrong number of arguments"; @@ -279,6 +706,30 @@ class ValidatorKeysTool_test : public beast::unit_test::suite 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); @@ -291,6 +742,60 @@ class ValidatorKeysTool_test : public beast::unit_test::suite 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 = [&]() { + ValidatorKeys const keys = + ValidatorKeys::make_ValidatorKeys(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); + } + { + std::string const command = "finish_token"; + testCommand(command, noArgs, keyFile, argError); + testCommand(command, oneArg, keyFile, masterKeyError); + testCommand(command, twoArgs, 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); + } + } } public: @@ -302,7 +807,11 @@ class ValidatorKeysTool_test : public beast::unit_test::suite testCreateKeyFile(); testCreateToken(); testCreateRevocation(); + testCreateKeyFileExternal(); + testCreateTokenExternal(); + testCreateRevocationExternal(); testSign(); + testHexSign(); testRunCommand(); } }; diff --git a/src/test/ValidatorKeys_test.cpp b/src/test/ValidatorKeys_test.cpp index 0352a7d..42fb274 100644 --- a/src/test/ValidatorKeys_test.cpp +++ b/src/test/ValidatorKeys_test.cpp @@ -88,6 +88,7 @@ class ValidatorKeys_test : public beast::unit_test::suite try { ValidatorKeys::make_ValidatorKeys(keyFile); + fail(); } catch (std::runtime_error& e) { @@ -107,6 +108,7 @@ class ValidatorKeys_test : public beast::unit_test::suite try { ValidatorKeys::make_ValidatorKeys(keyFile); + fail(); } catch (std::runtime_error& e) { @@ -222,6 +224,15 @@ class ValidatorKeys_test : public beast::unit_test::suite HashPrefix::manifest, keys.publicKey(), sfMasterSignature)); + + try + { + keys.verifyManifest(); + } + catch (std::exception const& e) + { + fail(e.what()); + } } } @@ -262,13 +273,26 @@ class ValidatorKeys_test : public beast::unit_test::suite 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 - testSign() + signWorker( + std::function modifyFunc, + std::function + signFunc) { - testcase("Sign"); + std::string const rawdata = "data to sign"; + std::string const data = modifyFunc(rawdata); std::map expected( {{KeyType::ed25519, @@ -280,24 +304,59 @@ class ValidatorKeys_test : public beast::unit_test::suite "51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5" "55438D64A229CD0B4BA2AE33402443B20409"}}); - std::string const data = "data to sign"; - for (auto const keyType : keyTypes) { auto const sk = generateSecretKey(keyType, generateSeed("test")); ValidatorKeys keys(keyType, sk, 1); - auto const signature = keys.sign(data); + { + ValidatorKeys 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(data), makeSlice(*ret))); + 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() { @@ -316,15 +375,20 @@ class ValidatorKeys_test : public beast::unit_test::suite keys.writeToFile(keyFile); BEAST_EXPECT(exists(keyFile)); - auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); - BEAST_EXPECT(keys == fileKeys); + { + auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); - // Overwrite file with new sequence - keys.createValidatorToken(KeyType::secp256k1); - keys.writeToFile(keyFile); + // Overwrite file with new sequence + keys.createValidatorToken(KeyType::secp256k1); + keys.writeToFile(keyFile); + } - fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); - BEAST_EXPECT(keys == fileKeys); + { + auto const fileKeys = + ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); + } } { // Write to key file in current relative directory @@ -366,6 +430,7 @@ class ValidatorKeys_test : public beast::unit_test::suite try { keys.writeToFile(badKeyFile); + fail(); } catch (std::runtime_error& e) { @@ -382,6 +447,7 @@ class ValidatorKeys_test : public beast::unit_test::suite try { keys.writeToFile(conflictingPath); + fail(); } catch (std::runtime_error& e) { @@ -391,6 +457,330 @@ class ValidatorKeys_test : public beast::unit_test::suite } } + //////////////////////////////////////////// + // Tests related to using external keys + // + // These tests will use two ValidatorKeys 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) + { + ValidatorKeys const externalKeys(keyType); + + KeyFileGuard const g(*this, subdir.string()); + + externalKeys.writeToFile(externalKeyFile); + BEAST_EXPECT(exists(externalKeyFile)); + + ValidatorKeys const keys(keyType, externalKeys.publicKey()); + keys.writeToFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + auto const keys2 = ValidatorKeys::make_ValidatorKeys(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); + + ValidatorKeys 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::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) + { + ValidatorKeys const externalKeys(keyType); + ValidatorKeys 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->secretKey); + + STObject st(sfGeneric); + auto const manifest = ripple::base64_decode(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(st, sfSigningPubKey); + BEAST_EXPECT(tpk); + BEAST_EXPECT(*tpk == tokenPublicKey); + BEAST_EXPECT(verify(st, HashPrefix::manifest, tokenPublicKey)); + + 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()); + } + } + } + + auto const keyType = KeyType::ed25519; + auto const kp = generateKeyPair(keyType, randomSeed()); + + { + // The next sequence is the special "revoked" value + auto keys = ValidatorKeys( + keyType, + kp.first, + std::numeric_limits::max() - 1); + + BEAST_EXPECT(!keys.startValidatorToken(keyType)); + } + + { + // Key is revoked + auto keys = ValidatorKeys( + keyType, kp.first, std::numeric_limits::max()); + + BEAST_EXPECT(!keys.startValidatorToken(keyType)); + } + } + + void + testExternalRevoke() + { + testcase("External Revoke"); + + using namespace std::string_literals; + + for (auto const keyType : keyTypes) + { + ValidatorKeys const externalKeys(keyType); + ValidatorKeys 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 = ripple::base64_decode(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::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; + ValidatorKeys const externalKeys(keyType); + ValidatorKeys 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 { + auto fileKeys = ValidatorKeys::make_ValidatorKeys(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 = ValidatorKeys::make_ValidatorKeys(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 = + ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); + } + } + } + public: void run() override @@ -399,7 +789,13 @@ class ValidatorKeys_test : public beast::unit_test::suite testCreateValidatorToken(); testRevoke(); testSign(); + testSignHex(); testWriteToFile(); + // External + testExternalMakeValidatorKeys(); + testExternalCreateValidatorToken(); + testExternalRevoke(); + testExternalWriteToFile(); } };