#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //------------------------------------------------------------------------------ // 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(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 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 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 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 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 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 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 const& args, ToolOptions const& options) { using namespace std; // Minimum and maximum number of positional arguments per command. static map> 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] [ ...]\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 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 " "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 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 []\n" " Finish generating token with " "external signature(s).\n" " start_revoke_keys Generate a partial revocation " "for external signing.\n" " finish_revoke_keys Finish generating revocation " "with external signature.\n" "Commands for validator lists: \n" " sign_list Sign a list with --token-file.\n" " start_sign_list \n" " Print the bytes to sign with an " "external signing key; needs --manifest-file.\n" " finish_sign_list \n" " Assemble the signed list from an " "external signature; needs --manifest-file.\n" " verify_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(), "Specify the key file.")( "token-key-type", po::value(), "Key type of a token's signing key: secp256k1 (default) or ed25519.")( "signing-key", po::value(), "External signing key a token delegates to (start_token).")( "token-file", po::value(), "File holding a [validator_token] block.")( "manifest-file", po::value(), "File holding a base64 manifest.")( "out", po::value(), "Write the token or signed list to this file.")( "list-version", po::value(), "Signed list version: 1 (default) or 2.")( "append", po::value(), "Version 2 list to add the new blob to.")( "validators", po::value(), "Unsigned list whose validators a published list must carry (verify_list).")( "expected-key", po::value(), "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(), "Command.")( "arguments", po::value>()->default_value(std::vector(), "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() : defaultKeyFile; if (vm.count("token-key-type")) { auto const keyType = xrpl::keyTypeFromString(vm["token-key-type"].as()); if (!keyType) throw std::runtime_error( "Unknown key type: " + vm["token-key-type"].as()); options.tokenKeyType = *keyType; } if (vm.count("signing-key")) options.signingKey = parsePublicKey(vm["signing-key"].as()); if (vm.count("token-file")) options.tokenFile = path(vm["token-file"].as()); if (vm.count("manifest-file")) options.manifestFile = path(vm["manifest-file"].as()); if (vm.count("out")) options.outFile = path(vm["out"].as()); if (vm.count("list-version")) options.listVersion = vm["list-version"].as(); if (vm.count("append")) options.appendFile = path(vm["append"].as()); if (vm.count("validators")) options.validatorsFile = path(vm["validators"].as()); if (vm.count("expected-key")) options.expectedKey = parsePublicKey(vm["expected-key"].as()); return runCommand( vm["command"].as(), vm["arguments"].as>(), options); } catch (std::exception const& e) { std::cerr << e.what() << "\n"; return EXIT_FAILURE; } return EXIT_SUCCESS; // LCOV_EXCL_STOP }