Skip to content

feat: Add validator-keys to the tree with external signing and validator-list signing - #8223

Open
dangell7 wants to merge 13 commits into
developfrom
dangell7/validator-keys
Open

dangell7 wants to merge 13 commits into
developfrom
dangell7/validator-keys

Conversation

@dangell7

@dangell7 dangell7 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Moves validator-keys from ripple/validator-keys-tool into the tree as src/tools/validator-keys. Same -Dvalidator_keys=ON option, same binary, same packages.

Also in this PR:

  • External signing from Add support for external signing ripple/validator-keys-tool#53 by @ximinez: create_external, start_token, finish_token, start_revoke_keys, finish_revoke_keys, sign_hex. The master key can stay in a hardware signer; the tool prints the bytes to sign and takes the signature back. start_token --signing-key and a two-signature finish_token cover a signing key that is external as well.
  • Validator list signing: sign_list, start_sign_list, finish_sign_list, verify_list. A publisher's signing key is a validator token from create_token --token-key-type ed25519, the input is the unsigned list JSON that ValidatorList already reads, and verify_list makes the checks a server makes before trusting a list.
  • create_token --out, and Rename rippled.cfg to xrpld.cfg ripple/validator-keys-tool#60.

ripple/validator-keys-tool#61 asked where the tool should live; a separate target in this repository was the option nobody objected to. Same shape as #7555 without the Conan test package change.

Context of Change

#7905 put validator-keys in the packages but left its source in another repository, pinned by commit. The XRPLF publishes its list with a separate signer that reimplements manifest and list signing in three languages, all copying what already exists here: Manifest.h and ValidatorList for verification, the helpers in Manifest_test.cpp and ValidatorList_test.cpp for signing. With the signing half in the tool that ships with xrpld there is one implementation to review, and the publisher tooling stops handling keys.

Manifest and revocation signing moves into libxrpl as makeManifest, makeRevocation and the unsigned-fields functions in Manifest.h, next to deserializeManifest. The tool uses them, and the copies in Manifest_test.cpp, ValidatorList_test.cpp and TrustedPublisherServer.h are deleted.

The tool's ValidatorKeys class is renamed SigningKeys because src/xrpld/app/misc/ValidatorKeys.h already declares one. Tests are gtest under src/tests/tools/validator-keys, built as validator_keys_tests; CI runs that where it ran validator-keys --unittest. The tool links xrpl.libxrpl only.

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Manifest.h gains signing functions; nothing existing changes. No RPC or peer protocol change.

Test Plan

validator_keys_tests: 27 cases, 0 failures. xrpld --unittest=xrpl.app.Manifest,xrpl.app.ValidatorList,xrpl.app.ValidatorSite,xrpl.app.ValidatorKeys: 8943 tests, 0 failures. A list signed by the tool was served to a standalone xrpld through [validator_list_sites] and the validators RPC reported the publisher available with both validators.

Future Tasks

Archive ripple/validator-keys-tool with a pointer here.

@dangell7
dangell7 requested a review from a team as a code owner September 14, 2026 02:33
@dangell7
dangell7 requested review from bthomee, mathbunnyru and ximinez and removed request for a team September 14, 2026 02:33
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.87500% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/libxrpl/server/Manifest.cpp 96.9% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File permission race condition: secrets written before chmod. See inline.

Comment thread src/tools/validator-keys/ValidatorKeysTool.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved correctness, security, and failure-handling findings remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Moves validator-keys into the repository, adding external signing and validator-list signing/verification.

Changes:

  • Adds in-tree key, token, manifest, revocation, and signing implementations.
  • Adds validator-list signing, verification, and version 2 support.
  • Updates tests, documentation, CMake, packaging, and CI.
File summaries
File Reviewed change
src/tools/validator-keys/ValidatorKeysTool.h Tool APIs and command options
src/tools/validator-keys/ValidatorKeysTool.cpp CLI commands and workflows
src/tools/validator-keys/test/ValidatorKeysTool_test.cpp Command-layer tests
src/tools/validator-keys/test/SigningKeys_test.cpp Signing and key-management tests
src/tools/validator-keys/test/ListSigning_test.cpp Validator-list signing and verification tests
src/tools/validator-keys/test/KeyFileGuard.h Key-file test helper
src/tools/validator-keys/SigningKeys.h Signing key interface
src/tools/validator-keys/SigningKeys.cpp Key, token, and manifest implementation
src/tools/validator-keys/README.md Tool build documentation
src/tools/validator-keys/ListSigning.h Validator-list APIs
src/tools/validator-keys/ListSigning.cpp List parsing, signing, and verification
src/tools/validator-keys/LICENSE Bundled licensing notice
src/tools/validator-keys/doc/validator-keys-tool-guide.md Operator guide
src/tools/validator-keys/CMakeLists.txt Tool target definition
package/README.md Packaging documentation
package/build_pkg.py Package validation updates
cmake/XrplValidatorKeys.cmake In-tree build and installation
.github/workflows/reusable-clang-tidy.yml Clang-tidy configuration
.github/scripts/rename/cmake.sh Removes obsolete repository renaming
.cspell.config.yaml Dictionary additions
.clang-tidy Includes tool headers in analysis
Review details

Suppressed comments (8)

src/tools/validator-keys/SigningKeys.cpp:494

  • This comment says "that 128 characters"; the intended comparison is "than 128 characters".
        // that 128 characters.

src/tools/validator-keys/SigningKeys.cpp:239

  • The key file is overwritten in place with trunc. A crash or write error after truncation can leave the only copy of the master key, manifest, and pending signing state partially written or empty; this path is exercised on every token operation. Write a fully validated temporary file in the same directory and atomically replace the key file (while preserving owner-only permissions).
    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();

src/tools/validator-keys/SigningKeys.cpp:123

  • isIntegral() includes Boolean, and asUInt() converts false/true to 0/1. A malformed key file with "token_sequence": false is therefore accepted and can generate a manifest from a fabricated sequence instead of being rejected as invalid key-file data; require signed/unsigned integer types explicitly.
        if (!jKeys["token_sequence"].isIntegral())
            throw std::runtime_error("");

        tokenSequence = jKeys["token_sequence"].asUInt();

src/tools/validator-keys/ValidatorKeysTool.cpp:284

  • The one-signature finish_token path has the same failure mode: it commits the pending token and clears its pending secret before emitBlock has successfully produced the token. A failed/open or silently short output leaves no way to reconstruct the validation secret for the manifest now stored in the key file; make output and key-file state commit failure-safe together.
    keys.writeToFile(options.keyFile);

src/tools/validator-keys/ValidatorKeysTool.cpp:159

  • After opening the signed-list output, this writes the JSON but never checks the stream or close result, then reports success. A disk-full or other write error can therefore leave a truncated vl.json while the command exits successfully; check the write/close status before printing the success message.
    o << jv.toStyledString();
    std::cout << "Written to " << outFile->string() << "\n";

src/tools/validator-keys/ValidatorKeysTool.cpp:142

  • The token writer also ignores failures from operator<< and close(), so a disk-full or short write can leave a truncated secret-bearing token file while the key-file update has already succeeded. Check the stream before changing permissions or reporting success.
    o << block.str();
    o.close();

src/tools/validator-keys/doc/validator-keys-tool-guide.md:72

  • The implementation allows the final non-revocation sequence (UINT32_MAX - 1): starting at sequence 0, the guard at SigningKeys.cpp:307 still permits the increment from UINT32_MAX - 2 to that value, while only UINT32_MAX is reserved for revocation. Therefore the documented limit is one token too low.
There is a hard limit of 4,294,967,293 tokens that can be generated for a given
validator key pair.

src/tools/validator-keys/doc/validator-keys-tool-guide.md:94

  • The documented revocation value also contains a literal |, which is not valid base64. Copying this example produces an invalid [validator_key_revocation]; remove the bar from the sample.
  JP////9xIe0hvssbqmgzFH4/NDp1z|3ShkmCtFXuC5A0IUocppHopnASQN2MuMD1Puoyjvnr
  • Files reviewed: 21/21 changed files
  • Comments generated: 13
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/tools/validator-keys/ListSigning.cpp Outdated
Comment thread src/tools/validator-keys/ListSigning.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated
Comment thread src/tools/validator-keys/ValidatorKeysTool.cpp Outdated
Comment thread src/tools/validator-keys/ValidatorKeysTool.cpp Outdated
Comment thread src/tools/validator-keys/ListSigning.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated
Comment thread package/README.md
Comment thread src/tools/validator-keys/ValidatorKeysTool.cpp Outdated
Comment thread src/tools/validator-keys/doc/validator-keys-tool-guide.md Outdated

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slice of the diff covers two new gtest suites (ListSigning.cpp, SigningKeys.cpp) exercising the new validator-keys tool's list-signing and key-management logic, plus the CMakeLists.txt wiring the xrpl.validator-keys static library and the validator-keys executable. The production implementation files (SigningKeys.h/.cpp, ListSigning.h/.cpp, Commands.cpp) were not present with actual content in this diff slice (Commands.cpp diff was empty), so the real logic under test could not be reviewed here. The test code itself is thorough (covers file I/O, permission checks, external signing, revocation, list append/rotation) and I did not find concrete correctness or security bugs in the added lines. The CMakeLists.txt additions look standard and consistent with other tool targets in the repo.

Comment thread src/tools/validator-keys/Commands.cpp Outdated
Comment thread src/tools/validator-keys/Commands.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated
Comment thread src/tools/validator-keys/Commands.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File permissions not validated on secrets—see inline.

Comment thread src/tools/validator-keys/Commands.cpp Outdated

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff adds only test infrastructure (Fixtures.h) and a comprehensive gtest suite (ListSigning.cpp) for the new validator-keys tool's list-signing functionality; no production code is included in this diff to review. The test code is well-structured (RAII TempDir, clear helper functions, thorough coverage of error paths for canonical JSON, unsigned-list parsing, versioned list signing/verification, and key-rotation append scenarios) and I found no correctness bugs, resource leaks, or test-pollution issues in it. Per review guidance, test-only style/naming/organization concerns are not flagged.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent filename in message — see inline.

Comment thread src/tools/validator-keys/Commands.cpp Outdated
Comment thread src/tools/validator-keys/SigningKeys.cpp Outdated
@xrplf-bot
xrplf-bot requested a lite review from Copilot September 14, 2026 18:57

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff excerpt covers the new gtest suite for SigningKeys (src/tests/tools/validator-keys/SigningKeys.cpp) and the CMake wiring for the new validator-keys tool target. The test coverage is thorough (key-file round-trips, error paths for malformed key files, external master/signing key flows, token exhaustion/revocation, and manifest signature checks), and the CMakeLists.txt correctly builds the static xrpl.validator-keys library, links it into the validator-keys executable, and installs the binary. I did not find correctness, security, or resource-leak issues in the added lines. The actual implementation files (SigningKeys.h/.cpp, Commands.h/.cpp, ListSigning.h/.cpp, Main.cpp) referenced by these tests are not included in this diff excerpt, so the underlying signing/token/manifest logic itself could not be reviewed here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical output-data-loss risk and additional correctness issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (12)

src/tests/tools/validator-keys/CMakeLists.txt:24

  • This suite is configured while cmake/XrplValidatorKeys.cmake is processed before the top-level include(CTest), so gtest_discover_tests() runs before CTest is enabled and can leave the suite absent from ctest (the direct CI executable invocation does not catch that). Enable CTest before adding this subdirectory in the top-level configuration, rather than relying only on the binary being built.
gtest_discover_tests(validator_keys_tests)

src/tools/validator-keys/Commands.cpp:185

  • base64Decode returns partial data for invalid input, so a valid public-key base64 value followed by trailing junk is silently accepted here and the junk is ignored. This makes create_external accept a key different from the supplied argument; require a base64 round-trip before checking publicKeyType, as decodeSignature does above.
    if (auto const bytes = base64Decode(data); publicKeyType(makeSlice(bytes)))
        return PublicKey(makeSlice(bytes));

src/tools/validator-keys/Commands.cpp:331

  • createToken advances the sequence and generates the secret before this state is persisted, but the key file is committed before output.block actually writes the token. If the already-open output fails on write/close (for example, disk-full), the destructor removes the incomplete output while the key file has advanced, so the generated secret is lost and the next token skips a sequence. Stage the token/output and commit the key-file state only after the output succeeds, or otherwise preserve a retryable pending token.
    keys.writeToFile(ctx.options.keyFile);

src/tools/validator-keys/Commands.cpp:355

  • The same commit ordering loses an externally signed token if output.block fails after this write: finishToken has already cleared the pending state and advanced the sequence, while the manifest/secret is only emitted afterward. A failed write therefore leaves no recoverable token but consumes the sequence. Commit the key-file transition only after the output is successfully written, or retain the finished token as retryable state.
    keys.writeToFile(ctx.options.keyFile);

src/tools/validator-keys/Commands.cpp:416

  • Changing a domain for an external-master key creates and advances a token state here before output.block emits the manifest/token instructions. A write/close failure after the key-file update leaves the new manifest unavailable while the key file has already changed, so the external signing workflow cannot recover the intended token cleanly. Defer the key-file commit until output succeeds or retain the generated manifest as pending state.
    keys.writeToFile(ctx.options.keyFile);

