diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index a9b85b766a7..faac3b100ff 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -50,7 +50,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "extra_cmake_args": "-Dvalidator_keys=ON" } ], @@ -58,7 +59,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "extra_cmake_args": "-Dvalidator_keys=ON" } ] }, diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93dc..e30133bc744 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -67,6 +67,7 @@ jobs: .github/workflows/reusable-package.yml .github/workflows/reusable-strategy-matrix.yml .github/workflows/reusable-test.yml + .github/workflows/reusable-test-conan-package.yml .github/workflows/reusable-upload-recipe.yml .clang-tidy .codecov.yml @@ -77,6 +78,7 @@ jobs: include/** src/** tests/** + validator-keys-tool/** CMakeLists.txt conanfile.py conan.lock @@ -145,10 +147,16 @@ jobs: if: ${{ needs.should-run.outputs.go == 'true' }} uses: ./.github/workflows/reusable-package.yml + test-conan-package: + needs: should-run + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-test-conan-package.yml + upload-recipe: needs: - should-run - build-test + - test-conan-package # Only run when committing to a PR that targets a release branch. if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }} uses: ./.github/workflows/reusable-upload-recipe.yml @@ -180,6 +188,7 @@ jobs: - clang-tidy - build-test - package + - test-conan-package - upload-recipe - notify-clio runs-on: ubuntu-latest diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 42d5827cab0..d6d64597f4d 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -16,8 +16,13 @@ defaults: shell: bash jobs: + test-conan-package: + if: ${{ github.repository == 'XRPLF/rippled' }} + uses: ./.github/workflows/reusable-test-conan-package.yml + upload-recipe: if: ${{ github.repository == 'XRPLF/rippled' }} + needs: test-conan-package uses: ./.github/workflows/reusable-upload-recipe.yml secrets: remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }} diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 74bca820198..3659601c326 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -24,6 +24,7 @@ on: - ".github/workflows/reusable-package.yml" - ".github/workflows/reusable-strategy-matrix.yml" - ".github/workflows/reusable-test.yml" + - ".github/workflows/reusable-test-conan-package.yml" - ".github/workflows/reusable-upload-recipe.yml" - ".clang-tidy" - ".codecov.yml" @@ -34,6 +35,7 @@ on: - "include/**" - "src/**" - "tests/**" + - "validator-keys-tool/**" - "CMakeLists.txt" - "conanfile.py" - "conan.lock" @@ -91,8 +93,13 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-conan-package: + uses: ./.github/workflows/reusable-test-conan-package.yml + upload-recipe: - needs: build-test + needs: + - build-test + - test-conan-package # Only run when pushing to the develop branch. if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }} uses: ./.github/workflows/reusable-upload-recipe.yml diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 95e6b0cbe29..ba52eae7804 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -235,6 +235,9 @@ jobs: run: | loader="$(/tmp/loader-path.sh)" patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/xrpld" + if [ -x "${{ env.BUILD_DIR }}/validator-keys" ]; then + patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/validator-keys" + fi # We're only running aarch64 Linux builds in Ubuntu-based images, so this is kept simple - name: Install libatomic (Linux aarch64) @@ -253,12 +256,21 @@ jobs: curl ${CCACHE_REMOTE_STORAGE%|*}/status || true fi + - name: Stage binary artifacts (Linux) + if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }} + run: | + mkdir -p "${BUILD_DIR}/artifacts" + cp "${BUILD_DIR}/xrpld" "${BUILD_DIR}/artifacts/xrpld" + if [ -x "${BUILD_DIR}/validator-keys" ]; then + cp "${BUILD_DIR}/validator-keys" "${BUILD_DIR}/artifacts/validator-keys" + fi + - name: Upload the binary (Linux) if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: xrpld-${{ inputs.config_name }} - path: ${{ env.BUILD_DIR }}/xrpld + path: ${{ env.BUILD_DIR }}/artifacts/* retention-days: 3 if-no-files-found: error diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 0e3f6570066..25409a1278b 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -77,8 +77,8 @@ jobs: name: ${{ matrix.artifact_name }} path: ${{ env.BUILD_DIR }} - - name: Make binary executable - run: chmod +x "${BUILD_DIR}/xrpld" + - name: Make binaries executable + run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys" - name: Build package env: diff --git a/.github/workflows/reusable-test-conan-package.yml b/.github/workflows/reusable-test-conan-package.yml new file mode 100644 index 00000000000..3ae4a109d5c --- /dev/null +++ b/.github/workflows/reusable-test-conan-package.yml @@ -0,0 +1,35 @@ +# Build the Conan package and run the consumer test package. +name: Test Conan package + +# This workflow can only be triggered by other workflows. +on: + workflow_call: + +defaults: + run: + shell: bash + +jobs: + test-conan-package: + runs-on: ubuntu-latest + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3 + timeout-minutes: 90 + + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Conan + uses: ./.github/actions/setup-conan + + - name: Export Conan package under test + run: conan export . --version=head + + - name: Run Conan package test + working-directory: tests/conan + run: | + conan test . xrpl/head \ + --profile:all ci \ + --build=missing \ + --settings:all build_type=Release \ + --conf:all tools.build:jobs="$(nproc)" diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dbe60a220e..a9988cbab71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,9 +133,9 @@ endif() include(XrplCore) include(XrplProtocolAutogen) +include(XrplValidatorKeys) include(XrplInstall) include(XrplPackaging) -include(XrplValidatorKeys) if(tests) include(CTest) diff --git a/cmake/XrplPackaging.cmake b/cmake/XrplPackaging.cmake index fe885c200c8..972002afcb8 100644 --- a/cmake/XrplPackaging.cmake +++ b/cmake/XrplPackaging.cmake @@ -25,6 +25,19 @@ if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE)) return() endif() +if(NOT TARGET xrpld) + message(STATUS "xrpld=ON is required; 'package' target not available") + return() +endif() + +if(NOT TARGET validator-keys) + message( + STATUS + "validator_keys=ON is required; 'package' target not available" + ) + return() +endif() + set(package_env SRC_DIR=${CMAKE_SOURCE_DIR} BUILD_DIR=${CMAKE_BINARY_DIR} @@ -38,7 +51,7 @@ add_custom_target( ${CMAKE_COMMAND} -E env ${package_env} ${CMAKE_SOURCE_DIR}/package/build_pkg.sh WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS xrpld + DEPENDS xrpld validator-keys COMMENT "Building Linux package (deb/rpm inferred from host tooling)" VERBATIM ) diff --git a/cmake/XrplValidatorKeys.cmake b/cmake/XrplValidatorKeys.cmake index 0e511b6a88b..253b72ead44 100644 --- a/cmake/XrplValidatorKeys.cmake +++ b/cmake/XrplValidatorKeys.cmake @@ -1,26 +1,22 @@ option( validator_keys - "Enables building of validator-keys tool as a separate target (imported via FetchContent)" + "Enables building of the vendored validator-keys tool as a separate target" OFF ) if(validator_keys) - git_branch(current_branch) - # default to tracking VK master branch unless we are on release - if(NOT (current_branch STREQUAL "release")) - set(current_branch "master") - endif() - message(STATUS "Tracking ValidatorKeys branch: ${current_branch}") + include(GNUInstallDirs) - FetchContent_Declare( - validator_keys - GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git - GIT_TAG "${current_branch}" + add_subdirectory( + "${CMAKE_SOURCE_DIR}/validator-keys-tool" + "${CMAKE_BINARY_DIR}/validator-keys-tool" ) - FetchContent_MakeAvailable(validator_keys) set_target_properties( validator-keys PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) - install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install( + TARGETS validator-keys + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime + ) endif() diff --git a/package/README.md b/package/README.md index 63c2ab88fc0..7ebc4b79ba2 100644 --- a/package/README.md +++ b/package/README.md @@ -1,6 +1,7 @@ # Linux Packaging -This directory contains all files needed to build RPM and Debian packages for `xrpld`. +This directory contains all files needed to build RPM and Debian packages for +`xrpld`. The packages also include the `validator-keys` utility. ## Directory layout @@ -48,16 +49,17 @@ To print the exact image tags for the current `linux.json`: Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call `reusable-strategy-matrix.yml` with `mode: packaging` to generate the matrix of `{artifact_name, os}` entries, then fan out to -`reusable-package.yml` per entry. That workflow downloads the pre-built `xrpld` -binary artifact, detects the package format from the container, and calls -`build_pkg.sh` directly — no CMake configure or build step is needed inside -the packaging job. +`reusable-package.yml` per entry. That workflow downloads the pre-built binary +artifact containing `xrpld` and `validator-keys`, detects the package format +from the container, and calls `build_pkg.sh` directly — no CMake configure or +build step is needed inside the packaging job. ### Locally (mirrors CI) -With an `xrpld` binary already built at `build/xrpld`, run the packaging step -inside the same container CI uses. The image tag is derived from `linux.json` -so you don't need to hardcode a SHA. +With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and +`build/validator-keys`, run the packaging step inside the same container CI +uses. The image tag is derived from `linux.json` so you don't need to hardcode a +SHA. ```bash # From the repo root. Pick any image flagged with `"package": true` in @@ -91,6 +93,7 @@ needed, but the host toolchain replaces the pinned CI image: ```bash cmake \ -Dxrpld=ON \ + -Dvalidator_keys=ON \ -Dxrpld_version=2.4.0-local \ -Dtests=OFF \ .. @@ -110,13 +113,13 @@ to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of environment variable. Flags override env vars; env vars override the built-in defaults. Run `./package/build_pkg.sh --help` for the same table: -| Flag | Env var | Default | Purpose | -| -------------------------- | ------------------- | ----------------------------- | ----------------------------------- | -| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root | -| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built `xrpld` | -| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` | -| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number | -| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp | +| Flag | Env var | Default | Purpose | +| -------------------------- | ------------------- | ----------------------------- | ------------------------------------ | +| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root | +| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built binaries | +| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` | +| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number | +| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp | The package format (`deb` or `rpm`) is inferred from the host's package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those @@ -141,7 +144,7 @@ into the staging area, and invokes the platform build tool. ### DEB 1. Creates a staging source tree at `debbuild/source/` inside the build directory. -2. Stages the binary, configs, `README.md`, and `LICENSE.md`. +2. Stages the binaries, configs, `README.md`, and `LICENSE.md`. 3. Copies `package/debian/` control files into `debbuild/source/debian/`. 4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically. 5. Generates a minimal `debian/changelog` (pre-release versions use `~` instead of `-`). diff --git a/package/build_pkg.sh b/package/build_pkg.sh index e2ec8fee3d5..b9d79a1662f 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# Build an RPM or Debian package from a pre-built xrpld binary. +# Build an RPM or Debian package from pre-built xrpld and validator-keys +# binaries. # # Flags override env vars; env vars override defaults. Env vars are intended # for CMake/systemd/CI integration; flags are for explicit invocation. @@ -12,7 +13,7 @@ Usage: build_pkg.sh [options] Options (each can also be set via the env var shown): --src-dir DIR repo root [SRC_DIR; default: $PWD] - --build-dir DIR directory holding xrpld [BUILD_DIR; default: $PWD/build] + --build-dir DIR directory holding binaries [BUILD_DIR; default: $PWD/build] --pkg-version STR version, e.g. 3.2.0-b1 [PKG_VERSION; default: parsed from xrpld --version] --pkg-release N package release number [PKG_RELEASE; default: 1] --source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; default: latest git commit ctime] @@ -134,6 +135,7 @@ stage_common() { mkdir -p "${dest}" cp "${BUILD_DIR}/xrpld" "${dest}/xrpld" + cp "${BUILD_DIR}/validator-keys" "${dest}/validator-keys" cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg" cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt" cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md" diff --git a/package/debian/control b/package/debian/control index 45d2acbbea5..570765024fb 100644 --- a/package/debian/control +++ b/package/debian/control @@ -20,4 +20,5 @@ Depends: Description: XRP Ledger daemon Reference implementation of the XRP Ledger protocol. Participates in the peer-to-peer network, processes transactions, - and maintains a local ledger copy. + and maintains a local ledger copy. Includes validator-keys for + validator key management. diff --git a/package/debian/rules b/package/debian/rules index 16574bca3fd..8f880b81927 100644 --- a/package/debian/rules +++ b/package/debian/rules @@ -18,6 +18,7 @@ override_dh_installsysusers: override_dh_install: install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld + install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec index 5595fd0d8da..33856200c03 100644 --- a/package/rpm/xrpld.spec +++ b/package/rpm/xrpld.spec @@ -21,6 +21,8 @@ BuildRequires: systemd-rpm-macros xrpld is the reference implementation of the XRP Ledger protocol. It participates in the peer-to-peer XRP Ledger network, processes transactions, and maintains the ledger database. +This package also includes the validator-keys tool for validator key +management. %prep : @@ -30,6 +32,7 @@ transactions, and maintains the ledger database. %install install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name} +install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt @@ -71,6 +74,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %dir %{_sysconfdir}/%{name} %{_bindir}/%{name} +%{_bindir}/validator-keys %config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg %config(noreplace) %{_sysconfdir}/%{name}/validators.txt diff --git a/tests/conan/.gitignore b/tests/conan/.gitignore new file mode 100644 index 00000000000..6f06fd1f5da --- /dev/null +++ b/tests/conan/.gitignore @@ -0,0 +1,2 @@ +# Conan test_package build output (cmake_layout) +/build/ diff --git a/tests/conan/CMakeLists.txt b/tests/conan/CMakeLists.txt index 871c4c60c01..8e3d8301417 100644 --- a/tests/conan/CMakeLists.txt +++ b/tests/conan/CMakeLists.txt @@ -1,12 +1,21 @@ cmake_minimum_required(VERSION 3.21) -set(name example) -set(version 0.1.0) +set(name validator-keys-conan-test) -project(${name} VERSION ${version} LANGUAGES CXX) +project(${name} LANGUAGES CXX) find_package(xrpl CONFIG REQUIRED) -add_executable(example) -target_sources(example PRIVATE src/example.cpp) -target_link_libraries(example PRIVATE xrpl::libxrpl) +# Build the in-repo validator-keys-tool source instead of fetching it from +# GitHub. Keep it out of the default build; the test recipe builds the target +# explicitly. +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/../../validator-keys-tool + ${CMAKE_BINARY_DIR}/validator-keys-tool + EXCLUDE_FROM_ALL +) + +set_target_properties( + validator-keys + PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) diff --git a/tests/conan/conanfile.py b/tests/conan/conanfile.py index 71e43ef25a1..a4d67ba4b7b 100644 --- a/tests/conan/conanfile.py +++ b/tests/conan/conanfile.py @@ -1,4 +1,4 @@ -from pathlib import Path +import os from conan.tools.build import can_run from conan.tools.cmake import CMake, cmake_layout @@ -6,15 +6,13 @@ from conan import ConanFile -class Example(ConanFile): - name = "example" +class ValidatorKeysConanTest(ConanFile): + name = "validator-keys-conan-test" license = "ISC" author = "John Freeman , Michael Legleux - -#include - -int -main(int argc, char const** argv) -{ - std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str()); - return 0; -} diff --git a/validator-keys-tool/.git-blame-ignore-revs b/validator-keys-tool/.git-blame-ignore-revs new file mode 100644 index 00000000000..1ad2552abdf --- /dev/null +++ b/validator-keys-tool/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# This feature requires Git >= 2.24 +# To use it by default in git blame: +# git config blame.ignoreRevsFile .git-blame-ignore-revs +8ae260cb466d4cd0d4db378e5ce0acb8e4432f7c diff --git a/validator-keys-tool/CMakeLists.txt b/validator-keys-tool/CMakeLists.txt new file mode 100644 index 00000000000..4504b7c2a28 --- /dev/null +++ b/validator-keys-tool/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.11) +project(validator-keys-tool) + +#[===========================================[ + This project is built as part of the xrpld + repository's Conan test package. The parent + project calls find_package(xrpl) and adds this + directory, providing the xrpl::libxrpl target. +#]===========================================] +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +if(NOT TARGET xrpl::libxrpl) + find_package(xrpl CONFIG REQUIRED) +endif() + +include(KeysSanity) +include(KeysCov) +include(KeysInterface) + +add_executable( + validator-keys + src/ValidatorKeys.cpp + src/ValidatorKeysTool.cpp + # UNIT TESTS: + src/test/ValidatorKeys_test.cpp + src/test/ValidatorKeysTool_test.cpp +) +target_include_directories(validator-keys PRIVATE src) +target_link_libraries(validator-keys xrpl::libxrpl Keys::opts) + +include(CTest) +if(BUILD_TESTING) + add_test(test validator-keys --unittest) +endif() diff --git a/validator-keys-tool/README.md b/validator-keys-tool/README.md new file mode 100644 index 00000000000..80d75613fb1 --- /dev/null +++ b/validator-keys-tool/README.md @@ -0,0 +1,28 @@ +# validator-keys-tool + +Xrpld validator key generation tool + +## Build + +Build from the xrpld repository root, enabling the optional +`validator_keys` target: + +``` +mkdir .build +cd .build +conan install .. --output-folder . --build missing --settings build_type=Release +cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \ + -DCMAKE_TOOLCHAIN_FILE:FILEPATH=conan_toolchain.cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -Dvalidator_keys=ON \ + .. +cmake --build . --target validator-keys +./validator-keys --unittest +``` + +The Conan test package in `tests/conan` builds the same target against the +exported `xrpl` package. + +## Guide + +[Validator Keys Tool Guide](doc/validator-keys-tool-guide.md) diff --git a/validator-keys-tool/cmake/KeysCov.cmake b/validator-keys-tool/cmake/KeysCov.cmake new file mode 100644 index 00000000000..987af0fea4c --- /dev/null +++ b/validator-keys-tool/cmake/KeysCov.cmake @@ -0,0 +1,137 @@ +#[===================================================================[ + coverage report target + + Copied from rippled https://github.com/ripple/rippled/blob/develop/Builds/CMake/RippledCov.cmake +#]===================================================================] + +# cspell: words xcrun + +if(coverage) + if(is_clang) + if(APPLE) + execute_process( + COMMAND xcrun -f llvm-profdata + OUTPUT_VARIABLE LLVM_PROFDATA + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + else() + find_program(LLVM_PROFDATA llvm-profdata) + endif() + if(NOT LLVM_PROFDATA) + message( + WARNING + "unable to find llvm-profdata - skipping coverage_report target" + ) + endif() + + if(APPLE) + execute_process( + COMMAND xcrun -f llvm-cov + OUTPUT_VARIABLE LLVM_COV + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + else() + find_program(LLVM_COV llvm-cov) + endif() + if(NOT LLVM_COV) + message( + WARNING + "unable to find llvm-cov - skipping coverage_report target" + ) + endif() + + set(extract_pattern "") + if(coverage_core_only) + set(extract_pattern "${CMAKE_CURRENT_SOURCE_DIR}/src/") + endif() + + if(LLVM_COV AND LLVM_PROFDATA) + add_custom_target( + coverage_report + USES_TERMINAL + COMMAND + ${CMAKE_COMMAND} -E echo + "Generating coverage - results will be in ${CMAKE_BINARY_DIR}/coverage/index.html." + COMMAND ${CMAKE_COMMAND} -E echo "Running validator-keys tests." + COMMAND + validator-keys + --unittest$<$:=${coverage_test}> + COMMAND + ${LLVM_PROFDATA} merge -sparse default.profraw -o + rip.profdata + COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:" + COMMAND + ${LLVM_COV} report -instr-profile=rip.profdata + $ ${extract_pattern} + # generate html report + COMMAND + ${LLVM_COV} show -format=html + -output-dir=${CMAKE_BINARY_DIR}/coverage + -instr-profile=rip.profdata $ + ${extract_pattern} + BYPRODUCTS coverage/index.html + ) + endif() + elseif(is_gcc) + find_program(LCOV lcov) + if(NOT LCOV) + message( + WARNING + "unable to find lcov - skipping coverage_report target" + ) + endif() + + find_program(GENHTML genhtml) + if(NOT GENHTML) + message( + WARNING + "unable to find genhtml - skipping coverage_report target" + ) + endif() + + set(extract_pattern "*") + if(coverage_core_only) + set(extract_pattern "*/src/*") + endif() + + if(LCOV AND GENHTML) + add_custom_target( + coverage_report + USES_TERMINAL + COMMAND + ${CMAKE_COMMAND} -E echo + "Generating coverage- results will be in ${CMAKE_BINARY_DIR}/coverage/index.html." + # create baseline info file + COMMAND + ${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d + . -i -o baseline.info | grep -v + "ignoring data for external file" + # run tests + COMMAND + ${CMAKE_COMMAND} -E echo + "Running validator-keys tests for coverage report." + COMMAND + validator-keys + --unittest$<$:=${coverage_test}> + # Create test coverage data file + COMMAND + ${LCOV} --no-external -d "${CMAKE_CURRENT_SOURCE_DIR}" -c -d + . -o tests.info | grep -v "ignoring data for external file" + # Combine baseline and test coverage data + COMMAND ${LCOV} -a baseline.info -a tests.info -o lcov-all.info + # extract our files + COMMAND + ${LCOV} -e lcov-all.info "${extract_pattern}" -o lcov.info + COMMAND ${CMAKE_COMMAND} -E echo "Summary of coverage:" + COMMAND ${LCOV} --summary lcov.info + # generate HTML report + COMMAND ${GENHTML} -o ${CMAKE_BINARY_DIR}/coverage lcov.info + BYPRODUCTS coverage/index.html + ) + endif() + else() + message(STATUS "Coverage: neither clang nor gcc") + endif() +else() + message(STATUS "Coverage disabled") +endif() diff --git a/validator-keys-tool/cmake/KeysInterface.cmake b/validator-keys-tool/cmake/KeysInterface.cmake new file mode 100644 index 00000000000..f75840814b1 --- /dev/null +++ b/validator-keys-tool/cmake/KeysInterface.cmake @@ -0,0 +1,87 @@ +#[===================================================================[ + rippled compile options/settings via an interface library +#]===================================================================] + +# cspell: words Wsuggest fprofile ftest + +add_library(keys_opts INTERFACE) +add_library(Keys::opts ALIAS keys_opts) +target_compile_definitions( + keys_opts + INTERFACE + BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS + $<$: + BOOST_ASIO_NO_DEPRECATED + BOOST_FILESYSTEM_NO_DEPRECATED + > + $<$>: + BOOST_COROUTINES_NO_DEPRECATION_WARNING + BOOST_BEAST_ALLOW_DEPRECATED + BOOST_FILESYSTEM_DEPRECATED + > + $<$: + USE_BEAST_HASHER + > + $<$:BEAST_NO_UNIT_TEST_INLINE=1> + $<$:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1> + $<$:XRPL_SINGLE_IO_SERVICE_THREAD=1> +) +target_compile_options( + keys_opts + INTERFACE + $<$,$>:-Wsuggest-override> + $<$:-fno-omit-frame-pointer> + $<$,$>:-fprofile-arcs + -ftest-coverage> + $<$,$>:-fprofile-instr-generate + -fcoverage-mapping> + $<$:-pg> + $<$,$>:-p> +) + +target_link_libraries( + keys_opts + INTERFACE + $<$,$>:-fprofile-arcs + -ftest-coverage> + $<$,$>:-fprofile-instr-generate + -fcoverage-mapping> + $<$:-pg> + $<$,$>:-p> +) + +if(jemalloc) + if(static) + set(JEMALLOC_USE_STATIC ON CACHE BOOL "" FORCE) + endif() + find_package(jemalloc REQUIRED) + target_compile_definitions(keys_opts INTERFACE PROFILE_JEMALLOC) + target_include_directories( + keys_opts + SYSTEM + INTERFACE ${JEMALLOC_INCLUDE_DIRS} + ) + target_link_libraries(keys_opts INTERFACE ${JEMALLOC_LIBRARIES}) + get_filename_component(JEMALLOC_LIB_PATH ${JEMALLOC_LIBRARIES} DIRECTORY) + ## TODO see if we can use the BUILD_RPATH target property (is it transitive?) + set(CMAKE_BUILD_RPATH ${CMAKE_BUILD_RPATH} ${JEMALLOC_LIB_PATH}) +endif() +if(san) + target_compile_options( + keys_opts + INTERFACE + # sanitizers recommend minimum of -O1 for reasonable performance + $<$:-O1> + ${SAN_FLAG} + -fno-omit-frame-pointer + ) + target_compile_definitions( + keys_opts + INTERFACE + $<$:SANITIZER=ASAN> + $<$:SANITIZER=TSAN> + $<$:SANITIZER=MSAN> + $<$:SANITIZER=UBSAN> + ) + target_link_libraries(keys_opts INTERFACE ${SAN_FLAG} ${SAN_LIB}) +endif() diff --git a/validator-keys-tool/cmake/KeysSanity.cmake b/validator-keys-tool/cmake/KeysSanity.cmake new file mode 100644 index 00000000000..f74144ad8b6 --- /dev/null +++ b/validator-keys-tool/cmake/KeysSanity.cmake @@ -0,0 +1,103 @@ +#[===================================================================[ + convenience variables and sanity checks +#]===================================================================] + +if(NOT ep_procs) + include(ProcessorCount) + ProcessorCount(ep_procs) + if(ep_procs GREATER 1) + # never use more than half of cores for EP builds + math(EXPR ep_procs "${ep_procs} / 2") + message(STATUS "Using ${ep_procs} cores for ExternalProject builds.") + endif() +endif() +get_property(is_multiconfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(is_multiconfig STREQUAL "NOTFOUND") + if( + ${CMAKE_GENERATOR} STREQUAL "Xcode" + OR ${CMAKE_GENERATOR} MATCHES "^Visual Studio" + ) + set(is_multiconfig TRUE) + endif() +endif() + +set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) +if(NOT is_multiconfig) + if(NOT CMAKE_BUILD_TYPE) + message(STATUS "Build type not specified - defaulting to Release") + set(CMAKE_BUILD_TYPE Release CACHE STRING "build type" FORCE) + elseif( + NOT (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL Release) + ) + # for simplicity, these are the only two config types we care about. Limiting + # the build types simplifies dealing with external project builds especially + message( + FATAL_ERROR + " *** Only Debug or Release build types are currently supported ***" + ) + endif() +endif() + +get_directory_property(has_parent PARENT_DIRECTORY) +if(has_parent) + set(is_root_project OFF) +else() + set(is_root_project ON) +endif() + +if("${CMAKE_CXX_COMPILER_ID}" MATCHES ".*Clang") # both Clang and AppleClang + set(is_clang TRUE) + if( + "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0 + ) + message(FATAL_ERROR "This project requires clang 7 or later") + endif() + # TODO min AppleClang version check ? +elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + set(is_gcc TRUE) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0) + message(FATAL_ERROR "This project requires GCC 7 or later") + endif() +endif() +if(CMAKE_GENERATOR STREQUAL "Xcode") + set(is_xcode TRUE) +endif() + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(is_linux TRUE) +else() + set(is_linux FALSE) +endif() + +if("$ENV{CI}" STREQUAL "true" OR "$ENV{CONTINUOUS_INTEGRATION}" STREQUAL "true") + set(is_ci TRUE) +else() + set(is_ci FALSE) +endif() + +# check for in-source build and fail +if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + message( + FATAL_ERROR + "Builds (in-source) are not allowed in " + "${CMAKE_CURRENT_SOURCE_DIR}. Please remove CMakeCache.txt and the CMakeFiles " + "directory from ${CMAKE_CURRENT_SOURCE_DIR} and try building in a separate directory." + ) +endif() + +if(MSVC AND CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") + message(FATAL_ERROR "Visual Studio 32-bit build is not supported.") +endif() + +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message( + FATAL_ERROR + "validator-keys requires a 64 bit target architecture.\n" + "The most likely cause of this warning is trying to build on a 32-bit OS." + ) +endif() + +if(APPLE AND NOT HOMEBREW) + find_program(HOMEBREW brew) +endif() diff --git a/validator-keys-tool/doc/validator-keys-tool-guide.md b/validator-keys-tool/doc/validator-keys-tool-guide.md new file mode 100644 index 00000000000..5ef65da1b1b --- /dev/null +++ b/validator-keys-tool/doc/validator-keys-tool-guide.md @@ -0,0 +1,117 @@ +# Validator Keys Tool Guide + + + +This guide explains how to set up a validator so its public key does not have to +change if the xrpld config and/or server are compromised. + +A validator uses a public/private key pair. The validator is identified by the +public key. The private key should be tightly controlled. It is used to: + +- sign tokens authorizing an xrpld server to run as the validator identified + by this public key. +- sign revocations indicating that the private key has been compromised and + the validator public key should no longer be trusted. + +Each new token invalidates all previous tokens for the validator public key. +The current token needs to be present in the xrpld config file. + +Servers that trust the validator will adapt automatically when the token +changes. + +## Validator Keys + +When first setting up a validator, use the `validator-keys` tool to generate +its key pair: + +``` + $ validator-keys create_keys +``` + +Sample output: + +``` + Validator keys stored in /home/ubuntu/.ripple/validator-keys.json +``` + +Keep the key file in a secure but recoverable location, such as an encrypted +USB flash drive. Do not modify its contents. + +## Validator Token + +After first creating the [validator keys](#validator-keys) or if the previous +token has been compromised, use the `validator-keys` tool to create a new +validator token: + +``` + $ validator-keys create_token +``` + +Sample output: + +``` + Update xrpld.cfg file with these values: + + # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr + + [validator_token] + eyJ2YWxpZGF0aW9uX3NlY3J|dF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT + QzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYT|kYWY2IiwibWFuaWZl + c3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE + hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG + bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2 + hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1 + NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj + VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ== +``` + +For a new validator, add the [validator_token] value to the xrpld config file. +For a pre-existing validator, replace the old [validator_token] value with the +newly generated one. A valid config file may only contain one [validator_token] +value. After the config is updated, restart xrpld. + +There is a hard limit of 4,294,967,293 tokens that can be generated for a given +validator key pair. + +## Key Revocation + +If a validator private key is compromised, the key must be revoked permanently. +To revoke the validator key, use the `validator-keys` tool to generate a +revocation, which indicates to other servers that the key is no longer valid: + +``` + $ validator-keys revoke_keys +``` + +Sample output: + +``` + WARNING: This will revoke your validator keys! + + Update xrpld.cfg file with these values and restart xrpld: + + # validator public key: nHUtNnLVx7odrz5dnfb2xpIgbEeJPbzJWfdicSkGyVw1eE5GpjQr + + [validator_key_revocation] + JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr + jQ2KJSO/2tsjRhjO6q0QQHppslQsKNSXWxjGQNIEa6nPisBOKlDDcJVZAMP4QcIyNCadzgM= +``` + +Add the `[validator_key_revocation]` value to this validator's config and +restart xrpld. Rename the old key file and generate new [validator keys](#validator-keys) and +a corresponding [validator token](#validator-token). + +## Signing + +The `validator-keys` tool can be used to sign arbitrary data with the validator +key. + +``` + $ validator-keys sign "your data to sign" +``` + +Sample output: + +``` + B91B73536235BBA028D344B81DBCBECF19C1E0034AC21FB51C2351A138C9871162F3193D7C41A49FB7AABBC32BC2B116B1D5701807BE462D8800B5AEA4F0550D +``` diff --git a/validator-keys-tool/src/ValidatorKeys.cpp b/validator-keys-tool/src/ValidatorKeys.cpp new file mode 100644 index 00000000000..70e348b157e --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeys.cpp @@ -0,0 +1,276 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace xrpl { + +std::string +ValidatorToken::toString() const +{ + json::Value jv; + jv["validation_secret_key"] = strHex(secretKey); + jv["manifest"] = manifest; + + return xrpl::base64Encode(to_string(jv)); +} + +ValidatorKeys::ValidatorKeys(KeyType const& keyType) + : keyType_(keyType) + , tokenSequence_(0) + , revoked_(false) + , keys_(generateKeyPair(keyType_, randomSeed())) +{ +} + +ValidatorKeys::ValidatorKeys( + KeyType const& keyType, + SecretKey const& secretKey, + std::uint32_t tokenSequence, + bool revoked) + : keyType_(keyType) + , tokenSequence_(tokenSequence) + , revoked_(revoked) + , keys_({derivePublicKey(keyType_, secretKey), secretKey}) +{ +} + +ValidatorKeys +ValidatorKeys::make_ValidatorKeys(boost::filesystem::path const& keyFile) +{ + std::ifstream ifsKeys(keyFile.c_str(), std::ios::in); + + if (!ifsKeys) + throw std::runtime_error("Failed to open key file: " + keyFile.string()); + + json::Reader reader; + json::Value jKeys; + if (!reader.parse(ifsKeys, jKeys)) + { + throw std::runtime_error("Unable to parse json key file: " + keyFile.string()); + } + + static std::array const requiredFields{ + {"key_type", "secret_key", "token_sequence", "revoked"}}; + + for (auto field : requiredFields) + { + if (!jKeys.isMember(field)) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + "' is missing \"" + field + "\" field"); + } + } + + auto const keyType = keyTypeFromString(jKeys["key_type"].asString()); + if (!keyType) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"key_type\" field: " + jKeys["key_type"].toStyledString()); + } + + auto const secret = + parseBase58(TokenType::NodePrivate, jKeys["secret_key"].asString()); + + if (!secret) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"secret_key\" field: " + jKeys["secret_key"].toStyledString()); + } + + std::uint32_t tokenSequence; + try + { + if (!jKeys["token_sequence"].isIntegral()) + throw std::runtime_error(""); + + tokenSequence = jKeys["token_sequence"].asUInt(); + } + catch (std::runtime_error&) + { + throw std::runtime_error( + "Key file '" + keyFile.string() + "' contains invalid \"token_sequence\" field: " + + jKeys["token_sequence"].toStyledString()); + } + + if (!jKeys["revoked"].isBool()) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"revoked\" field: " + jKeys["revoked"].toStyledString()); + + ValidatorKeys vk(*keyType, *secret, tokenSequence, jKeys["revoked"].asBool()); + + if (jKeys.isMember("domain")) + { + if (!jKeys["domain"].isString()) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"domain\" field: " + jKeys["domain"].toStyledString()); + + vk.domain(jKeys["domain"].asString()); + } + + if (jKeys.isMember("manifest")) + { + if (!jKeys["manifest"].isString()) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString()); + + auto ret = strUnHex(jKeys["manifest"].asString()); + + if (!ret || ret->size() == 0) + throw std::runtime_error( + "Key file '" + keyFile.string() + + "' contains invalid \"manifest\" field: " + jKeys["manifest"].toStyledString()); + + vk.manifest_.clear(); + vk.manifest_.reserve(ret->size()); + std::copy(ret->begin(), ret->end(), std::back_inserter(vk.manifest_)); + } + + return vk; +} + +void +ValidatorKeys::writeToFile(boost::filesystem::path const& keyFile) const +{ + using namespace boost::filesystem; + + json::Value jv; + jv["key_type"] = to_string(keyType_); + jv["public_key"] = toBase58(TokenType::NodePublic, keys_.publicKey); + jv["secret_key"] = toBase58(TokenType::NodePrivate, keys_.secretKey); + jv["token_sequence"] = json::UInt(tokenSequence_); + jv["revoked"] = revoked_; + if (!domain_.empty()) + jv["domain"] = domain_; + if (!manifest_.empty()) + jv["manifest"] = strHex(makeSlice(manifest_)); + + if (!keyFile.parent_path().empty()) + { + boost::system::error_code ec; + if (!exists(keyFile.parent_path())) + boost::filesystem::create_directories(keyFile.parent_path(), ec); + + if (ec || !is_directory(keyFile.parent_path())) + throw std::runtime_error("Cannot create directory: " + keyFile.parent_path().string()); + } + + std::ofstream o(keyFile.string(), std::ios_base::trunc); + if (o.fail()) + throw std::runtime_error("Cannot open key file: " + keyFile.string()); + + o << jv.toStyledString(); +} + +boost::optional +ValidatorKeys::createValidatorToken(KeyType const& keyType) +{ + if (revoked() || std::numeric_limits::max() - 1 <= tokenSequence_) + return boost::none; + + ++tokenSequence_; + + auto const tokenSecret = generateSecretKey(keyType, randomSeed()); + auto const tokenPublic = derivePublicKey(keyType, tokenSecret); + + STObject st(sfGeneric); + st[sfSequence] = tokenSequence_; + st[sfPublicKey] = keys_.publicKey; + st[sfSigningPubKey] = tokenPublic; + + if (!domain_.empty()) + st[sfDomain] = makeSlice(domain_); + + xrpl::sign(st, HashPrefix::Manifest, keyType, tokenSecret); + xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature); + + Serializer s; + st.add(s); + + manifest_.clear(); + manifest_.reserve(s.size()); + std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); + + return ValidatorToken{xrpl::base64Encode(manifest_.data(), manifest_.size()), tokenSecret}; +} + +std::string +ValidatorKeys::revoke() +{ + revoked_ = true; + + STObject st(sfGeneric); + st[sfSequence] = std::numeric_limits::max(); + st[sfPublicKey] = keys_.publicKey; + + xrpl::sign(st, HashPrefix::Manifest, keyType_, keys_.secretKey, sfMasterSignature); + + Serializer s; + st.add(s); + + manifest_.clear(); + manifest_.reserve(s.size()); + std::copy(s.begin(), s.end(), std::back_inserter(manifest_)); + + return xrpl::base64Encode(manifest_.data(), manifest_.size()); +} + +std::string +ValidatorKeys::sign(std::string const& data) const +{ + return strHex(xrpl::sign(keys_.publicKey, keys_.secretKey, makeSlice(data))); +} + +void +ValidatorKeys::domain(std::string d) +{ + if (!d.empty()) + { + // A valid domain for a validator must be at least 4 characters + // long, should contain at least one . and should not be longer + // that 128 characters. + if (d.size() < 4 || d.size() > 128) + throw std::runtime_error("The domain must be between 4 and 128 characters long."); + + // This regular expression should do a decent job of weeding out + // obviously wrong domain names but it isn't perfect. It does not + // really support IDNs. If this turns out to be an issue, a more + // thorough regex can be used or this check can just be removed. + static boost::regex const re( + "^" // Beginning of line + "(" // Hostname or domain name + "(?!-)" // - must not begin with '-' + "[a-zA-Z0-9-]{1,63}" // - only alphanumeric and '-' + "(? +#include + +#include + +#include +#include +#include + +namespace boost { +namespace filesystem { +class path; +} +} // namespace boost + +namespace xrpl { + +struct ValidatorToken +{ + std::string const manifest; + SecretKey const secretKey; + + /// Returns base64-encoded JSON object + std::string + toString() const; +}; + +class ValidatorKeys +{ +private: + KeyType keyType_; + + // struct used to contain both public and secret keys + struct Keys + { + PublicKey publicKey; + SecretKey secretKey; + + Keys() = delete; + Keys(std::pair p) : publicKey(p.first), secretKey(p.second) + { + } + }; + + std::vector manifest_; + std::uint32_t tokenSequence_; + bool revoked_; + std::string domain_; + Keys keys_; + +public: + explicit ValidatorKeys(KeyType const& keyType); + + ValidatorKeys( + KeyType const& keyType, + SecretKey const& secretKey, + std::uint32_t sequence, + bool revoked = false); + + /** Returns ValidatorKeys constructed from JSON file + + @param keyFile Path to JSON key file + + @throws std::runtime_error if file content is invalid + */ + static ValidatorKeys + make_ValidatorKeys(boost::filesystem::path const& keyFile); + + ~ValidatorKeys() = default; + ValidatorKeys(ValidatorKeys const&) = default; + ValidatorKeys& + operator=(ValidatorKeys const&) = default; + + inline bool + operator==(ValidatorKeys const& rhs) const + { + // SecretKey::operator== is deleted to discourage non-constant-time + // comparison. The public key is derived deterministically from the + // secret key, so comparing public keys is equivalent here. + return revoked_ == rhs.revoked_ && keyType_ == rhs.keyType_ && + tokenSequence_ == rhs.tokenSequence_ && keys_.publicKey == rhs.keys_.publicKey; + } + + /** Write keys to JSON file + + @param keyFile Path to file to write + + @note Overwrites existing key file + + @throws std::runtime_error if unable to create parent directory + */ + void + writeToFile(boost::filesystem::path const& keyFile) const; + + /** Returns validator token for current sequence + + @param keyType Key type for the token keys + */ + boost::optional + createValidatorToken(KeyType const& keyType = KeyType::Secp256k1); + + /** Revokes validator keys + + @return base64-encoded key revocation + */ + std::string + revoke(); + + /** Signs string with validator key + + @param data String to sign + + @return hex-encoded signature + */ + std::string + sign(std::string const& data) const; + + /** Returns the public key. */ + PublicKey const& + publicKey() const + { + return keys_.publicKey; + } + + /** Returns true if keys are revoked. */ + bool + revoked() const + { + return revoked_; + } + + /** Returns the domain associated with this key, if any */ + std::string + domain() const + { + return domain_; + } + + /** Sets the domain associated with this key */ + void + domain(std::string d); + + /** Returns the last manifest we generated for this domain, if available. */ + std::vector + manifest() const + { + return manifest_; + } + + /** Returns the sequence number of the last manifest generated. */ + std::uint32_t + sequence() const + { + return tokenSequence_; + } +}; + +} // namespace xrpl diff --git a/validator-keys-tool/src/ValidatorKeysTool.cpp b/validator-keys-tool/src/ValidatorKeysTool.cpp new file mode 100644 index 00000000000..65ae9835347 --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeysTool.cpp @@ -0,0 +1,455 @@ +#include + +// cspell: words STRINGIZE + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#ifdef BOOST_MSVC +#include +#endif + +//------------------------------------------------------------------------------ +// The build version number. You must edit this for each release +// and follow the format described at http://semver.org/ +//-------------------------------------------------------------------------- +char const* const versionString = + "0.3.2" + +#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; + reporter r; + bool const anyFailed = r.runEach(globalSuites()); + if (anyFailed) + return EXIT_FAILURE; // LCOV_EXCL_LINE + return EXIT_SUCCESS; +} + +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()); + + ValidatorKeys 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 +createToken(boost::filesystem::path const& keyFile) +{ + using namespace xrpl; + + auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); + + if (keys.revoked()) + throw std::runtime_error("Validator keys have been revoked."); + + auto const token = keys.createValidatorToken(); + + if (!token) + throw std::runtime_error( + "Maximum number of tokens have already been generated.\n" + "Revoke validator keys if previous token has been compromised."); + + // Update key file with new token sequence + keys.writeToFile(keyFile); + + std::cout << "Update xrpld.cfg file with these values and restart xrpld:\n\n"; + std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) + << "\n\n"; + std::cout << "[validator_token]\n"; + + auto const tokenStr = token->toString(); + auto const len = 72; + for (auto i = 0; i < tokenStr.size(); i += len) + std::cout << tokenStr.substr(i, len) << std::endl; + + std::cout << std::endl; +} + +void +createRevocation(boost::filesystem::path const& keyFile) +{ + using namespace xrpl; + + auto keys = ValidatorKeys::make_ValidatorKeys(keyFile); + + if (keys.revoked()) + std::cout << "WARNING: Validator keys have already been revoked!\n\n"; + else + std::cout << "WARNING: This will revoke your validator keys!\n\n"; + + auto const revocation = keys.revoke(); + + // Update key file with new token sequence + keys.writeToFile(keyFile); + + std::cout << "Update xrpld.cfg file with these values and restart xrpld:\n\n"; + std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) + << "\n\n"; + std::cout << "[validator_key_revocation]\n"; + + auto const len = 72; + for (auto i = 0; i < revocation.size(); i += len) + std::cout << revocation.substr(i, len) << std::endl; + + std::cout << std::endl; +} + +void +attestDomain(xrpl::ValidatorKeys 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 = ValidatorKeys::make_ValidatorKeys(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, boost::filesystem::path const& keyFile) +{ + using namespace xrpl; + + auto keys = ValidatorKeys::make_ValidatorKeys(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(); + 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(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"; + std::cout << "# validator public key: " << toBase58(TokenType::NodePublic, keys.publicKey()) + << "\n\n"; + std::cout << "[validator_token]\n"; + + auto const tokenStr = token->toString(); + auto const len = 72; + for (auto i = 0; i < tokenStr.size(); i += len) + std::cout << tokenStr.substr(i, len) << std::endl; + + std::cout << "\n"; +} + +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 = ValidatorKeys::make_ValidatorKeys(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 +generateManifest(std::string const& type, boost::filesystem::path const& keyFile) +{ + using namespace xrpl; + + auto keys = ValidatorKeys::make_ValidatorKeys(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"; +} + +int +runCommand( + std::string const& command, + std::vector const& args, + boost::filesystem::path const& keyFile) +{ + using namespace std; + + static map::size_type> const commandArgs = { + {"create_keys", 0}, + {"create_token", 0}, + {"revoke_keys", 0}, + {"set_domain", 1}, + {"clear_domain", 0}, + {"attest_domain", 0}, + {"show_manifest", 1}, + {"sign", 1}, + }; + + auto const iArgs = commandArgs.find(command); + + if (iArgs == commandArgs.end()) + throw std::runtime_error("Unknown command: " + command); + + if (args.size() != iArgs->second) + throw std::runtime_error("Syntax error: Wrong number of arguments"); + + if (command == "create_keys") + createKeyFile(keyFile); + else if (command == "create_token") + createToken(keyFile); + else if (command == "revoke_keys") + createRevocation(keyFile); + else if (command == "set_domain") + setDomain(args[0], keyFile); + else if (command == "clear_domain") + setDomain("", keyFile); + else if (command == "attest_domain") + attestDomain(keyFile); + else if (command == "sign") + signData(args[0], keyFile); + else if (command == "show_manifest") + generateManifest(args[0], keyFile); + + 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" + " 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"; +} +// 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.")( + "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; + path keyFile = vm.count("keyfile") ? vm["keyfile"].as() : defaultKeyFile; + + return runCommand( + vm["command"].as(), + vm["arguments"].as>(), + keyFile); + } + catch (std::exception const& e) + { + std::cerr << e.what() << "\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; + // LCOV_EXCL_STOP +} diff --git a/validator-keys-tool/src/ValidatorKeysTool.h b/validator-keys-tool/src/ValidatorKeysTool.h new file mode 100644 index 00000000000..2a3a925e0d7 --- /dev/null +++ b/validator-keys-tool/src/ValidatorKeysTool.h @@ -0,0 +1,30 @@ +#include + +#include + +namespace boost { +namespace filesystem { +class path; +} +} // namespace boost + +std::string const& +getVersionString(); + +void +createKeyFile(boost::filesystem::path const& keyFile); + +void +createToken(boost::filesystem::path const& keyFile); + +void +createRevocation(boost::filesystem::path const& keyFile); + +void +signData(std::string const& data, boost::filesystem::path const& keyFile); + +int +runCommand( + std::string const& command, + std::vector const& arg, + boost::filesystem::path const& keyFile); diff --git a/validator-keys-tool/src/test/KeyFileGuard.h b/validator-keys-tool/src/test/KeyFileGuard.h new file mode 100644 index 00000000000..a3ad8ea8922 --- /dev/null +++ b/validator-keys-tool/src/test/KeyFileGuard.h @@ -0,0 +1,58 @@ +#include + +#include + +#include + +namespace xrpl { + +/** + Write a key file dir and remove when done. + */ +class KeyFileGuard +{ +private: + using path = boost::filesystem::path; + path subDir_; + beast::unit_test::Suite& test_; + + auto + rmDir(path const& toRm) + { + if (is_directory(toRm)) + remove_all(toRm); + else + test_.log << "Expected " << toRm.string() << " to be an existing directory." + << std::endl; + }; + +public: + KeyFileGuard(beast::unit_test::Suite& test, std::string const& subDir) + : subDir_(subDir), test_(test) + { + using namespace boost::filesystem; + + if (!exists(subDir_)) + create_directory(subDir_); + else + // Cannot run the test. Someone created a file or directory + // where we want to put our directory + throw std::runtime_error("Cannot create directory: " + subDir_.string()); + } + ~KeyFileGuard() + { + try + { + using namespace boost::filesystem; + + rmDir(subDir_); + } + catch (std::exception& e) + { + // if we throw here, just let it die. + test_.log << "Error in ~KeyFileGuard: " << e.what() << std::endl; + }; + } +}; + +} // namespace xrpl diff --git a/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp b/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp new file mode 100644 index 00000000000..89736d7aae6 --- /dev/null +++ b/validator-keys-tool/src/test/ValidatorKeysTool_test.cpp @@ -0,0 +1,287 @@ +#include + +#include + +#include +#include + +namespace xrpl { + +namespace tests { + +class ValidatorKeysTool_test : public beast::unit_test::Suite +{ +private: + // Allow cout to be redirected. Destructor restores old cout streambuf. + class CoutRedirect + { + public: + CoutRedirect(std::stringstream& sStream) : old_(std::cout.rdbuf(sStream.rdbuf())) + { + } + + ~CoutRedirect() + { + std::cout.rdbuf(old_); + } + + private: + std::streambuf* const old_; + }; + + void + testCreateKeyFile() + { + testcase("Create Key File"); + + std::stringstream coutCapture; + CoutRedirect coutRedirect{coutCapture}; + + using namespace boost::filesystem; + + path const subdir = "test_key_file"; + KeyFileGuard const g(*this, subdir.string()); + path const keyFile = subdir / "validator_keys.json"; + + createKeyFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + std::string const expectedError = + "Refusing to overwrite existing key file: " + keyFile.string(); + std::string error; + try + { + createKeyFile(keyFile); + } + catch (std::exception const& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + } + + void + testCreateToken() + { + testcase("Create Token"); + + std::stringstream coutCapture; + CoutRedirect coutRedirect{coutCapture}; + + using namespace boost::filesystem; + + path const subdir = "test_key_file"; + KeyFileGuard const g(*this, subdir.string()); + path const keyFile = subdir / "validator_keys.json"; + + auto testToken = [this](path const& keyFile, std::string const& expectedError) { + try + { + createToken(keyFile); + BEAST_EXPECT(expectedError.empty()); + } + catch (std::exception const& e) + { + BEAST_EXPECT(e.what() == expectedError); + } + }; + + { + std::string const expectedError = "Failed to open key file: " + keyFile.string(); + testToken(keyFile, expectedError); + } + + createKeyFile(keyFile); + + { + std::string const expectedError = ""; + testToken(keyFile, expectedError); + } + { + auto const keyType = KeyType::Ed25519; + auto const kp = generateKeyPair(keyType, randomSeed()); + + auto keys = + ValidatorKeys(keyType, kp.second, std::numeric_limits::max() - 1); + + keys.writeToFile(keyFile); + std::string const expectedError = + "Maximum number of tokens have already been generated.\n" + "Revoke validator keys if previous token has been compromised."; + testToken(keyFile, expectedError); + } + { + createRevocation(keyFile); + std::string const expectedError = "Validator keys have been revoked."; + testToken(keyFile, expectedError); + } + } + + void + testCreateRevocation() + { + testcase("Create Revocation"); + + std::stringstream coutCapture; + CoutRedirect coutRedirect{coutCapture}; + + using namespace boost::filesystem; + + path const subdir = "test_key_file"; + KeyFileGuard const g(*this, subdir.string()); + path const keyFile = subdir / "validator_keys.json"; + + auto expectedError = "Failed to open key file: " + keyFile.string(); + std::string error; + try + { + createRevocation(keyFile); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + + createKeyFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + createRevocation(keyFile); + createRevocation(keyFile); + } + + void + testSign() + { + testcase("Sign"); + + std::stringstream coutCapture; + CoutRedirect coutRedirect{coutCapture}; + + using namespace boost::filesystem; + + auto testSign = + [this](std::string const& data, path const& keyFile, std::string const& expectedError) { + try + { + signData(data, keyFile); + BEAST_EXPECT(expectedError.empty()); + } + catch (std::exception const& e) + { + BEAST_EXPECT(e.what() == expectedError); + } + }; + + std::string const data = "data to sign"; + + path const subdir = "test_key_file"; + KeyFileGuard const g(*this, subdir.string()); + path const keyFile = subdir / "validator_keys.json"; + + { + std::string const expectedError = "Failed to open key file: " + keyFile.string(); + testSign(data, keyFile, expectedError); + } + + createKeyFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + { + std::string const emptyData = ""; + std::string const expectedError = "Syntax error: Must specify data string to sign"; + testSign(emptyData, keyFile, expectedError); + } + { + std::string const expectedError = ""; + testSign(data, keyFile, expectedError); + } + } + + void + testRunCommand() + { + testcase("Run Command"); + + std::stringstream coutCapture; + CoutRedirect coutRedirect{coutCapture}; + + using namespace boost::filesystem; + + path const subdir = "test_key_file"; + KeyFileGuard g(*this, subdir.string()); + path const keyFile = subdir / "validator_keys.json"; + + auto testCommand = [this]( + std::string const& command, + std::vector const& args, + path const& keyFile, + std::string const& expectedError) { + try + { + runCommand(command, args, keyFile); + BEAST_EXPECT(expectedError.empty()); + } + catch (std::exception const& e) + { + BEAST_EXPECT(e.what() == expectedError); + } + }; + + std::vector const noArgs; + std::vector const oneArg = {"some data"}; + std::vector const twoArgs = {"data", "more data"}; + std::string const noError = ""; + std::string const argError = "Syntax error: Wrong number of arguments"; + { + std::string const command = "unknown"; + std::string const expectedError = "Unknown command: " + command; + testCommand(command, noArgs, keyFile, expectedError); + testCommand(command, oneArg, keyFile, expectedError); + testCommand(command, twoArgs, keyFile, expectedError); + } + { + std::string const command = "create_keys"; + testCommand(command, noArgs, keyFile, noError); + testCommand(command, oneArg, keyFile, argError); + testCommand(command, twoArgs, keyFile, argError); + } + { + std::string const command = "create_token"; + testCommand(command, noArgs, keyFile, noError); + testCommand(command, oneArg, keyFile, argError); + testCommand(command, twoArgs, keyFile, argError); + } + { + std::string const command = "revoke_keys"; + testCommand(command, noArgs, keyFile, noError); + testCommand(command, oneArg, keyFile, argError); + testCommand(command, twoArgs, keyFile, argError); + } + { + std::string const command = "sign"; + testCommand(command, noArgs, keyFile, argError); + testCommand(command, oneArg, keyFile, noError); + testCommand(command, twoArgs, keyFile, argError); + } + } + +public: + void + run() override + { + getVersionString(); + + testCreateKeyFile(); + testCreateToken(); + testCreateRevocation(); + testSign(); + testRunCommand(); + } +}; + +BEAST_DEFINE_TESTSUITE(ValidatorKeysTool, keys, xrpl); + +} // namespace tests + +} // namespace xrpl diff --git a/validator-keys-tool/src/test/ValidatorKeys_test.cpp b/validator-keys-tool/src/test/ValidatorKeys_test.cpp new file mode 100644 index 00000000000..0adecf50197 --- /dev/null +++ b/validator-keys-tool/src/test/ValidatorKeys_test.cpp @@ -0,0 +1,374 @@ +#include + +#include +#include +#include +#include + +#include + +namespace xrpl { + +namespace tests { + +class ValidatorKeys_test : public beast::unit_test::Suite +{ +private: + void + testKeyFile( + boost::filesystem::path const& keyFile, + json::Value const& jv, + std::string const& expectedError) + { + { + std::ofstream o(keyFile.string(), std::ios_base::trunc); + o << jv.toStyledString(); + o.close(); + } + + try + { + ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(expectedError.empty()); + } + catch (std::runtime_error& e) + { + BEAST_EXPECT(e.what() == expectedError); + } + } + + std::array const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}}; + + void + testMakeValidatorKeys() + { + testcase("Make Validator Keys"); + + using namespace boost::filesystem; + + path const subdir = "test_key_file"; + path const keyFile = subdir / "validator_keys.json"; + + for (auto const keyType : keyTypes) + { + ValidatorKeys const keys(keyType); + + KeyFileGuard const g(*this, subdir.string()); + + keys.writeToFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + auto const keys2 = ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == keys2); + } + { + // Require expected fields + KeyFileGuard g(*this, subdir.string()); + + auto expectedError = "Failed to open key file: " + keyFile.string(); + std::string error; + try + { + ValidatorKeys::make_ValidatorKeys(keyFile); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + + expectedError = "Unable to parse json key file: " + keyFile.string(); + + { + std::ofstream o(keyFile.string(), std::ios_base::trunc); + o << "{{}"; + o.close(); + } + + try + { + ValidatorKeys::make_ValidatorKeys(keyFile); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + + json::Value jv; + jv["dummy"] = "field"; + expectedError = "Key file '" + keyFile.string() + "' is missing \"key_type\" field"; + testKeyFile(keyFile, jv, expectedError); + + jv["key_type"] = "dummy keytype"; + expectedError = "Key file '" + keyFile.string() + "' is missing \"secret_key\" field"; + testKeyFile(keyFile, jv, expectedError); + + jv["secret_key"] = "dummy secret"; + expectedError = + "Key file '" + keyFile.string() + "' is missing \"token_sequence\" field"; + testKeyFile(keyFile, jv, expectedError); + + jv["token_sequence"] = "dummy sequence"; + expectedError = "Key file '" + keyFile.string() + "' is missing \"revoked\" field"; + testKeyFile(keyFile, jv, expectedError); + + jv["revoked"] = "dummy revoked"; + expectedError = "Key file '" + keyFile.string() + + "' contains invalid \"key_type\" field: " + jv["key_type"].toStyledString(); + testKeyFile(keyFile, jv, expectedError); + + auto const keyType = KeyType::Ed25519; + jv["key_type"] = to_string(keyType); + expectedError = "Key file '" + keyFile.string() + + "' contains invalid \"secret_key\" field: " + jv["secret_key"].toStyledString(); + testKeyFile(keyFile, jv, expectedError); + + ValidatorKeys const keys(keyType); + { + auto const kp = generateKeyPair(keyType, randomSeed()); + jv["secret_key"] = toBase58(TokenType::NodePrivate, kp.second); + } + expectedError = "Key file '" + keyFile.string() + + "' contains invalid \"token_sequence\" field: " + + jv["token_sequence"].toStyledString(); + testKeyFile(keyFile, jv, expectedError); + + jv["token_sequence"] = -1; + expectedError = "Key file '" + keyFile.string() + + "' contains invalid \"token_sequence\" field: " + + jv["token_sequence"].toStyledString(); + testKeyFile(keyFile, jv, expectedError); + + jv["token_sequence"] = json::UInt(std::numeric_limits::max()); + expectedError = "Key file '" + keyFile.string() + + "' contains invalid \"revoked\" field: " + jv["revoked"].toStyledString(); + testKeyFile(keyFile, jv, expectedError); + + jv["revoked"] = false; + expectedError = ""; + testKeyFile(keyFile, jv, expectedError); + + jv["revoked"] = true; + testKeyFile(keyFile, jv, expectedError); + } + } + + void + testCreateValidatorToken() + { + testcase("Create Validator Token"); + + for (auto const keyType : keyTypes) + { + ValidatorKeys keys(keyType); + std::uint32_t sequence = 0; + + for (auto const tokenKeyType : keyTypes) + { + auto const token = keys.createValidatorToken(tokenKeyType); + + if (!BEAST_EXPECT(token)) + continue; + + auto const tokenPublicKey = derivePublicKey(tokenKeyType, token->secretKey); + + STObject st(sfGeneric); + auto const manifest = xrpl::base64Decode(token->manifest); + SerialIter sit(manifest.data(), manifest.size()); + st.set(sit); + + auto const seq = get(st, sfSequence); + BEAST_EXPECT(seq); + BEAST_EXPECT(*seq == ++sequence); + + auto const tpk = get(st, sfSigningPubKey); + BEAST_EXPECT(tpk); + BEAST_EXPECT(*tpk == tokenPublicKey); + BEAST_EXPECT(verify(st, HashPrefix::Manifest, tokenPublicKey)); + + auto const pk = get(st, sfPublicKey); + BEAST_EXPECT(pk); + BEAST_EXPECT(*pk == keys.publicKey()); + BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature)); + } + } + + auto const keyType = KeyType::Ed25519; + auto const kp = generateKeyPair(keyType, randomSeed()); + + auto keys = + ValidatorKeys(keyType, kp.second, std::numeric_limits::max() - 1); + + BEAST_EXPECT(!keys.createValidatorToken(keyType)); + + keys.revoke(); + BEAST_EXPECT(!keys.createValidatorToken(keyType)); + } + + void + testRevoke() + { + testcase("Revoke"); + + for (auto const keyType : keyTypes) + { + ValidatorKeys keys(keyType); + + auto const revocation = keys.revoke(); + + STObject st(sfGeneric); + auto const manifest = xrpl::base64Decode(revocation); + SerialIter sit(manifest.data(), manifest.size()); + st.set(sit); + + auto const seq = get(st, sfSequence); + BEAST_EXPECT(seq); + BEAST_EXPECT(*seq == std::numeric_limits::max()); + + auto const pk = get(st, sfPublicKey); + BEAST_EXPECT(pk); + BEAST_EXPECT(*pk == keys.publicKey()); + BEAST_EXPECT(verify(st, HashPrefix::Manifest, keys.publicKey(), sfMasterSignature)); + } + } + + void + testSign() + { + testcase("Sign"); + + std::map expected( + {{KeyType::Ed25519, + "2EE541D6825791BF5454C571D2B363EAB3F01C73159B1F" + "237AC6D38663A82B9D5EAD262D5F776B916E68247A1F082090F3BAE7ABC939" + "C8F29B0DC759FD712300"}, + {KeyType::Secp256k1, + "3045022100F142C27BF83D8D4541C7A4E759DE64A672" + "51A388A422DFDA6F4B470A2113ABC4022002DA56695F3A805F62B55E7CC8D5" + "55438D64A229CD0B4BA2AE33402443B20409"}}); + + std::string const data = "data to sign"; + + for (auto const keyType : keyTypes) + { + auto const sk = generateSecretKey(keyType, generateSeed("test")); + ValidatorKeys keys(keyType, sk, 1); + + auto const signature = keys.sign(data); + BEAST_EXPECT(expected[keyType] == signature); + + auto const ret = strUnHex(signature); + BEAST_EXPECT(ret); + BEAST_EXPECT(ret->size()); + BEAST_EXPECT(verify(keys.publicKey(), makeSlice(data), makeSlice(*ret))); + } + } + + void + testWriteToFile() + { + testcase("Write to File"); + + using namespace boost::filesystem; + + auto const keyType = KeyType::Ed25519; + ValidatorKeys keys(keyType); + + { + path const subdir = "test_key_file"; + path const keyFile = subdir / "validator_keys.json"; + KeyFileGuard g(*this, subdir.string()); + + keys.writeToFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + auto fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); + + // Overwrite file with new sequence + keys.createValidatorToken(KeyType::Secp256k1); + keys.writeToFile(keyFile); + + fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); + } + { + // Write to key file in current relative directory + path const keyFile = "test_validator_keys.json"; + if (!exists(keyFile)) + { + keys.writeToFile(keyFile); + remove(keyFile.string()); + } + else + { + // Cannot run the test. Someone created a file + // where we want to put our key file + Throw("Cannot create key file: " + keyFile.string()); + } + } + { + // Create key file directory + path const subdir = "test_key_file"; + path const keyFile = subdir / "directories/to/create/validator_keys.json"; + KeyFileGuard g(*this, subdir.string()); + + keys.writeToFile(keyFile); + BEAST_EXPECT(exists(keyFile)); + + auto const fileKeys = ValidatorKeys::make_ValidatorKeys(keyFile); + BEAST_EXPECT(keys == fileKeys); + } + { + // Fail if file cannot be opened for write + path const subdir = "test_key_file"; + KeyFileGuard g(*this, subdir.string()); + + path const badKeyFile = subdir / "."; + auto expectedError = "Cannot open key file: " + badKeyFile.string(); + std::string error; + try + { + keys.writeToFile(badKeyFile); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + + // Fail if parent directory is existing file + path const keyFile = subdir / "validator_keys.json"; + keys.writeToFile(keyFile); + path const conflictingPath = keyFile / "validators_keys.json"; + expectedError = "Cannot create directory: " + conflictingPath.parent_path().string(); + try + { + keys.writeToFile(conflictingPath); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + BEAST_EXPECT(error == expectedError); + } + } + +public: + void + run() override + { + testMakeValidatorKeys(); + testCreateValidatorToken(); + testRevoke(); + testSign(); + testWriteToFile(); + } +}; + +BEAST_DEFINE_TESTSUITE(ValidatorKeys, keys, xrpl); + +} // namespace tests + +} // namespace xrpl