Repository navigation
fix(provenance,conformance): reject private JWK members and wrap _pubkey_from_jwk errors (#535) - #541
Conversation
…key_from_jwk errors Reject RFC 7517/7518 private/secret JWK members (d, p, q, dp, dq, qi, k) in provenance.verify_record (both record['cnf']['jwk'] and trusted_jwk) and in conformance tr_sig (both check and check_cmcp_runtime), without echoing secret values in error or finding messages. Also wrap _pubkey_from_jwk(trusted_jwk) in provenance.verify_record so non-Ed25519 or malformed JWKs raise ProvenanceError instead of leaking a bare ValueError. Closes agentrust-io#535 Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
This PR closes the three gaps from #535 cleanly. provenance.verify_record now rejects all seven private JWK members on both cnf.jwk and trusted_jwk, does that check before any revocation callback, and wraps _pubkey_from_jwk failures as ProvenanceError instead of leaking a bare ValueError. Conformance tr_sig matches: check_cmcp_runtime rejects the full private set early, and plain check no longer keys only on "d" it uses the same _JWK_PRIVATE_PARAMS intersection. The set matches models/tr_env/schema, messages name only member keys (tests assert the secret is never echoed), and the new tests cover all seven members on both surfaces, including the signed level-1 plain-trace case and the revocation-not-called assertion.
One small nit: when _pubkey_from_jwk(trusted_jwk) fails, the message still says “the record's embedded key is unusable”; something like “trusted_jwk is unusable” would be more accurate, but it is not a functional issue
What this changes
Closes #535.
Fixes three gaps around RFC 7517 / RFC 7518 private key material (
"d","p","q","dp","dq","qi","k",GHSA-vc4p-h84j-7qxj) and JWK error handling acrossprovenance.verify_recordandconformance/src/trace_tests/modules/tr_sig.py:src/agentrust_trace/provenance.py(verify_record):record["cnf"]["jwk"]ortrusted_jwkcarrying private/secret JWK members (_JWK_PRIVATE_PARAMS = frozenset({"d", "p", "q", "dp", "dq", "qi", "k"})) withProvenanceError, naming only the offending member key names and never echoing secret values.trusted_jwkfor private key material before consultingrevocationso a call withrevocationsupplied does not invoke the revocation store or surface a thumbprint/revocation error before rejecting atrusted_jwkcarrying private key material._pubkey_from_jwk(trusted_jwk)intry ... except ValueError as exc: raise ProvenanceError(f"the record's embedded key is unusable: {exc}") from excso non-Ed25519 or malformed JWKs (RSA,EC,X25519, shortx, or padded/non-canonical base64urlx) raiseProvenanceErrorinstead of leaking a bareValueError.conformance/src/trace_tests/modules/tr_sig.py(checkandcheck_cmcp_runtime):_JWK_PRIVATE_PARAMS = frozenset({"d", "p", "q", "dp", "dq", "qi", "k"})(matchingtr_env.pyandagentrust_trace.models).check_cmcp_runtime, rejects anytrace.cnf.jwkcarrying a member of_JWK_PRIVATE_PARAMSwithFinding("TR-SIG-001", Status.FAIL, ...)before signature verification, without echoing the secret value.check(plaintraceformat), checks_JWK_PRIVATE_PARAMS.intersection(jwk)instead of only"d" in jwk, emittingTR-SIG-004(Status.FAIL) andTR-SIG-005(Status.UNVERIFIED) for all seven private JWK parameters without echoing the secret value.Cold-reader review (
code-reviewsub-agent):src/agentrust_trace/provenance.py): The initial placement of thetrusted_jwkprivate-key check ran afterif revocation is not None: _check_not_revoked(trusted_jwk, revocation). Whenrevocationwas passed alongside atrusted_jwkcontaining private key material,_check_not_revokedran first (invoking the external revocation store, or failing insidejwk_thumbprintforkty="oct").trusted_jwkprivate-key check ahead ofif revocation is not None:and added an assertion intest_provenance_verify_record_refuses_private_key_material_in_trusted_jwkverifying that a revocation store callback is never invoked whentrusted_jwkcarries private key material.conformance/tests/unit/test_tr_sig.py):test_plain_trace_all_private_jwk_members_fail_tr_sig_004initially used an unsignedECrecord atlevel=0; testing a validly signedOKP/Ed25519plaintracerecord atlevel=1directly pins that a validly signed record carrying any of the 7 private JWK members is never verified asTR-SIG-005PASS.test_plain_trace_all_private_jwk_members_fail_tr_sig_004to sign a validOKP/Ed25519plaintracerecord carrying each of the 7 private JWK members and check atlevel=1.Contributor: Raghuvar Durga (@drk-ram)
Type of change
Spec section
spec/server-provenance-v1.md/spec/server-provenance-v2.mdreference implementation (src/agentrust_trace/provenance.py)spec/trace-v0.2.mdsection 3.2.1 conformance module (conformance/src/trace_tests/modules/tr_sig.py)Tests for a fix
Verified with
python tools/review_bar.py --base origin/main --head HEAD(19 fail on behaviour onorigin/main, 0 fail on a missing name or file):tests/test_provenance_cnf_boundary.py::test_provenance_verify_record_refuses_private_key_material_in_cnf_jwk([d],[p],[q],[dp],[dq],[qi],[k]): fails onorigin/mainwithDID NOT RAISE ProvenanceError.tests/test_provenance_cnf_boundary.py::test_provenance_verify_record_refuses_private_key_material_in_trusted_jwk([d],[p],[q],[dp],[dq],[qi],[k]): fails onorigin/mainwithDID NOT RAISE ProvenanceError.tests/test_provenance_cnf_boundary.py::test_provenance_verify_record_wraps_pubkey_from_jwk_errors_in_provenance_error([rsa],[ec],[x25519],[short-x],[padded-x]): fails onorigin/mainwith unhandledValueError.conformance/tests/unit/test_tr_sig.py::test_cmcp_runtime_private_key_material_in_cnf_jwk_fails([d],[p],[q],[dp],[dq],[qi],[k]): fails when thecheck_cmcp_runtimeprivate-key check is switched off (Status.PASSinstead ofStatus.FAIL).conformance/tests/unit/test_tr_sig.py::test_plain_trace_all_private_jwk_members_fail_tr_sig_004([p],[q],[dp],[dq],[qi],[k]): fails whentr_sig.checkonly inspects"d" in jwk.Validation commands run and results:
pytest tests/test_provenance_cnf_boundary.py tests/test_provenance.py tests/test_public_functions_raise_what_they_document.py -q: passed (433 passed)pytest conformance/tests -q: passed (792 passed, 5 xpassed)python conformance/measurement/scripts/mutate_modules.py conformance: passed (27 checks measured, 27 verified, 0 unverified)python tools/review_bar.py --base origin/main --head HEAD: passed (19 fail on behaviour on base, 0 fail on missing name)ruff check src tests scripts: passedmypy src/agentrust_trace: passedpython tools/check_dashes.py: passedChecklist
git commit -s)CHANGELOG.mdupdated (for any normative change)<!-- CHANGED: #NNN: description -->in spec text