Skip to content

security: /epoch/enroll accepts unauthorized enrollments without ownership proof #2116

@createkr

Description

@createkr

/epoch/enroll lacks signature verification / ownership proof

Severity

Medium-High — Unauthorized enrollment enables miner_id hijacking and enrollment race attacks.

Summary

The POST /epoch/enroll endpoint accepts an arbitrary miner_pubkey in the request body without any cryptographic proof that the caller owns or controls that pubkey. Any caller who knows a pubkey with a recent attestation can enroll it — including hijacking the miner_id mapping via INSERT OR REPLACE INTO miner_header_keys.

Affected Code

  • node/rustchain_v2_integrated_v2.2.1_rip200.pyenroll_epoch() handler (line ~3027)
  • rustchain-miner/src/miner.rsenroll() method (line ~270)

Exploit Scenario

Attack 1: Miner ID Hijacking

  1. Victim completes attestation: POST /attest/submit → server records miner_attest_recent with miner = RTC_VICTIM
  2. Attacker calls POST /epoch/enroll with:
    {
      "miner_pubkey": "RTC_VICTIM",
      "miner_id": "attacker_controlled_id",
      "device": {"family": "x86_64", "arch": "default"}
    }
  3. Server executes INSERT OR REPLACE INTO miner_header_keys (miner_id, pubkey_hex) VALUES ('attacker_controlled_id', 'RTC_VICTIM')
  4. Victim's block submissions using their original miner_id may fail or be attributed to the attacker's mapping.

Attack 2: Enrollment Race (First-Come-First-Served)

  1. Victim attests but hasn't enrolled yet.
  2. Attacker enrolls victim's pubkey with a low-weight device (e.g., default x86 at 1.0x instead of victim's actual PowerPC G4 at 2.5x).
  3. INSERT OR IGNORE INTO epoch_enroll means the victim's legitimate high-weight enrollment is silently ignored.
  4. Victim earns at 1.0x weight instead of 2.5x for the entire epoch.

Note: Attack 2 is partially mitigated by the existing INSERT OR IGNORE fix, but the race window remains if the attacker enrolls first.

Root Cause

The enrollment endpoint verifies that the miner_pubkey has a recent attestation (via check_enrollment_requirements), but does not verify that the caller is the same entity that performed the attestation. There is no signature or ownership proof on the enrollment request.

Contrast with Prior Fixes

  • /attest/submit signature verification (prior fix, test_attest_signature_verification.py): Verifies Ed25519 signatures on attestation reports. This proves the attester controls the signing keypair. Already fixed.
  • INSERT OR IGNORE on epoch_enroll (prior fix, test_attestation_overwrite_reward_loss.py): Prevents weight downgrade attacks from repeated enrollment calls. Already fixed.
  • This finding: The enrollment endpoint itself has no signature verification. An attacker doesn't need to replay or modify an existing enrollment — they can submit a fresh enrollment for any pubkey with a recent attestation. Not previously addressed.

Fix

Require Ed25519 signatures on enrollment requests, verified against the signing pubkey stored during the miner's most recent attestation:

  1. Server-side: Store the Ed25519 public_key from attestation in miner_attest_recent.signing_pubkey. On enrollment, verify the signature against this stored key.
  2. Client-side (Rust miner): Generate the Ed25519 keypair once at startup and reuse it for both attestation and enrollment signatures.
  3. Backward compatibility: Unsigned enrollment requests are still accepted (warn-only) to allow legacy miners to continue working while operators upgrade.

Reproduction

# Without the fix, any caller can enroll any pubkey with a recent attestation:
curl -X POST http://localhost:8099/epoch/enroll \
  -H "Content-Type: application/json" \
  -d '{"miner_pubkey": "RTC_VICTIM", "miner_id": "attacker_id", "device": {"family": "x86_64", "arch": "default"}}'
# Returns: {"ok": true, "epoch": N, "weight": 1.0, "miner_pk": "RTC_VICTIM", "miner_id": "attacker_id"}

References

  • Prior fix: /attest/submit signature verification — tests/test_attest_signature_verification.py
  • Prior fix: INSERT OR IGNORE on epoch_enrolltests/test_attestation_overwrite_reward_loss.py
  • New tests: tests/test_enroll_signature_verification.py

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions