Skip to content

fix(claude-code): sign the TRACE record with the persisted key, not a fresh one - #180

Merged
imran-siddique merged 1 commit into
mainfrom
trace-record-stable-key
Sep 12, 2026
Merged

imran-siddique merged 1 commit into
mainfrom
trace-record-stable-key

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

The defect

sign_all signed each TRACE Trust Record with a brand-new key:

trace = sign_record(build_trace(cur), generate_key())   # engine/capture.py

The private half was discarded immediately, so the only copy of the public half was the cnf.jwk inside the record itself. The library refuses that by default:

ValueError: verify_record requires a trusted key. Pass an Ed25519PublicKey or JWK
dict, or set allow_embedded_key=True to (insecurely) trust the key embedded in
record.cnf.jwk.

and with the flag forced on, it warns that this "proves the record is internally consistent, NOT that it came from a trusted issuer."

So the signature on a per-session signed Trust Record attested to nothing about its origin, which is the one property the record exists to carry. Anyone can generate a key, sign a record, and embed the public half.

It also broke identity

Three records captured on one machine in August:

record trace cnf.jwk.x manifest key
2026-08-11 EQRU0ZFB… 92d6daa1…
2026-08-20/pre-detector-fix tjOZOANU… 92d6daa1…
2026-08-20 FJscFYnI… 92d6daa1…

The manifest had one stable identity. The record had a new one every session. An identity that changes per session cannot be pinned out of band, and cannot be registered as a trace-registry producer, because the registry looks the producer key up from producers/ rather than trusting the key a claim names for itself. That is the same principle as the witness key pin.

The fix

The record now uses the keypair the manifest already uses, persisted at ~/.claude/agentrust/signing_key.json, whose public half is published beside every record as verification_key.json. One file verifies both, and its note now says so.

Records emitted before this change cannot be retrofitted; their signing keys no longer exist. Re-run /trace.

Why it survived

The test suite asserted the manifest was externally verifiable with only the published key, and never asked the same of the record. The test asymmetry mirrored the code asymmetry exactly.

Two tests now mirror that assertion: the record verifies under the published key with no allow_embedded_key, refuses a different key, and the manifest and record share one key across two runs. Both fail against the previous behaviour — checked by reverting the one-line change and re-running, not assumed.

61 tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X2GDChXjA7BAdDNzCAmBJv

… fresh one

sign_all called agentrust_trace.generate_key() per record, so the private half
was discarded immediately and the only copy of the public half was the cnf.jwk
inside the record itself. agentrust_trace.verify_record refuses that by
default:

  ValueError: verify_record requires a trusted key. Pass an Ed25519PublicKey
  or JWK dict, or set allow_embedded_key=True to (insecurely) trust the key
  embedded in record.cnf.jwk.

and with the flag on it warns that this "proves the record is internally
consistent, NOT that it came from a trusted issuer". So the signature on a
per-session signed Trust Record attested to nothing about its origin, which is
the property the record exists to carry. Anyone can generate a key, sign a
record and embed the public half.

It also gave the agent a new TRACE identity every session. Three records
captured on this machine in August carry three different cnf.jwk keys
(EQRU0ZFB..., tjOZOANU..., FJscFYnI...) while their manifests all share key
92d6daa1.... An identity that changes per session cannot be pinned out of band
and cannot be registered as a trace-registry producer, because the registry
looks a producer key up from producers/ rather than trusting the key a claim
names for itself.

The record now uses the keypair the manifest already uses, persisted at
~/.claude/agentrust/signing_key.json, whose public half is published beside
every record as verification_key.json. Third parties verify both files with
that one key, and the note in it says so.

Records emitted before this change cannot be retrofitted; their signing keys
are gone.

The test suite asserted the manifest was externally verifiable and never asked
the same of the record, which is why this survived three sessions. Two tests
now mirror that assertion: the record verifies under the published key with no
allow_embedded_key, refuses a different key, and the manifest and record share
one key across two runs. Both fail against the previous behaviour, checked by
reverting. 61 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2GDChXjA7BAdDNzCAmBJv
@imran-siddique
imran-siddique merged commit 647f140 into main Sep 12, 2026
15 checks passed
@imran-siddique
imran-siddique deleted the trace-record-stable-key branch September 12, 2026 18:26
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.

1 participant