fix(claude-code): sign the TRACE record with the persisted key, not a fresh one - #180
Merged
Merged
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
sign_allsigned each TRACE Trust Record with a brand-new key:The private half was discarded immediately, so the only copy of the public half was the
cnf.jwkinside the record itself. The library refuses that by default: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:
cnf.jwk.xEQRU0ZFB…92d6daa1…tjOZOANU…92d6daa1…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-registryproducer, because the registry looks the producer key up fromproducers/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 asverification_key.json. One file verifies both, and itsnotenow 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