Skip to content

fix(attest): accept v3 canonical-JSON signature alongside legacy MAC#6426

Open
Scottcjn wants to merge 2 commits into
mainfrom
fix/server-attest-canonical-json-sig
Open

fix(attest): accept v3 canonical-JSON signature alongside legacy MAC#6426
Scottcjn wants to merge 2 commits into
mainfrom
fix/server-attest-canonical-json-sig

Conversation

@Scottcjn
Copy link
Copy Markdown
Owner

Summary

Every signed-flow attestation has been failing with INVALID_SIGNATURE since the rustchain-miner v3 rolled out, because the miner signs canonical JSON of the full attestation payload (GPT-5.4 audit finding #2 — full-payload commitment) but the server's _submit_attestation_impl was still verifying against the older 4-field MAC miner_id|wallet|nonce|commitment. Protocol mismatch.

Live evidence on Node 1 (50.28.86.131) just before this PR:

  • t40-thinkpad-banias (ThinkPad T40, Pentium M Banias 1.5GHz) → rejected
  • modern-sophia-Pow-98... (POWER8 fresh attest) → rejected
  • victus-x86-scott (Victus laptop fresh attest) → rejected

All currently-attesting miners are using the unsigned flow with the server's WARNING [ENROLL/SIG] UNSIGNED enrollment accepted ... upgrade miner to signed flow — confirming the signed flow has zero successful users to break.

Fix

Try the canonical-JSON scheme first (matches v3 miner), fall back to legacy 4-field MAC for backward compat. Same try-then-fallback shape already used in the wallet-transfer signed flow at line 8692.

# Scheme 1: v3 canonical-JSON full-payload
if sig_type in ('ed25519', '', 'canonical_json'):
    payload_for_sig = {k: v for k, v in data.items() if k not in ('signature', 'signature_type')}
    canonical_msg = json.dumps(payload_for_sig, sort_keys=True, separators=(',', ':')).encode('utf-8')
    if verify_rtc_signature(pubkey_hex, canonical_msg, sig_hex):
        verified = True

# Scheme 2: v2 legacy 4-field MAC (kept for backward compat)
if not verified:
    legacy_msg = '{}|{}|{}|{}'.format(miner_id_raw, miner, nonce, commitment).encode('utf-8')
    if verify_rtc_signature(pubkey_hex, legacy_msg, sig_hex):
        verified = True

Verification

Round-trip unit test: miner-side and server-side canonical bytes are byte-identical, signature verifies cleanly.

Miner signed:    b'{"device":{"arch":"modern","cpu":"Intel(R) Pentium(R) M","family":"x86","machine'...
Server verifies: b'{"device":{"arch":"modern","cpu":"Intel(R) Pentium(R) M","family":"x86","machine'...
Byte-identical: True
PASS: round-trip canonical-JSON sign+verify works

py_compile clean. The legacy path is untouched — any hypothetical 4-field-MAC caller still works.

Test plan

  • Round-trip unit test (canonical-JSON sign on miner side, verify on server side)
  • py_compile server file
  • Deploy to Node 1, restart, confirm T40 attestation lands with device_arch=pentium_m_banias (depends on PR feat(rip-200): add Pentium M antiquity tier (Banias/Dothan/Yonah) #6423 also being deployed)
  • Confirm POWER8 (modern-sophia-Pow-98...) starts succeeding
  • Confirm Victus (victus-x86-scott) starts succeeding

Related

🤖 Generated with Claude Code

…cy MAC

The rustchain-miner v3 (`miner_crypto.sign_payload`) signs
`canonical_json(attestation_dict)` over the entire payload BEFORE
adding the `signature` / `signature_type` fields (per GPT-5.4 audit
finding #2 — full-payload commitment defeats partial-field tampering).
But `_submit_attestation_impl` was still verifying against the older
4-field MAC `miner_id|wallet|nonce|commitment`. The two schemes
don't match, so every signed-flow attestation has been failing with
INVALID_SIGNATURE since miner v3 rolled out.

Live evidence on Node 1 (50.28.86.131) just now:
- t40-thinkpad-banias (ThinkPad T40, Pentium M Banias) → rejected
- modern-sophia-Pow-98... (POWER8 fresh attest) → rejected
- victus-x86-scott (Victus laptop fresh attest) → rejected
All other currently-attesting miners use the *unsigned* flow with the
server's WARNING `[ENROLL/SIG] UNSIGNED enrollment accepted ... upgrade
miner to signed flow` — confirming the signed flow has zero successful
users to break.

Fix: try the canonical-JSON scheme first (matches v3 miner), fall back
to legacy 4-field MAC for backward compat. Same try-then-fallback shape
already used in the wallet-transfer signed flow at line 8692:

```python
if verify_rtc_signature(public_key, message, signature):
    pass
elif verify_rtc_signature(public_key, legacy_message, signature):
    ...
```

Round-trip unit test confirms miner-side and server-side canonical bytes
are byte-identical and verify cleanly. The legacy path is untouched — any
hypothetical 4-field-MAC caller still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related size/S PR: 11-50 lines labels May 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

✅ BCOS v2 Scan Results

Metric Value
Trust Score 60/100
Certificate ID BCOS-c6d2b5c0
Tier L1 (met)

BCOS Badge

What does this mean?

The BCOS (Beacon Certified Open Source) engine scans for:

  • SPDX license header compliance
  • Known CVE vulnerabilities (OSV database)
  • Static analysis findings (Semgrep)
  • SBOM completeness
  • Dependency freshness
  • Test infrastructure evidence
  • Review attestation tier

Full report | What is BCOS?


BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

Great work! Thanks for contributing to RustChain! 🦀

The first sigfix in this PR only stripped 'signature' and 'signature_type'
from the canonical-JSON reconstruction, but the miner ALSO adds
'public_key' to the attestation dict AFTER signing (see miner
rustchain_linux_miner.py:516). So my server-side payload still had an
extra field the miner didn't sign over → verification still failed.

Round-trip test now PASSES with the full strip-list of all three fields.

Confirmed on Node 1 by checking the server log:
  [ATTEST/SIG] INVALID SIGNATURE: ... sig_type='ed25519' (tried canonical-JSON + legacy MAC)
The new code WAS running, sig_type WAS 'ed25519', both schemes WERE tried —
the canonical-JSON one just had the wrong byte set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Scottcjn
Copy link
Copy Markdown
Owner Author

Session 2026-05-27 series — this PR is one of five related fixes from a single session-arc that started with bringing a 2003 IBM ThinkPad T40 (Pentium M Banias) online as a RustChain miner. Each PR is independently reviewable + mergeable, but they're all the same root pattern: server validation was written for the v2 fingerprint format; the v3 miner uses different field names + locations:

  • #6423 — feat: Pentium M Banias antiquity tier (1.9x in both ANTIQUITY_MULTIPLIERS and HARDWARE_WEIGHTS)
  • #6426 — fix: accept canonical-JSON v3 signature alongside legacy 4-field MAC
  • #6428 — fix: hw-binding extract_entropy_profile accepts v3 field names
  • #6429 — fix: _has_powerpc_cache_profile accepts arch from simd_identity (POWER8 fix)
  • #6430 — scripts: backport settle_fix_prefer_epoch_enroll.py for production nodes behind main

Production deployment status: all five fixes deployed surgically to Node 1 (50.28.86.131), Node 2 (50.28.86.153), and POWER8 (rustchain-node.service on 100.75.100.89). Nodes 3 (Ryan, offline 6d) + 4 (createkr, no access) pending. T40 earning at 1.9x top share in epoch 176 confirming end-to-end fix. POWER8 attestation now passing after #6429.

Copy link
Copy Markdown
Contributor

@crystal-tensor crystal-tensor left a comment

Choose a reason for hiding this comment

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

✅ Code Review: APPROVED

Summary

Accepts v3 canonical-JSON Ed25519 signature alongside legacy MAC in attestation verification. Tries canonical-JSON first, falls back to legacy 4-field MAC.

Changes Reviewed

  • ✅ Scheme 1 (v3): Strips signature, public_key, signature_type from payload, signs canonical JSON (sorted keys, no whitespace)
  • ✅ Scheme 2 (legacy): miner_id|wallet|nonce|commitment MAC
  • ✅ Tries v3 first, legacy second (correct priority)
  • ✅ Clear error when both fail (includes sig_type for debugging)
  • ✅ Non-breaking: legacy miners still work
  • ✅ Prevents wallet hijack (full payload coverage in v3)

Result: APPROVED


Reviewed by QClaw AI Agent
Bounty claim: 3-25 RTC per CONTRIBUTING.md

Scottcjn added a commit that referenced this pull request May 28, 2026
* feat(windows-miner): add Ed25519 signing (canonical-JSON v3 scheme)

Ports the signing pattern from the Linux dev miner into the canonical
Windows miner. Without this, every Windows attestation goes through
the unsigned-flow path, which is vulnerable to wallet hijack via MITM
(server has no cryptographic binding between wallet field and sender).
With this, Windows miners match Linux miners on the signed-flow security
that PR #6426 enabled server-side.

Three small additions:

1. Top-of-file: try-import miner_crypto with CRYPTO_AVAILABLE flag.
2. RustChainMiner.__init__: generate/load Ed25519 keypair via
   get_or_create_keypair() so identity persists across reinstalls.
3. Right before /attest/submit POST: sign canonical JSON of the
   attestation dict, attach signature + public_key + signature_type
   fields. Server reproduces the same canonical bytes by stripping
   those three fields and verifying with the Ed25519 verify path.

Backward-compatible: if PyNaCl isn't installed or miner_crypto.py is
missing, CRYPTO_AVAILABLE=False and the miner falls back to the
legacy sha512 pseudo-signature path. Server accepts both (PR #6426
canonical-JSON OR legacy 4-field MAC).

Supporting changes:
- requirements-miner.txt: added PyNaCl>=1.5.0
- rustchain_miner_setup.bat: updated MINER_SHA256 + adds miner_crypto.py
  download with a clear "warning: falling back to unsigned mode" notice
  if the file isn't fetched.

This unblocks v3.1.0-miner release with the v3 fingerprint format AND
signed attestation flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(windows-miner): also sign /epoch/enroll with Ed25519

PR #6432 added signing for /attest/submit but the separate /epoch/enroll
endpoint was still going unsigned, producing "[ENROLL/SIG] UNSIGNED
enrollment accepted ... upgrade miner to signed flow" warnings.

This commit signs the enrollment too. Server expects a 3-field MAC
"(miner_pubkey|miner_id|epoch)" verified against the SAME Ed25519 key
used during attestation (server cross-checks via signing_pubkey column).

- enroll() fetches current epoch from /epoch before signing
- Builds the 3-field MAC and signs with miner_crypto.sign_payload
- Attaches signature + public_key to enrollment payload
- Graceful fallback: if PyNaCl missing OR /epoch fetch fails, sends
  payload unsigned (server accepts with warning, no regression)

Updated MINER_SHA256 in rustchain_miner_setup.bat.

Closes the last wallet-hijack surface for v3.1.x Windows miners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scottcjn added a commit that referenced this pull request May 28, 2026
…#6428)

extract_entropy_profile() looked for legacy keys ('L1', 'L2', 'ratio',
'cv' in jitter) but the current v3 fingerprint_checks.py emits 'l1_ns',
'l2_ns', 'drift_ratio', and avg/stdev pairs for jitter (no pre-computed
CV). Result: only 1 of 5 entropy fields was non-zero (just clock_cv,
which happens to use the same key in both formats), every miner running
v3 fingerprint_checks fell below MIN_COMPARABLE_FIELDS=3, and bind_hardware_v2
returned `HARDWARE_BINDING_FAILED:entropy_insufficient` on first attestation.

Server log seen on Node 1:
  [HW_BIND_V2] REJECTED: ralph - entropy_insufficient: {
    'required_nonzero_fields': 3, 'provided_nonzero_fields': 0, ...
  }
  [HW_BIND_V2] REJECTED: t40-thinkpad-banias - entropy_insufficient: {
    'required_nonzero_fields': 3, 'provided_nonzero_fields': 1, ...
  }

Fix: extract_entropy_profile now accepts both naming conventions and derives
jitter CV from the avg/stdev pair when no 'cv' key exists. Legacy format
still works (verified by unit test). Real T40 Pentium M fingerprint now
extracts 5/5 fields cleanly.

Surfaced while validating PRs #6423 (Pentium M Banias tier) and
#6426 (canonical-JSON signature). Together these three fixes restore
end-to-end attestation for v3-miner hosts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

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

Reviewed df70203ca246d85b0376aa94563821b261bba2ec, focusing on the attestation signature verifier.

The code change itself matches the stated v3 miner signing model: reconstructing canonical JSON while stripping signature, signature_type, and public_key reproduces the bytes the miner signs after the second commit. I also verified with a local PyNaCl probe that the reconstructed canonical bytes verify successfully, and that tampering with a signed field changes the bytes and fails verification.

Blocking request: please add committed regression coverage for this verifier before merge. This is a security/protocol boundary, but the PR currently changes only node/rustchain_v2_integrated_v2.2.1_rip200.py and does not add a test proving either:

  • a v3 canonical-JSON attestation succeeds when signature, signature_type, and public_key are appended after signing, and fails if a signed payload field is tampered; or
  • the legacy miner_id|wallet|nonce|commitment fallback still works for backward compatibility.

Validation I ran:

  • python -m py_compile node\rustchain_v2_integrated_v2.2.1_rip200.py -> passed
  • git diff --check origin/main...HEAD -> clean
  • Local PyNaCl canonical reconstruction probe -> canonical_equal True, canonical_verify_ok True, tamper_rejected True
  • python -m pytest node\tests\test_enroll_signature_verification.py tests\test_signed_transfer_replay.py -q -> failed for existing environmental/drift reasons (MISSING_FINGERPRINT in enroll setup and missing httpx for one transfer test), so these do not provide useful coverage for this PR as-is.

I received RTC compensation for this review.

Copy link
Copy Markdown
Contributor

@litaibai2046-debug litaibai2046-debug left a comment

Choose a reason for hiding this comment

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

Reviewed current head df70203ca246d85b0376aa94563821b261bba2ec, focusing on the attestation signature parsing path.

Two specific observations:

  1. The branch predates the current main type guard for attestation signature and public_key. At PR head, sig_hex = (data.get('signature') or '').strip().lower() and pubkey_hex = (data.get('public_key') or '').strip().lower() run directly on raw JSON values. Current main has explicit 400 responses for non-string values before calling .strip(). This PR is currently CONFLICTING, so the rebase should preserve that #5697-style validation; otherwise a signed-attestation fix can accidentally reintroduce the old non-string signature/public_key 500 path.

  2. The new signature_type parsing has the same type-safety shape: sig_type = (data.get('signature_type') or '').strip().lower(). Because this PR adds signature_type as a verifier selector, it should either validate it as a string or coerce only after an isinstance(..., str) check. A payload with signature_type: true should be a clean 400, not an AttributeError inside the security boundary.

Positive note: the second commit correctly strips all three fields added after signing (signature, signature_type, and public_key) before reconstructing canonical JSON. That matches the miner-side signing model and avoids the earlier byte-set mismatch.

Validation performed:

  • fetched PR head and inspected node/rustchain_v2_integrated_v2.2.1_rip200.py at lines around the attestation verifier;
  • compared against current origin/main, which already has the raw signature/public_key type guards;
  • checked PR metadata: GitHub currently reports the PR as CONFLICTING, so a rebase is already required before merge.

I received RTC compensation for this review.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing. Approved.

Scottcjn added a commit that referenced this pull request May 30, 2026
…6523)

The canonical Linux miner at miners/linux/rustchain_linux_miner.py had
no signing infrastructure at all. Every attestation and enrollment went
through the unsigned-flow path, which is vulnerable to wallet hijack via
MITM (server has no cryptographic binding between wallet field and sender).
PR #6432 closed that gap for Windows; this PR brings Linux to parity.

Three additions matching the Windows pattern:

1. Top-of-file: try-import miner_crypto with CRYPTO_AVAILABLE flag.
   Falls back to unsigned (server accepts with WARNING) when PyNaCl OR
   miner_crypto.py is missing — no behavior regression.

2. LocalMiner.__init__: generate/load Ed25519 keypair via
   get_or_create_keypair() so identity persists across reinstalls.

3. In attest(): sign canonical JSON of attestation BEFORE the /attest/submit
   POST. Attaches signature + public_key + signature_type. Server (PR #6426)
   strips those three fields and re-canonicalizes for verification.

4. In enroll(): fetch current epoch from /epoch, then sign the 3-field MAC
   "miner_pubkey|miner_id|epoch" — the server's expected format from
   rustchain_v2_integrated_v2.2.1_rip200.py line ~4155. Server cross-checks
   the pubkey matches its miner_attest_recent.signing_pubkey record.

Plus: miners/linux/miner_crypto.py copied in (same module already shipped
in miners/windows/ via PR #6432). Depends only on PyNaCl + stdlib.

Live-verified on dev miner deployment: T40 (t40-thinkpad-banias), POWER8
(power8-s824-sophia, replaced a literal "0"*128 mock-sig path), Victus
(victus-x86-scott), C4130 (modern-sophia-Pow-9862e3be) all now Ed25519-
signed end-to-end. Server log no longer emits [ENROLL/SIG] UNSIGNED
warnings for any of those wallets.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@aisoh877 aisoh877 left a comment

Choose a reason for hiding this comment

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

Thanks for working on the canonical JSON attestation signature path. I think this needs another pass before merge.

I ran the existing focused attestation signature tests against this branch and against current origin/main with the same Python 3.13 venv/dependencies. origin/main passes, but this branch fails 7 existing cases:

PYTHONPATH=node pytest -q node/tests/test_attest_signature_verification.py node/tests/test_attest_submit_challenge_binding.py
# on origin/main: 15 passed, 3 subtests passed
# on this branch: 7 failed, 8 passed, 3 subtests passed

The failures include the valid legacy signed attestation path and the unsigned backward-compat path returning 422 instead of the expected success/rejection statuses:

  • test_valid_signature_accepted: expected 200, got 422
  • test_missing_signature_allowed: expected 200, got 422
  • tampered wallet/nonce/commitment and invalid signature cases expected 400/503, got 422

This suggests the new canonical/legacy signature flow changes how existing attestation payloads proceed through validation, rather than only adding the canonical JSON acceptance path. The branch also no longer merges cleanly into current origin/main (git merge-tree --write-tree origin/main HEAD reports a conflict in node/rustchain_v2_integrated_v2.2.1_rip200.py). Please rebase and keep the existing legacy/unsigned behavior covered while adding focused tests for the new canonical JSON scheme.

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

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants