fix(attest): require Ed25519 signature on /attest/submit#8052
Conversation
The /attest/submit endpoint only verified Ed25519 signatures when BOTH signature and public_key fields were present. Omitting both skipped the verification block entirely, allowing forged attestations impersonating any wallet without any signature. Added a guard that rejects unsigned attestations unless explicitly enabled via RTC_ALLOW_UNSIGNED_ATTEST env var. Updated the existing test suite to cover both the default (reject) and compat (allow) paths. Closes Scottcjn#8016
|
Welcome to RustChain! Thanks for your first pull request. Before we review, please make sure:
Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150) A maintainer will review your PR soon. Thanks for contributing! |
The attestation signature requirement from Scottcjn#8016 caused 9 failures in test_wallet_review_holds.py because those tests submit unsigned payloads as part of their setup. Adding the compat env var to the shared fixture.
PR Review: #8052 - fix(attest): require Ed25519 signature on /attest/submitSummaryThis PR addresses a critical security vulnerability where the ReviewSecurity Impact: High
Code Quality:
Testing Considerations:
Suggested Follow-up:
RecommendationThis is a critical security fix that should be prioritized for merge. The implementation is sound and maintains backward compatibility through an opt-in environment variable. Approval: ✅ Recommend merge after CI passes Reviewer: FlintLeng |
mumuzhong3
left a comment
There was a problem hiding this comment.
Substantive review on the compat-flag duplication and a couple of follow-ups worth considering before merge:
-
monkeypatch.setenv("RTC_ALLOW_UNSIGNED_ATTEST", "true")is now repeated in 5 separate fixtures (test_attest_init_schema.py,test_attestation_fuzz.py,test_rip201_fleet_bypass.py,test_wallet_review_holds.py, plus the newtest_unsigned_allowed_via_compat_flag). Every existing attestation test had to be touched because unsigned attestations were the implicit baseline. Suggest hoisting this into a single autouse fixture intests/conftest.py(or a node-leveltests/node/conftest.py) so future attestation tests inherit the compat flag and individual files stop carrying the same boilerplate. Also means toggling the default in future is a one-line change. -
Manual env save/restore in
test_missing_signature_rejected—prev = os.environ.pop(...)then laterif prev is not None: os.environ[...] = prev. If the test raises between the pop and the restore (assertion in_submit, an unexpected exception), the env var leaks into other tests in the same process.monkeypatch.setenv/monkeypatch.delenvwould auto-clean on test teardown regardless of outcome. Tiny safety win forpytest-xdistruns. -
Document
RTC_ALLOW_UNSIGNED_ATTESTsomewhere operators will see it. Operators still on the unsigned miner path need to know the env var exists and that it's only meant as a migration bridge. Even a one-liner in.env.exampleor the operator runbook would do — currently nothing in the repo (I greppedRTC_ALLOW) points new operators at it. -
Error shape consistency. The new 400 returns
{ok, error, message, code}which matches whattest_tampered_commitment_rejectedalready returns (code: "INVALID_SIGNATURE"). Good — worth noting in the PR description that this matches existing error-envelope conventions so client error handling doesn't need to branch. -
The
(not sig_hex or not pubkey_hex)check — does it correctly handle the case where one is empty string and the other is non-empty (e.g.,sig_hex="",pubkey_hex="abc...")? The OR short-circuits correctly here, but it's worth a parametrized test for that mixed case (currently onlytest_missing_signature_rejectedcovers both-blank, andtest_tampered_commitment_rejectedcovers both-present).
None of these block merge — fix is sound and the rename of test_missing_signature_allowed → _rejected plus the compat test pair are exactly the right shape. Happy to open a small follow-up PR for items 1 + 2 (conftest hoist + monkeypatch) if useful.
Problem
The
/attest/submitendpoint accepts attestation submissions without Ed25519 signature verification. When bothsignatureandpublic_keyfields are omitted, the entire verification block is skipped — execution falls through to nonce validation and ticket issuance.This means anyone can submit forged attestations impersonating any wallet, since
/attest/challengeissues nonces without authentication either.Reported in #8016.
Fix
Added a guard right after the existing field normalization that rejects unsigned attestations with HTTP 400 unless explicitly allowed:
The
RTC_ALLOW_UNSIGNED_ATTESTenv var provides a backward-compat escape hatch for operators still running older miners that have not been updated to sign attestations.Test changes
test_missing_signature_allowed→ renamed totest_missing_signature_rejected— now asserts HTTP 400 withMISSING_SIGNATUREtest_unsigned_allowed_via_compat_flag— verifies the compat path still works whenRTC_ALLOW_UNSIGNED_ATTEST=truetest_signature_rejected_when_pynacl_missingto reflect the new default behaviorBoth new tests pass individually. The full test file has pre-existing prometheus registry conflicts when run together (unrelated to this change).