src/tools/validator-keys/ListSigning.cpp:325

  • When appending a v2 list whose top-level manifest differs, this branch always re-signs with the supplied manifest, even if its sequence is older or equal to the existing one. A server that has already cached the existing newer manifest treats the supplied manifest as stale and verifies with the cached signing key, so the generated list is rejected. Compare the incoming and existing/per-entry manifest sequences and reject stale or same-sequence rotations.
        if (existing[jss::manifest].asString() != manifestBase64)
        {
            if (!resign)
            {

src/tools/validator-keys/ListSigning.cpp:323

  • If the appended document has the same manifest, blobs_v2 is copied without validating its entries; the validation below only runs when the manifest changes. Thus a malformed existing v2 file (for example, an entry without a signature) is accepted and the output remains unusable by ValidatorSite::parseBlobs. Validate the existing entries before copying them in either branch.
        jv[jss::blobs_v2] = existing[jss::blobs_v2];
        if (existing[jss::manifest].asString() != manifestBase64)
        {

src/tools/validator-keys/ListSigning.cpp:150

  • Duplicate detection scans the entire accumulated vector for every validator, making parseUnsignedList O(n²) in the list size. The parser accepts documents up to 4 MiB, so a large or untrusted list can make verify_list spend disproportionate time comparing keys; track seen keys in a std::set/hash set while building the vector instead.
        if (std::ranges::find(list.validators, *key) != list.validators.end())

src/tools/validator-keys/ListSigning.cpp:123

  • ValidatorList::verify accepts an empty validators array and updatePublisherList deliberately handles it by removing the publisher's previous entries (it only logs a warning at src/xrpld/app/misc/detail/ValidatorList.cpp:985-988). Requiring a non-empty array here makes sign_list unable to produce a valid empty list for clearing a publisher's roster, and makes verify_list reject such server-accepted lists; allow an empty array while still validating each entry.
    if (!jv.isMember(jss::validators) || !jv[jss::validators].isArray() ||
        jv[jss::validators].size() == 0)
        throw std::runtime_error("\"validators\" must be a non-empty array");

src/tools/validator-keys/Main.cpp:43

  • The help text makes the encoding argument look optional ([hex|base64]), but the command table requires exactly one argument and cmdShowManifest indexes args[0]. validator-keys show_manifest therefore fails despite the advertised syntax; either implement a default encoding or show the argument as required.
                 "     show_manifest [hex|base64]    Displays the last generated "
                 "manifest\n"

src/tools/validator-keys/SigningKeys.cpp:431

  • startPending signs a manifest built with the current domain_, but finishPending rebuilds it from the mutable domain_ at finish time. For an external key, set_domain/clear_domain can change the persisted domain between start_token and finish_token; the returned bytes were signed without that change, so finishing with valid signatures fails (and startToken no longer fixes the manifest contents as documented). Persist the domain snapshot in Pending or retain/reuse the exact serialized partial manifest.
    STObject st = partialManifest(tokenSequence_ + 1, pending.signingKey);

src/tools/validator-keys/doc/validator-keys-tool-guide.md:208

  • This documents the version-2 append limit as four blobs, but the implementation and server accept five (kMaxBlobs/kMaxSupportedBlobs are 5). Operators following this guide may unnecessarily reject a valid fifth blob or misunderstand the replacement window; update the guide to say five.
`blobs_v2` instead, and `--append <existing.json>` adds it to a version 2
document that already holds up to four blobs, so a list can be published
alongside the one it will replace.
  • Files reviewed: 28/28 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/tools/validator-keys/Commands.cpp Outdated

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This portion of the diff refactors TrustedPublisherServer.h to reuse the shared makeManifest() helper from xrpl/server/Manifest.h instead of a local, duplicated implementation, and adds the new gtest suite for the validator-keys tool (CMakeLists.txt, Commands.cpp, Fixtures.h). The refactor looks correct and consistent with the change's stated goal of de-duplicating manifest/list signing logic. I didn't find any clear correctness, security, or resource-leak issues in the code shown; the new test files are well-scoped test helpers and cases, and per review guidance I'm not flagging test-only style/naming/assertion-pattern nitpicks. Without visibility into the corresponding production sources (src/tools/validator-keys/Commands.h, SigningKeys.h, ListSigning.h), I can't independently verify some of the exact error-string/sequence-number expectations asserted in the tests, so I'm not flagging those as bugs.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symlink TOCTOU vulnerability — see inline.

throw std::runtime_error("Refusing to write through a symlink: " + path.string());
}

stream_.open(temp_, std::ios_base::trunc);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symlink TOCTOU between is_symlink check and stream_.open. Use O_NOFOLLOW in atomic creation:

Suggested change
stream_.open(temp_, std::ios_base::trunc);
int fd = ::open(temp_.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
if (fd < 0)
throw std::runtime_error("Cannot safely create file");
stream_.rdbuf()->open(fd, std::ios::out);

throw std::runtime_error("Refusing to write through a symlink: " + path.string());
}

stream_.open(temp_, std::ios_base::trunc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: LOW

TOCTOU race between the is_symlink() check (line 20) and stream_.open(). An attacker with write access to the parent directory can replace the path with a symlink between the check and the open, causing secret key material (master keys, token secrets) to be written to an attacker-controlled location. ofstream::open follows symlinks; use open() with O_CREAT|O_EXCL|O_NOFOLLOW for atomic creation.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Replace the is_symlink() check + stream_.open() pattern with a POSIX open() call using O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW (and optionally O_EXCL if the file should not already exist). This atomically refuses to open a symlink, eliminating the TOCTOU race window.

Concretely:

  1. In OwnerOnlyFile.h, add #include <fcntl.h> and #include <unistd.h> (or use <cstdio> with platform guards for Windows).
  2. In the constructor, replace the symlink check loop and stream_.open(temp_, ...) with:
    • int fd = ::open(temp_.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0600);
    • If fd < 0 and errno == ELOOP, throw the symlink error.
    • If fd < 0 otherwise, throw the cannot-write error.
    • Use a platform-specific mechanism to attach fd to stream_ (e.g., GCC's __gnu_cxx::stdio_filebuf, or fdopen() + a custom std::streambuf, or switch stream_ from std::ofstream to a POSIX file-descriptor–based wrapper).
  3. Keep the symlink check for target_ (which is only used later in commit() via fs::rename), or apply a similar O_NOFOLLOW strategy if target_ is also opened elsewhere.
  4. On Windows, O_NOFOLLOW is not available; guard with #ifdef _WIN32 or use CreateFile with appropriate flags.

The fs::permissions() call at line 29 can also be replaced since open() with mode 0600 sets owner-only permissions atomically at creation time.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical and moderate review findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (9)

Previously missed (2) — in code that hasn't changed since the last review.

src/tools/validator-keys/Commands.cpp:171

  • base64Decode stops at the first invalid character, so a value such as the canonical base64 public key followed by ! decodes to the valid 33-byte key and is accepted here even though the supplied encoding is malformed. Require a canonical round trip (base64Encode(bytes) == data) as decodeSignature does before constructing the key.
    src/tools/validator-keys/ListSigning.cpp:322
  • If the existing manifest is unchanged, blobs_v2 is copied without validating any entry; the shape checks below only run when the manifest changes. A malformed append (for example an entry without a string signature) can therefore produce a signed-list document that verify_list and xrpld reject. Validate every existing blob entry before copying it.

src/tools/validator-keys/Commands.cpp:318

  • The key file is committed before the output file is written and renamed. Output only pre-opens <out>.tmp; a later failure such as --out naming an existing directory makes commit() fail after this line, so create_token loses the generated validation secret (the same ordering is used by token/domain commands). Stage both updates or otherwise make output failure recoverable before consuming the key state.
    keys.writeToFile(ctx.options.keyFile);
    output.block("validator_token", nodePublic(keys), tokenToBase64(token));

src/tools/validator-keys/ListSigning.cpp:322

  • When the supplied manifest differs from the existing v2 document, this path re-signs all existing blobs with the supplied key without checking its sequence. Passing an older token therefore downgrades the top-level manifest; a server that has already cached the existing newer manifest will treat the new one as stale and reject blobs signed by the older key. Compare manifest sequences and reject a downgrade before appending.
        if (existing[jss::manifest].asString() != manifestBase64)

src/tools/validator-keys/ListSigning.cpp:359

  • Appending this blob does not validate its sequence against the existing v2 blobs. --append can therefore emit a document with a lower or duplicate sequence; ValidatorList::applyList treats that blob as stale/same and ignores it, while verifyList can report the document as valid. Compare the parsed new sequence with the existing blobs and reject non-increasing values.
    json::Value entry(json::ValueType::Object);
    entry[jss::blob] = blob;
    entry[jss::signature] = signatureHex;
    jv[jss::blobs_v2].append(entry);

src/tools/validator-keys/OwnerOnlyFile.cpp:16

  • The deterministic temporary name can be an input file that Output did not reject: for example, --out foo --keyfile foo.tmp opens foo.tmp with truncation before the key is loaded, destroying the key file. Use a non-colliding temporary name or reject inputs equivalent to target + ".tmp" before opening it.
    : target_(std::move(target)), temp_(target_.string() + ".tmp"), what_(std::move(what))

src/tools/validator-keys/SigningKeys.cpp:213

  • A stored regular manifest is only checked for signatures and master/revocation state; its sequence is not compared with tokenSequence_. If a key file is migrated or corrupted with manifest.sequence > token_sequence, the next create_token signs token_sequence + 1, producing a stale manifest that xrpld will ignore. Reject regular stored manifests whose sequence exceeds the persisted token sequence (while allowing a higher token sequence for migration).
    src/tools/validator-keys/SigningKeys.cpp:393
  • startToken signs bytes containing the current domain_, but Pending does not retain that domain; finishPending reconstructs the manifest with whatever domain is set later. Running set_domain/domain() between start_token and finish_token therefore makes the previously returned signature fail. Snapshot the domain in the pending state or reject domain changes while a token is pending.
    src/tools/validator-keys/SigningKeys.cpp:468
  • revoke() reports that an external key file cannot be used to "sign tokens", even though this command is creating a key revocation. This is misleading for the documented external-signing flow; use the existing generic signing error (or a revocation-specific message) here.
  • Files reviewed: 35/35 changed files
  • Comments generated: 2
  • Review effort level: Lite


namespace xrpl {

class STObject;
throw std::runtime_error("Refusing to write through a symlink: " + path.string());
}

stream_.open(temp_, std::ios_base::trunc);

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOCTOU race in file permissions - see inline.

throw std::runtime_error("Refusing to write through a symlink: " + path.string());
}

stream_.open(temp_, std::ios_base::trunc);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOCTOU: File permissions set after creation; use umask(0177) or open(O_CREAT|O_EXCL, 0600):

Suggested change
stream_.open(temp_, std::ios_base::trunc);
auto const old_umask = umask(0177);
stream_.open(temp_, std::ios_base::trunc);
umask(old_umask);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants