fix(schema): refuse private key material in cnf.jwk - #296
Merged
Conversation
GHSA-vc4p-h84j-7qxj. RFC 8747 defines cnf as a confirmation key: the public half, carried so a verifier can bind the record to the key that signed it. models.TrustRecord already refuses d, p, q, dp, dq, qi and k, with the comment "cnf.jwk is a public proof-of-possession key". The verification path validates against the schema rather than the model, and the schema's jwk block constrained only kty, crv, x and y shapes. Everything else fell through additionalProperties, so a record carrying its own private key validated and sign.verify_record accepted it. No attacker is involved. This is a producer mistake the format did not defend against, and its consequence is durable: the record is signed, self-authenticating and typically anchored, so the only remedy afterwards is to revoke the identity. A format that can be handed the signing key by accident should say no in the half that verification actually calls. Both copies of the schema get the constraint and stay byte-identical, with a test that keeps them that way. sign.sign_record was never able to produce this, because it builds cnf from key_to_jwk, which returns the public half only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbDBXDWWvMFa7c2jGgyq9t
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.
Closes GHSA-vc4p-h84j-7qxj. Reproduced against current
mainbefore changing anything.The gap
RFC 8747 defines
cnfas a confirmation key: the public half, carried so a verifier can bind the record to the key that signed it.models.TrustRecordalready knew that. It refusesd,p,q,dp,dq,qiandkvia_JWK_PRIVATE_PARAMS, with the comment "cnf.jwk is a public proof-of-possession key".But the verification path validates against
schema/trace-claim.json, not the model. The schema'sjwkblock constrained only thekty/crv/x/yshapes through twoif/thenentries; everything else fell throughadditionalProperties. So a Trust Record carrying its own private key validated cleanly andverify_recordaccepted it.The rule existed, and only in the half that verification does not call.
Why it matters even with no attacker
There is no attacker step here. This is a producer mistake the format did not defend against. What makes it worth fixing rather than documenting is that the consequence is durable: the record is signed, self-authenticating, and typically anchored. Once it is out, the key is out, and the only remedy is to revoke the identity.
sign.sign_recordcould never produce this, because it buildscnffromkey_to_jwk, which returns the public half only. The exposure is a record assembled by hand or by another implementation, which is exactly the population a published schema exists to constrain.The fix
A
not/anyOfconstraint over the seven private JWK members, placed alongside the existingallOfshape rules, mirroring_JWK_PRIVATE_PARAMSin the reference model. Public JWKs are unaffected.Applied to both copies,
schema/trace-claim.json(the published artifact) andsrc/agentrust_trace/schema/trace-v0.2.json(what the package ships). They were byte-identical and still are, with a test that keeps them that way, since drifting copies is how this kind of gap survives a fix.Tests
Nine in
test_provenance_cnf_boundary.py: one per private member, two that ordinary OKP and EC public JWKs still validate, and the copy-equality check. Reverting the schema turns all seven member tests red.Full suite: 1172 passed, with the same 4 failures that reproduce on a clean
main(three fixture-regeneration tests and one schema-classification test, all unrelated).Not fixed here
agentrust-io/trace-testscarries a third copy of this schema with the same gap, and its conformance modules do not inspectcnf.jwkmembers (TR-ENV-004checks only thatktyis present, so a record carryingdpasses the suite). Tracked separately against that repo.🤖 Generated with Claude Code
https://claude.ai/code/session_01XbDBXDWWvMFa7c2jGgyq9t