├── .clang-format ├── .git-blame-ignore-revs ├── .github └── workflows │ └── clang-format.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── RELEASENOTES.md ├── cmake ├── KeysCov.cmake ├── KeysInterface.cmake └── KeysSanity.cmake ├── conanfile.txt ├── doc └── validator-keys-tool-guide.md └── src ├── ValidatorKeys.cpp ├── ValidatorKeys.h ├── ValidatorKeysTool.cpp ├── ValidatorKeysTool.h └── test ├── KeyFileGuard.h ├── ValidatorKeysTool_test.cpp └── ValidatorKeys_test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: AlwaysBreak 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlinesLeft: true 8 | AlignOperands: false 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: false 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: false 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterReturnType: All 17 | AlwaysBreakBeforeMultilineStrings: true 18 | AlwaysBreakTemplateDeclarations: true 19 | BinPackArguments: false 20 | BinPackParameters: false 21 | BraceWrapping: 22 | AfterClass: true 23 | AfterControlStatement: true 24 | AfterEnum: false 25 | AfterFunction: true 26 | AfterNamespace: false 27 | AfterObjCDeclaration: true 28 | AfterStruct: true 29 | AfterUnion: true 30 | BeforeCatch: true 31 | BeforeElse: true 32 | IndentBraces: false 33 | BreakBeforeBinaryOperators: false 34 | BreakBeforeBraces: Custom 35 | BreakBeforeTernaryOperators: true 36 | BreakConstructorInitializersBeforeComma: true 37 | ColumnLimit: 80 38 | CommentPragmas: '^ IWYU pragma:' 39 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 40 | ConstructorInitializerIndentWidth: 4 41 | ContinuationIndentWidth: 4 42 | Cpp11BracedListStyle: true 43 | DerivePointerAlignment: false 44 | DisableFormat: false 45 | ExperimentalAutoDetectBinPacking: false 46 | ForEachMacros: [ Q_FOREACH, BOOST_FOREACH ] 47 | IncludeBlocks: Regroup 48 | 49 | IncludeCategories: 50 | - Regex: '^' 51 | Priority: 2 52 | - Regex: '^<(test)/' 53 | Priority: 1 54 | - Regex: '^<.*\.h>' 55 | Priority: 0 56 | - Regex: '^<(boost)/' 57 | Priority: 3 58 | - Regex: '.*' 59 | Priority: 4 60 | IncludeIsMainRegex: '$' 61 | IndentCaseLabels: true 62 | IndentFunctionDeclarationAfterType: false 63 | IndentWidth: 4 64 | IndentWrappedFunctionNames: false 65 | KeepEmptyLinesAtTheStartOfBlocks: false 66 | MaxEmptyLinesToKeep: 1 67 | NamespaceIndentation: None 68 | ObjCSpaceAfterProperty: false 69 | ObjCSpaceBeforeProtocolList: false 70 | PenaltyBreakBeforeFirstCallParameter: 1 71 | PenaltyBreakComment: 300 72 | PenaltyBreakFirstLessLess: 120 73 | PenaltyBreakString: 1000 74 | PenaltyExcessCharacter: 1000000 75 | PenaltyReturnTypeOnItsOwnLine: 200 76 | PointerAlignment: Left 77 | ReflowComments: true 78 | SortIncludes: true 79 | SpaceAfterCStyleCast: false 80 | SpaceBeforeAssignmentOperators: true 81 | SpaceBeforeParens: ControlStatements 82 | SpaceInEmptyParentheses: false 83 | SpacesBeforeTrailingComments: 2 84 | SpacesInAngles: false 85 | SpacesInContainerLiterals: true 86 | SpacesInCStyleCastParentheses: false 87 | SpacesInParentheses: false 88 | SpacesInSquareBrackets: false 89 | Standard: Cpp11 90 | TabWidth: 8 91 | UseTab: Never 92 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This feature requires Git >= 2.24 2 | # To use it by default in git blame: 3 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 4 | 8ae260cb466d4cd0d4db378e5ce0acb8e4432f7c 5 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.yml: -------------------------------------------------------------------------------- 1 | name: clang-format 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-20.04 8 | env: 9 | CLANG_VERSION: 10 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install clang-format 13 | run: | 14 | codename=$( lsb_release --codename --short ) 15 | sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null <:=${coverage_test}> 42 | COMMAND ${LLVM_PROFDATA} 43 | merge -sparse default.profraw -o rip.profdata 44 | COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:" 45 | COMMAND ${LLVM_COV} 46 | report -instr-profile=rip.profdata 47 | $ ${extract_pattern} 48 | # generate html report 49 | COMMAND ${LLVM_COV} 50 | show -format=html -output-dir=${CMAKE_BINARY_DIR}/coverage 51 | -instr-profile=rip.profdata 52 | $ ${extract_pattern} 53 | BYPRODUCTS coverage/index.html) 54 | endif () 55 | elseif (is_gcc) 56 | find_program (LCOV lcov) 57 | if (NOT LCOV) 58 | message (WARNING "unable to find lcov - skipping coverage_report target") 59 | endif () 60 | 61 | find_program (GENHTML genhtml) 62 | if (NOT GENHTML) 63 | message (WARNING "unable to find genhtml - skipping coverage_report target") 64 | endif () 65 | 66 | set (extract_pattern "*") 67 | if (coverage_core_only) 68 | set (extract_pattern "*/src/*") 69 | endif () 70 | 71 | if (LCOV AND GENHTML) 72 | add_custom_target (coverage_report 73 | USES_TERMINAL 74 | COMMAND ${CMAKE_COMMAND} -E echo "Generating coverage- results will be in ${CMAKE_BINARY_DIR}/coverage/index.html." 75 | # create baseline info file 76 | COMMAND ${LCOV} 77 | --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d . -i -o baseline.info 78 | | grep -v "ignoring data for external file" 79 | # run tests 80 | COMMAND ${CMAKE_COMMAND} -E echo "Running validator-keys tests for coverage report." 81 | COMMAND validator-keys --unittest$<$:=${coverage_test}> 82 | # Create test coverage data file 83 | COMMAND ${LCOV} 84 | --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d . -o tests.info 85 | | grep -v "ignoring data for external file" 86 | # Combine baseline and test coverage data 87 | COMMAND ${LCOV} 88 | -a baseline.info -a tests.info -o lcov-all.info 89 | # extract our files 90 | COMMAND ${LCOV} 91 | -e lcov-all.info "${extract_pattern}" -o lcov.info 92 | COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:" 93 | COMMAND ${LCOV} --summary lcov.info 94 | # generate HTML report 95 | COMMAND ${GENHTML} 96 | -o ${CMAKE_BINARY_DIR}/coverage lcov.info 97 | BYPRODUCTS coverage/index.html) 98 | endif () 99 | else() 100 | message(STATUS "Coverage: neither clang nor gcc") 101 | endif () 102 | else() 103 | message(STATUS "Coverage disabled") 104 | endif () 105 | -------------------------------------------------------------------------------- /cmake/KeysInterface.cmake: -------------------------------------------------------------------------------- 1 | #[===================================================================[ 2 | rippled compile options/settings via an interface library 3 | #]===================================================================] 4 | 5 | add_library (keys_opts INTERFACE) 6 | add_library (Keys::opts ALIAS keys_opts) 7 | target_compile_definitions (keys_opts 8 | INTERFACE 9 | BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS 10 | $<$: 11 | BOOST_ASIO_NO_DEPRECATED 12 | BOOST_FILESYSTEM_NO_DEPRECATED 13 | > 14 | $<$>: 15 | BOOST_COROUTINES_NO_DEPRECATION_WARNING 16 | BOOST_BEAST_ALLOW_DEPRECATED 17 | BOOST_FILESYSTEM_DEPRECATED 18 | > 19 | $<$: 20 | USE_BEAST_HASHER 21 | > 22 | $<$:BEAST_NO_UNIT_TEST_INLINE=1> 23 | $<$:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1> 24 | $<$:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>) 25 | target_compile_options (keys_opts 26 | INTERFACE 27 | $<$,$>:-Wsuggest-override> 28 | $<$:-fno-omit-frame-pointer> 29 | $<$,$>:-fprofile-arcs -ftest-coverage> 30 | $<$,$>:-fprofile-instr-generate -fcoverage-mapping> 31 | $<$:-pg> 32 | $<$,$>:-p>) 33 | 34 | target_link_libraries (keys_opts 35 | INTERFACE 36 | $<$,$>:-fprofile-arcs -ftest-coverage> 37 | $<$,$>:-fprofile-instr-generate -fcoverage-mapping> 38 | $<$:-pg> 39 | $<$,$>:-p>) 40 | 41 | if (jemalloc) 42 | if (static) 43 | set(JEMALLOC_USE_STATIC ON CACHE BOOL "" FORCE) 44 | endif () 45 | find_package (jemalloc REQUIRED) 46 | target_compile_definitions (keys_opts INTERFACE PROFILE_JEMALLOC) 47 | target_include_directories (keys_opts SYSTEM INTERFACE ${JEMALLOC_INCLUDE_DIRS}) 48 | target_link_libraries (keys_opts INTERFACE ${JEMALLOC_LIBRARIES}) 49 | get_filename_component (JEMALLOC_LIB_PATH ${JEMALLOC_LIBRARIES} DIRECTORY) 50 | ## TODO see if we can use the BUILD_RPATH target property (is it transitive?) 51 | set (CMAKE_BUILD_RPATH ${CMAKE_BUILD_RPATH} ${JEMALLOC_LIB_PATH}) 52 | endif () 53 | 54 | if (san) 55 | target_compile_options (keys_opts 56 | INTERFACE 57 | # sanitizers recommend minimum of -O1 for reasonable performance 58 | $<$:-O1> 59 | ${SAN_FLAG} 60 | -fno-omit-frame-pointer) 61 | target_compile_definitions (keys_opts 62 | INTERFACE 63 | $<$:SANITIZER=ASAN> 64 | $<$:SANITIZER=TSAN> 65 | $<$:SANITIZER=MSAN> 66 | $<$:SANITIZER=UBSAN>) 67 | target_link_libraries (keys_opts INTERFACE ${SAN_FLAG} ${SAN_LIB}) 68 | endif () 69 | 70 | -------------------------------------------------------------------------------- /cmake/KeysSanity.cmake: -------------------------------------------------------------------------------- 1 | #[===================================================================[ 2 | convenience variables and sanity checks 3 | #]===================================================================] 4 | 5 | if (NOT ep_procs) 6 | include(ProcessorCount) 7 | ProcessorCount(ep_procs) 8 | if (ep_procs GREATER 1) 9 | # never use more than half of cores for EP builds 10 | math (EXPR ep_procs "${ep_procs} / 2") 11 | message (STATUS "Using ${ep_procs} cores for ExternalProject builds.") 12 | endif () 13 | endif () 14 | get_property (is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if (is_multiconfig STREQUAL "NOTFOUND") 16 | if (${CMAKE_GENERATOR} STREQUAL "Xcode" OR ${CMAKE_GENERATOR} MATCHES "^Visual Studio") 17 | set (is_multiconfig TRUE) 18 | endif () 19 | endif () 20 | 21 | set (CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) 22 | if (NOT is_multiconfig) 23 | if (NOT CMAKE_BUILD_TYPE) 24 | message (STATUS "Build type not specified - defaulting to Release") 25 | set (CMAKE_BUILD_TYPE Release CACHE STRING "build type" FORCE) 26 | elseif (NOT (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL Release)) 27 | # for simplicity, these are the only two config types we care about. Limiting 28 | # the build types simplifies dealing with external project builds especially 29 | message (FATAL_ERROR " *** Only Debug or Release build types are currently supported ***") 30 | endif () 31 | endif () 32 | 33 | get_directory_property(has_parent PARENT_DIRECTORY) 34 | if (has_parent) 35 | set (is_root_project OFF) 36 | else () 37 | set (is_root_project ON) 38 | endif () 39 | 40 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES ".*Clang") # both Clang and AppleClang 41 | set (is_clang TRUE) 42 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND 43 | CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0) 44 | message (FATAL_ERROR "This project requires clang 7 or later") 45 | endif () 46 | # TODO min AppleClang version check ? 47 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 48 | set (is_gcc TRUE) 49 | if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0) 50 | message (FATAL_ERROR "This project requires GCC 7 or later") 51 | endif () 52 | endif () 53 | if (CMAKE_GENERATOR STREQUAL "Xcode") 54 | set (is_xcode TRUE) 55 | endif () 56 | 57 | if (CMAKE_SYSTEM_NAME STREQUAL "Linux") 58 | set (is_linux TRUE) 59 | else () 60 | set (is_linux FALSE) 61 | endif () 62 | 63 | if ("$ENV{CI}" STREQUAL "true" OR "$ENV{CONTINUOUS_INTEGRATION}" STREQUAL "true") 64 | set (is_ci TRUE) 65 | else () 66 | set (is_ci FALSE) 67 | endif () 68 | 69 | # check for in-source build and fail 70 | if ("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") 71 | message (FATAL_ERROR "Builds (in-source) are not allowed in " 72 | "${CMAKE_CURRENT_SOURCE_DIR}. Please remove CMakeCache.txt and the CMakeFiles " 73 | "directory from ${CMAKE_CURRENT_SOURCE_DIR} and try building in a separate directory.") 74 | endif () 75 | 76 | if (MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") 77 | message (FATAL_ERROR "Visual Studio 32-bit build is not supported.") 78 | endif () 79 | 80 | if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) 81 | message (FATAL_ERROR "Rippled requires a 64 bit target architecture.\n" 82 | "The most likely cause of this warning is trying to build rippled with a 32-bit OS.") 83 | endif () 84 | 85 | if (APPLE AND NOT HOMEBREW) 86 | find_program (HOMEBREW brew) 87 | endif () 88 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | xrpl/2.3.0-b4 3 | 4 | [generators] 5 | CMakeDeps 6 | CMakeToolchain 7 | -------------------------------------------------------------------------------- /doc/validator-keys-tool-guide.md: -------------------------------------------------------------------------------- 1 | # Validator Keys Tool Guide 2 | 3 | This guide explains how to set up a validator so its public key does not have to 4 | change if the rippled config and/or server are compromised. 5 | 6 | A validator uses a public/private key pair. The validator is identified by the 7 | public key. The private key should be tightly controlled. It is used to: 8 | 9 | * sign tokens authorizing a rippled server to run as the validator identified 10 | by this public key. 11 | * sign revocations indicating that the private key has been compromised and 12 | the validator public key should no longer be trusted. 13 | 14 | Each new token invalidates all previous tokens for the validator public key. 15 | The current token needs to be present in the rippled config file. 16 | 17 | Servers that trust the validator will adapt automatically when the token 18 | changes. 19 | 20 | ## Validator Keys 21 | 22 | When first setting up a validator, use the `validator-keys` tool to generate 23 | its key pair: 24 | 25 | ``` 26 | $ validator-keys create_keys 27 | ``` 28 | 29 | Sample output: 30 | ``` 31 | Validator keys stored in /home/ubuntu/.ripple/validator-keys.json 32 | ``` 33 | 34 | Keep the key file in a secure but recoverable location, such as an encrypted 35 | USB flash drive. Do not modify its contents. 36 | 37 | ## Validator Token 38 | 39 | After first creating the [validator keys](#validator-keys) or if the previous 40 | token has been compromised, use the `validator-keys` tool to create a new 41 | validator token: 42 | 43 | ``` 44 | $ validator-keys create_token 45 | ``` 46 | 47 | Sample output: 48 | 49 | ``` 50 | Update rippled.cfg file with these values: 51 | 52 | # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr 53 | 54 | [validator_token] 55 | eyJ2YWxpZGF0aW9uX3NlY3J|dF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT 56 | QzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYT|kYWY2IiwibWFuaWZl 57 | c3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE 58 | hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG 59 | bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2 60 | hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1 61 | NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj 62 | VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ== 63 | ``` 64 | 65 | For a new validator, add the [validator_token] value to the rippled config file. 66 | For a pre-existing validator, replace the old [validator_token] value with the 67 | newly generated one. A valid config file may only contain one [validator_token] 68 | value. After the config is updated, restart rippled. 69 | 70 | There is a hard limit of 4,294,967,293 tokens that can be generated for a given 71 | validator key pair. 72 | 73 | ## Key Revocation 74 | 75 | If a validator private key is compromised, the key must be revoked permanently. 76 | To revoke the validator key, use the `validator-keys` tool to generate a 77 | revocation, which indicates to other servers that the key is no longer valid: 78 | 79 | ``` 80 | $ validator-keys revoke_keys 81 | ``` 82 | 83 | Sample output: 84 | 85 | ``` 86 | WARNING: This will revoke your validator keys! 87 | 88 | Update rippled.cfg file with these values and restart rippled: 89 | 90 | # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr 91 | 92 | [validator_key_revocation] 93 | JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr 94 | jQ2KJSO/2tsjRhjO6q0QQHppslQsKNSXWxjGQNIEa6nPisBOKlDDcJVZAMP4QcIyNCadzgM= 95 | ``` 96 | 97 | Add the `[validator_key_revocation]` value to this validator's config and 98 | restart rippled. Rename the old key file and generate new [validator keys](#validator-keys) and 99 | a corresponding [validator token](#validator-token). 100 | 101 | ## Signing 102 | 103 | The `validator-keys` tool can be used to sign arbitrary data with the validator 104 | key. 105 | 106 | ``` 107 | $ validator-keys sign "your data to sign" 108 | ``` 109 | 110 | Sample output: 111 | 112 | ``` 113 | B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D 114 | ``` 115 | -------------------------------------------------------------------------------- /src/ValidatorKeys.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of validator-keys-tool: 4 | https://github.com/ripple/validator-keys-tool 5 | Copyright (c) 2016 Ripple Labs Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any 8 | purpose with or without fee is hereby granted, provided that the above 9 | copyright notice and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | //============================================================================== 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #include 35 | 36 | namespace ripple { 37 | 38 | std::string 39 | ValidatorToken::toString() const 40 | { 41 | Json::Value jv; 42 | jv["validation_secret_key"] = strHex(secretKey); 43 | jv["manifest"] = manifest; 44 | 45 | return ripple::base64_encode(to_string(jv)); 46 | } 47 | 48 | ValidatorKeys::ValidatorKeys(KeyType const& keyType) 49 | : keyType_(keyType) 50 | , tokenSequence_(0) 51 | , revoked_(false) 52 | , keys_(generateKeyPair(keyType_, randomSeed())) 53 | { 54 | } 55 | 56 | ValidatorKeys::ValidatorKeys( 57 | KeyType const& keyType, 58 | SecretKey const& secretKey, 59 | std::uint32_t tokenSequence, 60 | bool revoked) 61 | : keyType_(keyType) 62 | , tokenSequence_(tokenSequence) 63 | , revoked_(revoked) 64 | , keys_({derivePublicKey(keyType_, secretKey), secretKey}) 65 | { 66 | } 67 | 68 | ValidatorKeys 69 | ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile) 70 | { 71 | std::ifstream ifsKeys(keyFile.c_str(), std::ios::in); 72 | 73 | if (!ifsKeys) 74 | throw std::runtime_error( 75 | "Failed to open key file: " + keyFile.string()); 76 | 77 | Json::Reader reader; 78 | Json::Value jKeys; 79 | if (!reader.parse(ifsKeys, jKeys)) 80 | { 81 | throw std::runtime_error( 82 | "Unable to parse json key file: " + keyFile.string()); 83 | } 84 | 85 | static std::array const requiredFields{ 86 | {"key_type", "secret_key", "token_sequence", "revoked"}}; 87 | 88 | for (auto field : requiredFields) 89 | { 90 | if (!jKeys.isMember(field)) 91 | { 92 | throw std::runtime_error( 93 | "Key file '" + keyFile.string() + "' is missing \"" + field + 94 | "\" field"); 95 | } 96 | } 97 | 98 | auto const keyType = keyTypeFromString(jKeys["key_type"].asString()); 99 | if (!keyType) 100 | { 101 | throw std::runtime_error( 102 | "Key file '" + keyFile.string() + 103 | "' contains invalid \"key_type\" field: " + 104 | jKeys["key_type"].toStyledString()); 105 | } 106 | 107 | auto const secret = parseBase58( 108 | TokenType::NodePrivate, jKeys["secret_key"].asString()); 109 | 110 | if (!secret) 111 | { 112 | throw std::runtime_error( 113 | "Key file '" + keyFile.string() + 114 | "' contains invalid \"secret_key\" field: " + 115 | jKeys["secret_key"].toStyledString()); 116 | } 117 | 118 | std::uint32_t tokenSequence; 119 | try 120 | { 121 | if (!jKeys["token_sequence"].isIntegral()) 122 | throw std::runtime_error(""); 123 | 124 | tokenSequence = jKeys["token_sequence"].asUInt(); 125 | } 126 | catch (std::runtime_error&) 127 | { 128 | throw std::runtime_error( 129 | "Key file '" + keyFile.string() + 130 | "' contains invalid \"token_sequence\" field: " + 131 | jKeys["token_sequence"].toStyledString()); 132 | } 133 | 134 | if (!jKeys["revoked"].isBool()) 135 | throw std::runtime_error( 136 | "Key file '" + keyFile.string() + 137 | "' contains invalid \"revoked\" field: " + 138 | jKeys["revoked"].toStyledString()); 139 | 140 | ValidatorKeys vk( 141 | *keyType, *secret, tokenSequence, jKeys["revoked"].asBool()); 142 | 143 | if (jKeys.isMember("domain")) 144 | { 145 | if (!jKeys["domain"].isString()) 146 | throw std::runtime_error( 147 | "Key file '" + keyFile.string() + 148 | "' contains invalid \"domain\" field: " + 149 | jKeys["domain"].toStyledString()); 150 | 151 | vk.domain(jKeys["domain"].asString()); 152 | } 153 | 154 | if (jKeys.isMember("manifest")) 155 | { 156 | if (!jKeys["manifest"].isString()) 157 | throw std::runtime_error( 158 | "Key file '" + keyFile.string() + 159 | "' contains invalid \"manifest\" field: " + 160 | jKeys["manifest"].toStyledString()); 161 | 162 | auto ret = strUnHex(jKeys["manifest"].asString()); 163 | 164 | if (!ret || ret->size() == 0) 165 | throw std::runtime_error( 166 | "Key file '" + keyFile.string() + 167 | "' contains invalid \"manifest\" field: " + 168 | jKeys["manifest"].toStyledString()); 169 | 170 | vk.manifest_.clear(); 171 | vk.manifest_.reserve(ret->size()); 172 | std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_)); 173 | } 174 | 175 | return vk; 176 | } 177 | 178 | void 179 | ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const 180 | { 181 | using namespace boost::filesystem; 182 | 183 | Json::Value jv; 184 | jv["key_type"] = to_string(keyType_); 185 | jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey); 186 | jv["secret_key"] = toBase58(TokenType::NodePrivate, keys_.secretKey); 187 | jv["token_sequence"] = Json::UInt(tokenSequence_); 188 | jv["revoked"] = revoked_; 189 | if (!domain_.empty()) 190 | jv["domain"] = domain_; 191 | if (!manifest_.empty()) 192 | jv["manifest"] = strHex(makeSlice(manifest_)); 193 | 194 | if (!keyFile.parent_path().empty()) 195 | { 196 | boost::system::error_code ec; 197 | if (!exists(keyFile.parent_path())) 198 | boost::filesystem::create_directories(keyFile.parent_path(), ec); 199 | 200 | if (ec || !is_directory(keyFile.parent_path())) 201 | throw std::runtime_error( 202 | "Cannot create directory: " + keyFile.parent_path().string()); 203 | } 204 | 205 | std::ofstream o(keyFile.string(), std::ios_base::trunc); 206 | if (o.fail()) 207 | throw std::runtime_error("Cannot open key file: " + keyFile.string()); 208 | 209 | o << jv.toStyledString(); 210 | } 211 | 212 | boost::optional 213 | ValidatorKeys::createValidatorToken(KeyType const& keyType) 214 | { 215 | if (revoked() || 216 | std::numeric_limits::max() - 1 <= tokenSequence_) 217 | return boost::none; 218 | 219 | ++tokenSequence_; 220 | 221 | auto const tokenSecret = generateSecretKey(keyType, randomSeed()); 222 | auto const tokenPublic = derivePublicKey(keyType, tokenSecret); 223 | 224 | STObject st(sfGeneric); 225 | st[sfSequence] = tokenSequence_; 226 | st[sfPublicKey] = keys_.publicKey; 227 | st[sfSigningPubKey] = tokenPublic; 228 | 229 | if (!domain_.empty()) 230 | st[sfDomain] = makeSlice(domain_); 231 | 232 | ripple::sign(st, HashPrefix::manifest, keyType, tokenSecret); 233 | ripple::sign( 234 | st, HashPrefix::manifest, keyType_, keys_.secretKey, sfMasterSignature); 235 | 236 | Serializer s; 237 | st.add(s); 238 | 239 | manifest_.clear(); 240 | manifest_.reserve(s.size()); 241 | std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); 242 | 243 | return ValidatorToken{ 244 | ripple::base64_encode(manifest_.data(), manifest_.size()), tokenSecret}; 245 | } 246 | 247 | std::string 248 | ValidatorKeys::revoke() 249 | { 250 | revoked_ = true; 251 | 252 | STObject st(sfGeneric); 253 | st[sfSequence] = std::numeric_limits::max(); 254 | st[sfPublicKey] = keys_.publicKey; 255 | 256 | ripple::sign( 257 | st, HashPrefix::manifest, keyType_, keys_.secretKey, sfMasterSignature); 258 | 259 | Serializer s; 260 | st.add(s); 261 | 262 | manifest_.clear(); 263 | manifest_.reserve(s.size()); 264 | std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); 265 | 266 | return ripple::base64_encode(manifest_.data(), manifest_.size()); 267 | } 268 | 269 | std::string 270 | ValidatorKeys::sign(std::string const& data) const 271 | { 272 | return strHex( 273 | ripple::sign(keys_.publicKey, keys_.secretKey, makeSlice(data))); 274 | } 275 | 276 | void 277 | ValidatorKeys::domain(std::string d) 278 | { 279 | if (!d.empty()) 280 | { 281 | // A valid domain for a validator must be at least 4 characters 282 | // long, should contain at least one . and should not be longer 283 | // that 128 characters. 284 | if (d.size() < 4 || d.size() > 128) 285 | throw std::runtime_error( 286 | "The domain must be between 4 and 128 characters long."); 287 | 288 | // This regular expression should do a decent job of weeding out 289 | // obviously wrong domain names but it isn't perfect. It does not 290 | // really support IDNs. If this turns out to be an issue, a more 291 | // thorough regex can be used or this check can just be removed. 292 | static boost::regex const re( 293 | "^" // Beginning of line 294 | "(" // Hostname or domain name 295 | "(?!-)" // - must not begin with '-' 296 | "[a-zA-Z0-9-]{1,63}" // - only alphanumeric and '-' 297 | "(? 22 | #include 23 | 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | namespace boost { 31 | namespace filesystem { 32 | class path; 33 | } 34 | } // namespace boost 35 | 36 | namespace ripple { 37 | 38 | struct ValidatorToken 39 | { 40 | std::string const manifest; 41 | SecretKey const secretKey; 42 | 43 | /// Returns base64-encoded JSON object 44 | std::string 45 | toString() const; 46 | }; 47 | 48 | class ValidatorKeys 49 | { 50 | private: 51 | KeyType keyType_; 52 | 53 | // struct used to contain both public and secret keys 54 | struct Keys 55 | { 56 | PublicKey publicKey; 57 | SecretKey secretKey; 58 | 59 | Keys() = delete; 60 | Keys(std::pair p) 61 | : publicKey(p.first), secretKey(p.second) 62 | { 63 | } 64 | }; 65 | 66 | std::vector manifest_; 67 | std::uint32_t tokenSequence_; 68 | bool revoked_; 69 | std::string domain_; 70 | Keys keys_; 71 | 72 | public: 73 | explicit ValidatorKeys(KeyType const& keyType); 74 | 75 | ValidatorKeys( 76 | KeyType const& keyType, 77 | SecretKey const& secretKey, 78 | std::uint32_t sequence, 79 | bool revoked = false); 80 | 81 | /** Returns ValidatorKeys constructed from JSON file 82 | 83 | @param keyFile Path to JSON key file 84 | 85 | @throws std::runtime_error if file content is invalid 86 | */ 87 | static ValidatorKeys 88 | make_ValidatorKeys(boost::filesystem::path const& keyFile); 89 | 90 | ~ValidatorKeys() = default; 91 | ValidatorKeys(ValidatorKeys const&) = default; 92 | ValidatorKeys& 93 | operator=(ValidatorKeys const&) = default; 94 | 95 | inline bool 96 | operator==(ValidatorKeys const& rhs) const 97 | { 98 | return revoked_ == rhs.revoked_ && keyType_ == rhs.keyType_ && 99 | tokenSequence_ == rhs.tokenSequence_ && 100 | keys_.publicKey == rhs.keys_.publicKey && 101 | keys_.secretKey == rhs.keys_.secretKey; 102 | } 103 | 104 | /** Write keys to JSON file 105 | 106 | @param keyFile Path to file to write 107 | 108 | @note Overwrites existing key file 109 | 110 | @throws std::runtime_error if unable to create parent directory 111 | */ 112 | void 113 | writeToFile(boost::filesystem::path const& keyFile) const; 114 | 115 | /** Returns validator token for current sequence 116 | 117 | @param keyType Key type for the token keys 118 | */ 119 | boost::optional 120 | createValidatorToken(KeyType const& keyType = KeyType::secp256k1); 121 | 122 | /** Revokes validator keys 123 | 124 | @return base64-encoded key revocation 125 | */ 126 | std::string 127 | revoke(); 128 | 129 | /** Signs string with validator key 130 | 131 | @papam data String to sign 132 | 133 | @return hex-encoded signature 134 | */ 135 | std::string 136 | sign(std::string const& data) const; 137 | 138 | /** Returns the public key. */ 139 | PublicKey const& 140 | publicKey() const 141 | { 142 | return keys_.publicKey; 143 | } 144 | 145 | /** Returns true if keys are revoked. */ 146 | bool 147 | revoked() const 148 | { 149 | return revoked_; 150 | } 151 | 152 | /** Returns the domain associated with this key, if any */ 153 | std::string 154 | domain() const 155 | { 156 | return domain_; 157 | } 158 | 159 | /** Sets the domain associated with this key */ 160 | void 161 | domain(std::string d); 162 | 163 | /** Returns the last manifest we generated for this domain, if available. */ 164 | std::vector 165 | manifest() const 166 | { 167 | return manifest_; 168 | } 169 | 170 | /** Returns the sequence number of the last manifest generated. */ 171 | std::uint32_t 172 | sequence() const 173 | { 174 | return tokenSequence_; 175 | } 176 | }; 177 | 178 | } // namespace ripple 179 | -------------------------------------------------------------------------------- /src/ValidatorKeysTool.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of validator-keys-tool: 4 | https://github.com/ripple/validator-keys-tool 5 | Copyright (c) 2016 Ripple Labs Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any 8 | purpose with or without fee is hereby granted, provided that the above 9 | copyright notice and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | //============================================================================== 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #ifdef BOOST_MSVC 35 | #include 36 | #endif 37 | 38 | //------------------------------------------------------------------------------ 39 | // The build version number. You must edit this for each release 40 | // and follow the format described at http://semver.org/ 41 | //-------------------------------------------------------------------------- 42 | char const* const versionString = 43 | "0.3.2" 44 | 45 | #if defined(DEBUG) || defined(SANITIZER) 46 | "+" 47 | #ifdef DEBUG 48 | "DEBUG" 49 | #ifdef SANITIZER 50 | "." 51 | #endif 52 | #endif 53 | 54 | #ifdef SANITIZER 55 | BOOST_PP_STRINGIZE(SANITIZER) 56 | #endif 57 | #endif 58 | 59 | //-------------------------------------------------------------------------- 60 | ; 61 | 62 | static int 63 | runUnitTests() 64 | { 65 | using namespace beast::unit_test; 66 | reporter r; 67 | bool const anyFailed = r.run_each(global_suites()); 68 | if (anyFailed) 69 | return EXIT_FAILURE; // LCOV_EXCL_LINE 70 | return EXIT_SUCCESS; 71 | } 72 | 73 | void 74 | createKeyFile(boost::filesystem::path const& keyFile) 75 | { 76 | using namespace ripple; 77 | 78 | if (exists(keyFile)) 79 | throw std::runtime_error( 80 | "Refusing to overwrite existing key file: " + keyFile.string()); 81 | 82 | ValidatorKeys const keys(KeyType::ed25519); 83 | keys.writeToFile(keyFile); 84 | 85 | std::cout << "Validator keys stored in " << keyFile.string() 86 | << "\n\nThis file should be stored securely and not shared.\n\n"; 87 | } 88 | 89 | void 90 | createToken(boost::filesystem::path const& keyFile) 91 | { 92 | using namespace ripple; 93 | 94 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 95 | 96 | if (keys.revoked()) 97 | throw std::runtime_error("Validator keys have been revoked."); 98 | 99 | auto const token = keys.createValidatorToken(); 100 | 101 | if (!token) 102 | throw std::runtime_error( 103 | "Maximum number of tokens have already been generated.\n" 104 | "Revoke validator keys if previous token has been compromised."); 105 | 106 | // Update key file with new token sequence 107 | keys.writeToFile(keyFile); 108 | 109 | std::cout 110 | << "Update rippled.cfg file with these values and restart rippled:\n\n"; 111 | std::cout << "# validator public key: " 112 | << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; 113 | std::cout << "[validator_token]\n"; 114 | 115 | auto const tokenStr = token->toString(); 116 | auto const len = 72; 117 | for (auto i = 0; i < tokenStr.size(); i += len) 118 | std::cout << tokenStr.substr(i, len) << std::endl; 119 | 120 | std::cout << std::endl; 121 | } 122 | 123 | void 124 | createRevocation(boost::filesystem::path const& keyFile) 125 | { 126 | using namespace ripple; 127 | 128 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 129 | 130 | if (keys.revoked()) 131 | std::cout << "WARNING: Validator keys have already been revoked!\n\n"; 132 | else 133 | std::cout << "WARNING: This will revoke your validator keys!\n\n"; 134 | 135 | auto const revocation = keys.revoke(); 136 | 137 | // Update key file with new token sequence 138 | keys.writeToFile(keyFile); 139 | 140 | std::cout 141 | << "Update rippled.cfg file with these values and restart rippled:\n\n"; 142 | std::cout << "# validator public key: " 143 | << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; 144 | std::cout << "[validator_key_revocation]\n"; 145 | 146 | auto const len = 72; 147 | for (auto i = 0; i < revocation.size(); i += len) 148 | std::cout << revocation.substr(i, len) << std::endl; 149 | 150 | std::cout << std::endl; 151 | } 152 | 153 | void 154 | attestDomain(ripple::ValidatorKeys const& keys) 155 | { 156 | using namespace ripple; 157 | 158 | if (keys.domain().empty()) 159 | { 160 | std::cout << "No attestation is necessary if no domain is specified!\n"; 161 | std::cout << "If you have an attestation in your xrpl-ledger.toml\n"; 162 | std::cout << "you should remove it at this time.\n"; 163 | return; 164 | } 165 | 166 | std::cout << "The domain attestation for validator " 167 | << toBase58(TokenType::NodePublic, keys.publicKey()) 168 | << " is:\n\n"; 169 | 170 | std::cout << "attestation=\"" 171 | << keys.sign( 172 | "[domain-attestation-blob:" + keys.domain() + ":" + 173 | toBase58(TokenType::NodePublic, keys.publicKey()) + "]") 174 | << "\"\n\n"; 175 | 176 | std::cout << "You should include it in your xrp-ledger.toml file in the\n"; 177 | std::cout << "section for this validator.\n"; 178 | } 179 | 180 | void 181 | attestDomain(boost::filesystem::path const& keyFile) 182 | { 183 | using namespace ripple; 184 | 185 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 186 | 187 | if (keys.revoked()) 188 | throw std::runtime_error( 189 | "Operation error: The specified master key has been revoked!"); 190 | 191 | attestDomain(keys); 192 | } 193 | 194 | void 195 | setDomain(std::string const& domain, boost::filesystem::path const& keyFile) 196 | { 197 | using namespace ripple; 198 | 199 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 200 | 201 | if (keys.revoked()) 202 | throw std::runtime_error( 203 | "Operation error: The specified master key has been revoked!"); 204 | 205 | if (domain == keys.domain()) 206 | { 207 | if (domain.empty()) 208 | std::cout << "The domain name was already cleared!\n"; 209 | else 210 | std::cout << "The domain name was already set.\n"; 211 | return; 212 | } 213 | 214 | // Set the domain and generate a new token 215 | keys.domain(domain); 216 | auto const token = keys.createValidatorToken(); 217 | if (!token) 218 | throw std::runtime_error( 219 | "Maximum number of tokens have already been generated.\n" 220 | "Revoke validator keys if previous token has been compromised."); 221 | 222 | // Flush to disk 223 | keys.writeToFile(keyFile); 224 | 225 | if (domain.empty()) 226 | std::cout << "The domain name has been cleared.\n"; 227 | else 228 | std::cout << "The domain name has been set to: " << domain << "\n\n"; 229 | attestDomain(keys); 230 | 231 | std::cout << "\n"; 232 | std::cout << "You also need to update the rippled.cfg file to add a new\n"; 233 | std::cout << "validator token and restart rippled:\n\n"; 234 | std::cout << "# validator public key: " 235 | << toBase58(TokenType::NodePublic, keys.publicKey()) << "\n\n"; 236 | std::cout << "[validator_token]\n"; 237 | 238 | auto const tokenStr = token->toString(); 239 | auto const len = 72; 240 | for (auto i = 0; i < tokenStr.size(); i += len) 241 | std::cout << tokenStr.substr(i, len) << std::endl; 242 | 243 | std::cout << "\n"; 244 | } 245 | 246 | void 247 | signData(std::string const& data, boost::filesystem::path const& keyFile) 248 | { 249 | using namespace ripple; 250 | 251 | if (data.empty()) 252 | throw std::runtime_error( 253 | "Syntax error: Must specify data string to sign"); 254 | 255 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 256 | 257 | if (keys.revoked()) 258 | std::cout << "WARNING: Validator keys have been revoked!\n\n"; 259 | 260 | std::cout << keys.sign(data) << std::endl; 261 | std::cout << std::endl; 262 | } 263 | 264 | void 265 | generateManifest( 266 | std::string const& type, 267 | boost::filesystem::path const& keyFile) 268 | { 269 | using namespace ripple; 270 | 271 | auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); 272 | 273 | auto const m = keys.manifest(); 274 | 275 | if (m.empty()) 276 | { 277 | std::cout << "The last manifest generated is unavailable. You can\n"; 278 | std::cout << "generate a new one.\n\n"; 279 | return; 280 | } 281 | 282 | if (type == "base64") 283 | { 284 | std::cout << "Manifest #" << keys.sequence() << " (Base64):\n"; 285 | std::cout << base64_encode(m.data(), m.size()) << "\n\n"; 286 | return; 287 | } 288 | 289 | if (type == "hex") 290 | { 291 | std::cout << "Manifest #" << keys.sequence() << " (Hex):\n"; 292 | std::cout << strHex(makeSlice(m)) << "\n\n"; 293 | return; 294 | } 295 | 296 | std::cout << "Unknown encoding '" << type << "'\n"; 297 | } 298 | 299 | int 300 | runCommand( 301 | std::string const& command, 302 | std::vector const& args, 303 | boost::filesystem::path const& keyFile) 304 | { 305 | using namespace std; 306 | 307 | static map::size_type> const commandArgs = { 308 | {"create_keys", 0}, 309 | {"create_token", 0}, 310 | {"revoke_keys", 0}, 311 | {"set_domain", 1}, 312 | {"clear_domain", 0}, 313 | {"attest_domain", 0}, 314 | {"show_manifest", 1}, 315 | {"sign", 1}, 316 | }; 317 | 318 | auto const iArgs = commandArgs.find(command); 319 | 320 | if (iArgs == commandArgs.end()) 321 | throw std::runtime_error("Unknown command: " + command); 322 | 323 | if (args.size() != iArgs->second) 324 | throw std::runtime_error("Syntax error: Wrong number of arguments"); 325 | 326 | if (command == "create_keys") 327 | createKeyFile(keyFile); 328 | else if (command == "create_token") 329 | createToken(keyFile); 330 | else if (command == "revoke_keys") 331 | createRevocation(keyFile); 332 | else if (command == "set_domain") 333 | setDomain(args[0], keyFile); 334 | else if (command == "clear_domain") 335 | setDomain("", keyFile); 336 | else if (command == "attest_domain") 337 | attestDomain(keyFile); 338 | else if (command == "sign") 339 | signData(args[0], keyFile); 340 | else if (command == "show_manifest") 341 | generateManifest(args[0], keyFile); 342 | 343 | return 0; 344 | } 345 | 346 | // LCOV_EXCL_START 347 | static std::string 348 | getEnvVar(char const* name) 349 | { 350 | std::string value; 351 | 352 | auto const v = getenv(name); 353 | 354 | if (v != nullptr) 355 | value = v; 356 | 357 | return value; 358 | } 359 | 360 | void 361 | printHelp(const boost::program_options::options_description& desc) 362 | { 363 | std::cerr 364 | << "validator-keys [options] [ ...]\n" 365 | << desc << std::endl 366 | << "Commands: \n" 367 | " create_keys Generate validator keys.\n" 368 | " create_token Generate validator token.\n" 369 | " revoke_keys Revoke validator keys.\n" 370 | " sign Sign string with validator " 371 | "key.\n" 372 | " show_manifest [hex|base64] Displays the last generated " 373 | "manifest\n" 374 | " set_domain Associate a domain with the " 375 | "validator key.\n" 376 | " clear_domain Disassociate a domain from a " 377 | "validator key.\n" 378 | " attest_domain Produce the attestation string " 379 | "for a domain.\n"; 380 | } 381 | // LCOV_EXCL_STOP 382 | 383 | std::string const& 384 | getVersionString() 385 | { 386 | static std::string const value = [] { 387 | std::string const s = versionString; 388 | beast::SemanticVersion v; 389 | if (!v.parse(s) || v.print() != s) 390 | throw std::logic_error( 391 | s + ": Bad version string"); // LCOV_EXCL_LINE 392 | return s; 393 | }(); 394 | return value; 395 | } 396 | 397 | int 398 | main(int argc, char** argv) 399 | { 400 | namespace po = boost::program_options; 401 | 402 | po::variables_map vm; 403 | 404 | // Set up option parsing. 405 | // 406 | po::options_description general("General Options"); 407 | general.add_options()("help,h", "Display this message.")( 408 | "keyfile", po::value(), "Specify the key file.")( 409 | "unittest,u", "Perform unit tests.")( 410 | "version", "Display the build version."); 411 | 412 | po::options_description hidden("Hidden options"); 413 | hidden.add_options()("command", po::value(), "Command.")( 414 | "arguments", 415 | po::value>()->default_value( 416 | std::vector(), "empty"), 417 | "Arguments."); 418 | po::positional_options_description p; 419 | p.add("command", 1).add("arguments", -1); 420 | 421 | po::options_description cmdline_options; 422 | cmdline_options.add(general).add(hidden); 423 | 424 | // Parse options, if no error. 425 | try 426 | { 427 | po::store( 428 | po::command_line_parser(argc, argv) 429 | .options(cmdline_options) // Parse options. 430 | .positional(p) 431 | .run(), 432 | vm); 433 | po::notify(vm); // Invoke option notify functions. 434 | } 435 | // LCOV_EXCL_START 436 | catch (std::exception const&) 437 | { 438 | std::cerr << "validator-keys: Incorrect command line syntax." 439 | << std::endl; 440 | std::cerr << "Use '--help' for a list of options." << std::endl; 441 | return EXIT_FAILURE; 442 | } 443 | // LCOV_EXCL_STOP 444 | 445 | // Run the unit tests if requested. 446 | // The unit tests will exit the application with an appropriate return code. 447 | if (vm.count("unittest")) 448 | return runUnitTests(); 449 | 450 | // LCOV_EXCL_START 451 | if (vm.count("version")) 452 | { 453 | std::cout << "validator-keys version " << getVersionString() 454 | << std::endl; 455 | return 0; 456 | } 457 | 458 | if (vm.count("help") || !vm.count("command")) 459 | { 460 | printHelp(general); 461 | return EXIT_SUCCESS; 462 | } 463 | 464 | std::string const homeDir = getEnvVar("HOME"); 465 | std::string const defaultKeyFile = 466 | (homeDir.empty() ? boost::filesystem::current_path().string() 467 | : homeDir) + 468 | "/.ripple/validator-keys.json"; 469 | 470 | try 471 | { 472 | using namespace boost::filesystem; 473 | path keyFile = vm.count("keyfile") ? vm["keyfile"].as() 474 | : defaultKeyFile; 475 | 476 | return runCommand( 477 | vm["command"].as(), 478 | vm["arguments"].as>(), 479 | keyFile); 480 | } 481 | catch (std::exception const& e) 482 | { 483 | std::cerr << e.what() << "\n"; 484 | return EXIT_FAILURE; 485 | } 486 | 487 | return EXIT_SUCCESS; 488 | // LCOV_EXCL_STOP 489 | } 490 | -------------------------------------------------------------------------------- /src/ValidatorKeysTool.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of validator-keys-tool: 4 | https://github.com/ripple/validator-keys-tool 5 | Copyright (c) 2016 Ripple Labs Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any 8 | purpose with or without fee is hereby granted, provided that the above 9 | copyright notice and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | //============================================================================== 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace boost { 26 | namespace filesystem { 27 | class path; 28 | } 29 | } // namespace boost 30 | 31 | std::string const& 32 | getVersionString(); 33 | 34 | void 35 | createKeyFile(boost::filesystem::path const& keyFile); 36 | 37 | void 38 | createToken(boost::filesystem::path const& keyFile); 39 | 40 | void 41 | createRevocation(boost::filesystem::path const& keyFile); 42 | 43 | void 44 | signData(std::string const& data, boost::filesystem::path const& keyFile); 45 | 46 | int 47 | runCommand( 48 | std::string const& command, 49 | std::vector const& arg, 50 | boost::filesystem::path const& keyFile); 51 | -------------------------------------------------------------------------------- /src/test/KeyFileGuard.h: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of rippled: https://github.com/ripple/rippled 4 | Copyright 2016 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | #include 21 | 22 | #include 23 | 24 | #include 25 | 26 | namespace ripple { 27 | 28 | /** 29 | Write a key file dir and remove when done. 30 | */ 31 | class KeyFileGuard 32 | { 33 | private: 34 | using path = boost::filesystem::path; 35 | path subDir_; 36 | beast::unit_test::suite& test_; 37 | 38 | auto 39 | rmDir(path const& toRm) 40 | { 41 | if (is_directory(toRm)) 42 | remove_all(toRm); 43 | else 44 | test_.log << "Expected " << toRm.string() 45 | << " to be an existing directory." << std::endl; 46 | }; 47 | 48 | public: 49 | KeyFileGuard(beast::unit_test::suite& test, std::string const& subDir) 50 | : subDir_(subDir), test_(test) 51 | { 52 | using namespace boost::filesystem; 53 | 54 | if (!exists(subDir_)) 55 | create_directory(subDir_); 56 | else 57 | // Cannot run the test. Someone created a file or directory 58 | // where we want to put our directory 59 | throw std::runtime_error( 60 | "Cannot create directory: " + subDir_.string()); 61 | } 62 | ~KeyFileGuard() 63 | { 64 | try 65 | { 66 | using namespace boost::filesystem; 67 | 68 | rmDir(subDir_); 69 | } 70 | catch (std::exception& e) 71 | { 72 | // if we throw here, just let it die. 73 | test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl; 74 | }; 75 | } 76 | }; 77 | 78 | } // namespace ripple 79 | -------------------------------------------------------------------------------- /src/test/ValidatorKeysTool_test.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of rippled: https://github.com/ripple/rippled 4 | Copyright 2016 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include 26 | 27 | namespace ripple { 28 | 29 | namespace tests { 30 | 31 | class ValidatorKeysTool_test : public beast::unit_test::suite 32 | { 33 | private: 34 | // Allow cout to be redirected. Destructor restores old cout streambuf. 35 | class CoutRedirect 36 | { 37 | public: 38 | CoutRedirect(std::stringstream& sStream) 39 | : old_(std::cout.rdbuf(sStream.rdbuf())) 40 | { 41 | } 42 | 43 | ~CoutRedirect() 44 | { 45 | std::cout.rdbuf(old_); 46 | } 47 | 48 | private: 49 | std::streambuf* const old_; 50 | }; 51 | 52 | void 53 | testCreateKeyFile() 54 | { 55 | testcase("Create Key File"); 56 | 57 | std::stringstream coutCapture; 58 | CoutRedirect coutRedirect{coutCapture}; 59 | 60 | using namespace boost::filesystem; 61 | 62 | path const subdir = "test_key_file"; 63 | KeyFileGuard const g(*this, subdir.string()); 64 | path const keyFile = subdir / "validator_keys.json"; 65 | 66 | createKeyFile(keyFile); 67 | BEAST_EXPECT(exists(keyFile)); 68 | 69 | std::string const expectedError = 70 | "Refusing to overwrite existing key file: " + keyFile.string(); 71 | std::string error; 72 | try 73 | { 74 | createKeyFile(keyFile); 75 | } 76 | catch (std::exception const& e) 77 | { 78 | error = e.what(); 79 | } 80 | BEAST_EXPECT(error == expectedError); 81 | } 82 | 83 | void 84 | testCreateToken() 85 | { 86 | testcase("Create Token"); 87 | 88 | std::stringstream coutCapture; 89 | CoutRedirect coutRedirect{coutCapture}; 90 | 91 | using namespace boost::filesystem; 92 | 93 | path const subdir = "test_key_file"; 94 | KeyFileGuard const g(*this, subdir.string()); 95 | path const keyFile = subdir / "validator_keys.json"; 96 | 97 | auto testToken = 98 | [this](path const& keyFile, std::string const& expectedError) { 99 | try 100 | { 101 | createToken(keyFile); 102 | BEAST_EXPECT(expectedError.empty()); 103 | } 104 | catch (std::exception const& e) 105 | { 106 | BEAST_EXPECT(e.what() == expectedError); 107 | } 108 | }; 109 | 110 | { 111 | std::string const expectedError = 112 | "Failed to open key file: " + keyFile.string(); 113 | testToken(keyFile, expectedError); 114 | } 115 | 116 | createKeyFile(keyFile); 117 | 118 | { 119 | std::string const expectedError = ""; 120 | testToken(keyFile, expectedError); 121 | } 122 | { 123 | auto const keyType = KeyType::ed25519; 124 | auto const kp = generateKeyPair(keyType, randomSeed()); 125 | 126 | auto keys = ValidatorKeys( 127 | keyType, 128 | kp.second, 129 | std::numeric_limits::max() - 1); 130 | 131 | keys.writeToFile(keyFile); 132 | std::string const expectedError = 133 | "Maximum number of tokens have already been generated.\n" 134 | "Revoke validator keys if previous token has been compromised."; 135 | testToken(keyFile, expectedError); 136 | } 137 | { 138 | createRevocation(keyFile); 139 | std::string const expectedError = 140 | "Validator keys have been revoked."; 141 | testToken(keyFile, expectedError); 142 | } 143 | } 144 | 145 | void 146 | testCreateRevocation() 147 | { 148 | testcase("Create Revocation"); 149 | 150 | std::stringstream coutCapture; 151 | CoutRedirect coutRedirect{coutCapture}; 152 | 153 | using namespace boost::filesystem; 154 | 155 | path const subdir = "test_key_file"; 156 | KeyFileGuard const g(*this, subdir.string()); 157 | path const keyFile = subdir / "validator_keys.json"; 158 | 159 | auto expectedError = "Failed to open key file: " + keyFile.string(); 160 | std::string error; 161 | try 162 | { 163 | createRevocation(keyFile); 164 | } 165 | catch (std::runtime_error& e) 166 | { 167 | error = e.what(); 168 | } 169 | BEAST_EXPECT(error == expectedError); 170 | 171 | createKeyFile(keyFile); 172 | BEAST_EXPECT(exists(keyFile)); 173 | 174 | createRevocation(keyFile); 175 | createRevocation(keyFile); 176 | } 177 | 178 | void 179 | testSign() 180 | { 181 | testcase("Sign"); 182 | 183 | std::stringstream coutCapture; 184 | CoutRedirect coutRedirect{coutCapture}; 185 | 186 | using namespace boost::filesystem; 187 | 188 | auto testSign = [this]( 189 | std::string const& data, 190 | path const& keyFile, 191 | std::string const& expectedError) { 192 | try 193 | { 194 | signData(data, keyFile); 195 | BEAST_EXPECT(expectedError.empty()); 196 | } 197 | catch (std::exception const& e) 198 | { 199 | BEAST_EXPECT(e.what() == expectedError); 200 | } 201 | }; 202 | 203 | std::string const data = "data to sign"; 204 | 205 | path const subdir = "test_key_file"; 206 | KeyFileGuard const g(*this, subdir.string()); 207 | path const keyFile = subdir / "validator_keys.json"; 208 | 209 | { 210 | std::string const expectedError = 211 | "Failed to open key file: " + keyFile.string(); 212 | testSign(data, keyFile, expectedError); 213 | } 214 | 215 | createKeyFile(keyFile); 216 | BEAST_EXPECT(exists(keyFile)); 217 | 218 | { 219 | std::string const emptyData = ""; 220 | std::string const expectedError = 221 | "Syntax error: Must specify data string to sign"; 222 | testSign(emptyData, keyFile, expectedError); 223 | } 224 | { 225 | std::string const expectedError = ""; 226 | testSign(data, keyFile, expectedError); 227 | } 228 | } 229 | 230 | void 231 | testRunCommand() 232 | { 233 | testcase("Run Command"); 234 | 235 | std::stringstream coutCapture; 236 | CoutRedirect coutRedirect{coutCapture}; 237 | 238 | using namespace boost::filesystem; 239 | 240 | path const subdir = "test_key_file"; 241 | KeyFileGuard g(*this, subdir.string()); 242 | path const keyFile = subdir / "validator_keys.json"; 243 | 244 | auto testCommand = [this]( 245 | std::string const& command, 246 | std::vector const& args, 247 | path const& keyFile, 248 | std::string const& expectedError) { 249 | try 250 | { 251 | runCommand(command, args, keyFile); 252 | BEAST_EXPECT(expectedError.empty()); 253 | } 254 | catch (std::exception const& e) 255 | { 256 | BEAST_EXPECT(e.what() == expectedError); 257 | } 258 | }; 259 | 260 | std::vector const noArgs; 261 | std::vector const oneArg = {"some data"}; 262 | std::vector const twoArgs = {"data", "more data"}; 263 | std::string const noError = ""; 264 | std::string const argError = "Syntax error: Wrong number of arguments"; 265 | { 266 | std::string const command = "unknown"; 267 | std::string const expectedError = "Unknown command: " + command; 268 | testCommand(command, noArgs, keyFile, expectedError); 269 | testCommand(command, oneArg, keyFile, expectedError); 270 | testCommand(command, twoArgs, keyFile, expectedError); 271 | } 272 | { 273 | std::string const command = "create_keys"; 274 | testCommand(command, noArgs, keyFile, noError); 275 | testCommand(command, oneArg, keyFile, argError); 276 | testCommand(command, twoArgs, keyFile, argError); 277 | } 278 | { 279 | std::string const command = "create_token"; 280 | testCommand(command, noArgs, keyFile, noError); 281 | testCommand(command, oneArg, keyFile, argError); 282 | testCommand(command, twoArgs, keyFile, argError); 283 | } 284 | { 285 | std::string const command = "revoke_keys"; 286 | testCommand(command, noArgs, keyFile, noError); 287 | testCommand(command, oneArg, keyFile, argError); 288 | testCommand(command, twoArgs, keyFile, argError); 289 | } 290 | { 291 | std::string const command = "sign"; 292 | testCommand(command, noArgs, keyFile, argError); 293 | testCommand(command, oneArg, keyFile, noError); 294 | testCommand(command, twoArgs, keyFile, argError); 295 | } 296 | } 297 | 298 | public: 299 | void 300 | run() override 301 | { 302 | getVersionString(); 303 | 304 | testCreateKeyFile(); 305 | testCreateToken(); 306 | testCreateRevocation(); 307 | testSign(); 308 | testRunCommand(); 309 | } 310 | }; 311 | 312 | BEAST_DEFINE_TESTSUITE(ValidatorKeysTool, keys, ripple); 313 | 314 | } // namespace tests 315 | 316 | } // namespace ripple 317 | -------------------------------------------------------------------------------- /src/test/ValidatorKeys_test.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | /* 3 | This file is part of rippled: https://github.com/ripple/rippled 4 | Copyright 2016 Ripple Labs Inc. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | //============================================================================== 19 | 20 | #include 21 | 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace ripple { 30 | 31 | namespace tests { 32 | 33 | class ValidatorKeys_test : public beast::unit_test::suite 34 | { 35 | private: 36 | void 37 | testKeyFile( 38 | boost::filesystem::path const& keyFile, 39 | Json::Value const& jv, 40 | std::string const& expectedError) 41 | { 42 | { 43 | std::ofstream o(keyFile.string(), std::ios_base::trunc); 44 | o << jv.toStyledString(); 45 | o.close(); 46 | } 47 | 48 | try 49 | { 50 | ValidatorKeys::make_ValidatorKeys(keyFile); 51 | BEAST_EXPECT(expectedError.empty()); 52 | } 53 | catch (std::runtime_error& e) 54 | { 55 | BEAST_EXPECT(e.what() == expectedError); 56 | } 57 | } 58 | 59 | std::array const keyTypes{ 60 | {KeyType::ed25519, KeyType::secp256k1}}; 61 | 62 | void 63 | testMakeValidatorKeys() 64 | { 65 | testcase("Make Validator Keys"); 66 | 67 | using namespace boost::filesystem; 68 | 69 | path const subdir = "test_key_file"; 70 | path const keyFile = subdir / "validator_keys.json"; 71 | 72 | for (auto const keyType : keyTypes) 73 | { 74 | ValidatorKeys const keys(keyType); 75 | 76 | KeyFileGuard const g(*this, subdir.string()); 77 | 78 | keys.writeToFile(keyFile); 79 | BEAST_EXPECT(exists(keyFile)); 80 | 81 | auto const keys2 = ValidatorKeys::make_ValidatorKeys(keyFile); 82 | BEAST_EXPECT(keys == keys2); 83 | } 84 | { 85 | // Require expected fields 86 | KeyFileGuard g(*this, subdir.string()); 87 | 88 | auto expectedError = "Failed to open key file: " + keyFile.string(); 89 | std::string error; 90 | try 91 | { 92 | ValidatorKeys::make_ValidatorKeys(keyFile); 93 | } 94 | catch (std::runtime_error& e) 95 | { 96 | error = e.what(); 97 | } 98 | BEAST_EXPECT(error == expectedError); 99 | 100 | expectedError = 101 | "Unable to parse json key file: " + keyFile.string(); 102 | 103 | { 104 | std::ofstream o(keyFile.string(), std::ios_base::trunc); 105 | o << "{{}"; 106 | o.close(); 107 | } 108 | 109 | try 110 | { 111 | ValidatorKeys::make_ValidatorKeys(keyFile); 112 | } 113 | catch (std::runtime_error& e) 114 | { 115 | error = e.what(); 116 | } 117 | BEAST_EXPECT(error == expectedError); 118 | 119 | Json::Value jv; 120 | jv["dummy"] = "field"; 121 | expectedError = "Key file '" + keyFile.string() + 122 | "' is missing \"key_type\" field"; 123 | testKeyFile(keyFile, jv, expectedError); 124 | 125 | jv["key_type"] = "dummy keytype"; 126 | expectedError = "Key file '" + keyFile.string() + 127 | "' is missing \"secret_key\" field"; 128 | testKeyFile(keyFile, jv, expectedError); 129 | 130 | jv["secret_key"] = "dummy secret"; 131 | expectedError = "Key file '" + keyFile.string() + 132 | "' is missing \"token_sequence\" field"; 133 | testKeyFile(keyFile, jv, expectedError); 134 | 135 | jv["token_sequence"] = "dummy sequence"; 136 | expectedError = "Key file '" + keyFile.string() + 137 | "' is missing \"revoked\" field"; 138 | testKeyFile(keyFile, jv, expectedError); 139 | 140 | jv["revoked"] = "dummy revoked"; 141 | expectedError = "Key file '" + keyFile.string() + 142 | "' contains invalid \"key_type\" field: " + 143 | jv["key_type"].toStyledString(); 144 | testKeyFile(keyFile, jv, expectedError); 145 | 146 | auto const keyType = KeyType::ed25519; 147 | jv["key_type"] = to_string(keyType); 148 | expectedError = "Key file '" + keyFile.string() + 149 | "' contains invalid \"secret_key\" field: " + 150 | jv["secret_key"].toStyledString(); 151 | testKeyFile(keyFile, jv, expectedError); 152 | 153 | ValidatorKeys const keys(keyType); 154 | { 155 | auto const kp = generateKeyPair(keyType, randomSeed()); 156 | jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second); 157 | } 158 | expectedError = "Key file '" + keyFile.string() + 159 | "' contains invalid \"token_sequence\" field: " + 160 | jv["token_sequence"].toStyledString(); 161 | testKeyFile(keyFile, jv, expectedError); 162 | 163 | jv["token_sequence"] = -1; 164 | expectedError = "Key file '" + keyFile.string() + 165 | "' contains invalid \"token_sequence\" field: " + 166 | jv["token_sequence"].toStyledString(); 167 | testKeyFile(keyFile, jv, expectedError); 168 | 169 | jv["token_sequence"] = 170 | Json::UInt(std::numeric_limits::max()); 171 | expectedError = "Key file '" + keyFile.string() + 172 | "' contains invalid \"revoked\" field: " + 173 | jv["revoked"].toStyledString(); 174 | testKeyFile(keyFile, jv, expectedError); 175 | 176 | jv["revoked"] = false; 177 | expectedError = ""; 178 | testKeyFile(keyFile, jv, expectedError); 179 | 180 | jv["revoked"] = true; 181 | testKeyFile(keyFile, jv, expectedError); 182 | } 183 | } 184 | 185 | void 186 | testCreateValidatorToken() 187 | { 188 | testcase("Create Validator Token"); 189 | 190 | for (auto const keyType : keyTypes) 191 | { 192 | ValidatorKeys keys(keyType); 193 | std::uint32_t sequence = 0; 194 | 195 | for (auto const tokenKeyType : keyTypes) 196 | { 197 | auto const token = keys.createValidatorToken(tokenKeyType); 198 | 199 | if (!BEAST_EXPECT(token)) 200 | continue; 201 | 202 | auto const tokenPublicKey = 203 | derivePublicKey(tokenKeyType, token->secretKey); 204 | 205 | STObject st(sfGeneric); 206 | auto const manifest = ripple::base64_decode(token->manifest); 207 | SerialIter sit(manifest.data(), manifest.size()); 208 | st.set(sit); 209 | 210 | auto const seq = get(st, sfSequence); 211 | BEAST_EXPECT(seq); 212 | BEAST_EXPECT(*seq == ++sequence); 213 | 214 | auto const tpk = get(st, sfSigningPubKey); 215 | BEAST_EXPECT(tpk); 216 | BEAST_EXPECT(*tpk == tokenPublicKey); 217 | BEAST_EXPECT(verify(st, HashPrefix::manifest, tokenPublicKey)); 218 | 219 | auto const pk = get(st, sfPublicKey); 220 | BEAST_EXPECT(pk); 221 | BEAST_EXPECT(*pk == keys.publicKey()); 222 | BEAST_EXPECT(verify( 223 | st, 224 | HashPrefix::manifest, 225 | keys.publicKey(), 226 | sfMasterSignature)); 227 | } 228 | } 229 | 230 | auto const keyType = KeyType::ed25519; 231 | auto const kp = generateKeyPair(keyType, randomSeed()); 232 | 233 | auto keys = ValidatorKeys( 234 | keyType, kp.second, std::numeric_limits::max() - 1); 235 | 236 | BEAST_EXPECT(!keys.createValidatorToken(keyType)); 237 | 238 | keys.revoke(); 239 | BEAST_EXPECT(!keys.createValidatorToken(keyType)); 240 | } 241 | 242 | void 243 | testRevoke() 244 | { 245 | testcase("Revoke"); 246 | 247 | for (auto const keyType : keyTypes) 248 | { 249 | ValidatorKeys keys(keyType); 250 | 251 | auto const revocation = keys.revoke(); 252 | 253 | STObject st(sfGeneric); 254 | auto const manifest = ripple::base64_decode(revocation); 255 | SerialIter sit(manifest.data(), manifest.size()); 256 | st.set(sit); 257 | 258 | auto const seq = get(st, sfSequence); 259 | BEAST_EXPECT(seq); 260 | BEAST_EXPECT(*seq == std::numeric_limits::max()); 261 | 262 | auto const pk = get(st, sfPublicKey); 263 | BEAST_EXPECT(pk); 264 | BEAST_EXPECT(*pk == keys.publicKey()); 265 | BEAST_EXPECT(verify( 266 | st, HashPrefix::manifest, keys.publicKey(), sfMasterSignature)); 267 | } 268 | } 269 | 270 | void 271 | testSign() 272 | { 273 | testcase("Sign"); 274 | 275 | std::map expected( 276 | {{KeyType::ed25519, 277 | "2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F" 278 | "237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939" 279 | "C8F29B0DC759FD712300"}, 280 | {KeyType::secp256k1, 281 | "3045022100F142C27BF83D8D4541C7A4E759DE64A672" 282 | "51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5" 283 | "55438D64A229CD0B4BA2AE33402443B20409"}}); 284 | 285 | std::string const data = "data to sign"; 286 | 287 | for (auto const keyType : keyTypes) 288 | { 289 | auto const sk = generateSecretKey(keyType, generateSeed("test")); 290 | ValidatorKeys keys(keyType, sk, 1); 291 | 292 | auto const signature = keys.sign(data); 293 | BEAST_EXPECT(expected[keyType] == signature); 294 | 295 | auto const ret = strUnHex(signature); 296 | BEAST_EXPECT(ret); 297 | BEAST_EXPECT(ret->size()); 298 | BEAST_EXPECT( 299 | verify(keys.publicKey(), makeSlice(data), makeSlice(*ret))); 300 | } 301 | } 302 | 303 | void 304 | testWriteToFile() 305 | { 306 | testcase("Write to File"); 307 | 308 | using namespace boost::filesystem; 309 | 310 | auto const keyType = KeyType::ed25519; 311 | ValidatorKeys keys(keyType); 312 | 313 | { 314 | path const subdir = "test_key_file"; 315 | path const keyFile = subdir / "validator_keys.json"; 316 | KeyFileGuard g(*this, subdir.string()); 317 | 318 | keys.writeToFile(keyFile); 319 | BEAST_EXPECT(exists(keyFile)); 320 | 321 | auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); 322 | BEAST_EXPECT(keys == fileKeys); 323 | 324 | // Overwrite file with new sequence 325 | keys.createValidatorToken(KeyType::secp256k1); 326 | keys.writeToFile(keyFile); 327 | 328 | fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); 329 | BEAST_EXPECT(keys == fileKeys); 330 | } 331 | { 332 | // Write to key file in current relative directory 333 | path const keyFile = "test_validator_keys.json"; 334 | if (!exists(keyFile)) 335 | { 336 | keys.writeToFile(keyFile); 337 | remove(keyFile.string()); 338 | } 339 | else 340 | { 341 | // Cannot run the test. Someone created a file 342 | // where we want to put our key file 343 | Throw( 344 | "Cannot create key file: " + keyFile.string()); 345 | } 346 | } 347 | { 348 | // Create key file directory 349 | path const subdir = "test_key_file"; 350 | path const keyFile = 351 | subdir / "directories/to/create/validator_keys.json"; 352 | KeyFileGuard g(*this, subdir.string()); 353 | 354 | keys.writeToFile(keyFile); 355 | BEAST_EXPECT(exists(keyFile)); 356 | 357 | auto const fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); 358 | BEAST_EXPECT(keys == fileKeys); 359 | } 360 | { 361 | // Fail if file cannot be opened for write 362 | path const subdir = "test_key_file"; 363 | KeyFileGuard g(*this, subdir.string()); 364 | 365 | path const badKeyFile = subdir / "."; 366 | auto expectedError = "Cannot open key file: " + badKeyFile.string(); 367 | std::string error; 368 | try 369 | { 370 | keys.writeToFile(badKeyFile); 371 | } 372 | catch (std::runtime_error& e) 373 | { 374 | error = e.what(); 375 | } 376 | BEAST_EXPECT(error == expectedError); 377 | 378 | // Fail if parent directory is existing file 379 | path const keyFile = subdir / "validator_keys.json"; 380 | keys.writeToFile(keyFile); 381 | path const conflictingPath = keyFile / "validators_keys.json"; 382 | expectedError = "Cannot create directory: " + 383 | conflictingPath.parent_path().string(); 384 | try 385 | { 386 | keys.writeToFile(conflictingPath); 387 | } 388 | catch (std::runtime_error& e) 389 | { 390 | error = e.what(); 391 | } 392 | BEAST_EXPECT(error == expectedError); 393 | } 394 | } 395 | 396 | public: 397 | void 398 | run() override 399 | { 400 | testMakeValidatorKeys(); 401 | testCreateValidatorToken(); 402 | testRevoke(); 403 | testSign(); 404 | testWriteToFile(); 405 | } 406 | }; 407 | 408 | BEAST_DEFINE_TESTSUITE(ValidatorKeys, keys, ripple); 409 | 410 | } // namespace tests 411 | 412 | } // namespace ripple 413 | --------------------------------------------------------------------------------