From 40128ef5840c2a0ef13527dd1cb7d27fd1e0ddef Mon Sep 17 00:00:00 2001 From: minitru Date: Sat, 17 Oct 2026 13:37:15 -0400 Subject: [PATCH 1/4] integrations: xChk registry (TRACE record-producer, Level 0; sealed receipts as external evidence) An xChk registry licenses AI agents under a verified person and seals a record of each gated act, with the person's own passkey assertion on the acts they answered. This listing is the relying party's side of the xchk-sealed-receipts crosswalk (trace-spec#560): verify the registry's Trust Record with released agentrust-trace, resolve each approval-outcome reference at the registry's public receipt endpoint, recompute the digest with rfc8785, and run the registry's standard-library verifier on the bundle; report one verdict per reference in the crosswalk's vocabulary. Fixtures from a live run (ENG-WNP3 run 3057): the approval as served, and the same person's assertion over another ask placed in it, told apart only by the challenge the device signed. Tests recompute both verdicts from the committed bytes; the live resolution is opt-in. Conformance: trace-tests verify, Level 0, PASS (15 checks). Index and Marketplace catalog regenerated. Co-Authored-By: Claude Fable 5.1 Signed-off-by: minitru --- .../workflows/xchk-registry-conformance.yml | 56 ++ README.md | 1 + integrations/xchk-registry/LICENSE | 201 +++++ integrations/xchk-registry/NOTICE | 8 + integrations/xchk-registry/README.md | 67 ++ .../01-approval-confirmed.bundle.json | 368 +++++++++ .../examples/01-approval-confirmed.json | 47 ++ .../08-assertion-over-another-ask.bundle.json | 368 +++++++++ .../xchk-registry/examples/expected.json | 13 + .../xchk-registry/examples/verify_receipt.py | 698 ++++++++++++++++++ integrations/xchk-registry/integration.yaml | 30 + integrations/xchk-registry/pyproject.toml | 24 + .../xchk-registry/tests/test_xchk_registry.py | 79 ++ integrations/xchk-registry/xchk_trace.py | 120 +++ marketplace/catalog.json | 26 +- 15 files changed, 2105 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/xchk-registry-conformance.yml create mode 100644 integrations/xchk-registry/LICENSE create mode 100644 integrations/xchk-registry/NOTICE create mode 100644 integrations/xchk-registry/README.md create mode 100644 integrations/xchk-registry/examples/01-approval-confirmed.bundle.json create mode 100644 integrations/xchk-registry/examples/01-approval-confirmed.json create mode 100644 integrations/xchk-registry/examples/08-assertion-over-another-ask.bundle.json create mode 100644 integrations/xchk-registry/examples/expected.json create mode 100644 integrations/xchk-registry/examples/verify_receipt.py create mode 100644 integrations/xchk-registry/integration.yaml create mode 100644 integrations/xchk-registry/pyproject.toml create mode 100644 integrations/xchk-registry/tests/test_xchk_registry.py create mode 100644 integrations/xchk-registry/xchk_trace.py diff --git a/.github/workflows/xchk-registry-conformance.yml b/.github/workflows/xchk-registry-conformance.yml new file mode 100644 index 00000000..9f357e00 --- /dev/null +++ b/.github/workflows/xchk-registry-conformance.yml @@ -0,0 +1,56 @@ +# xChk registry integration workflow. +# +# floating installs the latest released agentrust-trace, unpinned on purpose, as drift detection. +# fixed installs the agentrust-trace version named in integration.yaml tested_against. +# Both recompute the fixtures' verdicts from the committed bytes with the registry's own offline verifier and run +# the Level 0 conformance suite on the committed Trust Record. Neither reaches the registry: the live resolution +# test is opt-in (REGISTRY_LIVE=1) so CI never depends on a third party's uptime. +name: xchk-registry conformance +on: + push: + paths: + - "integrations/xchk-registry/**" + - ".github/workflows/xchk-registry-conformance.yml" + pull_request: + paths: + - "integrations/xchk-registry/**" + - ".github/workflows/xchk-registry-conformance.yml" + schedule: + - cron: "0 6 * * 1" # weekly: catch drift against the latest released packages + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + name: ${{ matrix.mode }} (py${{ matrix.python }}) + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13"] + mode: [floating, fixed] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python }} + - name: Install agentrust-trace and agentrust-trace-tests (latest release) + if: matrix.mode == 'floating' + run: pip install agentrust-trace agentrust-trace-tests + - name: Install agentrust-trace and agentrust-trace-tests (tested_against) + if: matrix.mode == 'fixed' + run: | + v=$(sed -n 's/^ agentrust-trace: "\(.*\)"$/\1/p' integrations/xchk-registry/integration.yaml) + t=$(sed -n 's/^ agentrust-trace-tests: "\(.*\)"$/\1/p' integrations/xchk-registry/integration.yaml) + test -n "$v" && test -n "$t" + pip install "agentrust-trace==$v" "agentrust-trace-tests==$t" + - name: Install this integration + run: pip install -e "integrations/xchk-registry[test]" + - name: Integration tests (offline) + run: pytest integrations/xchk-registry/tests -q + - name: Level 0 conformance on the committed Trust Record + run: trace-tests verify --record integrations/xchk-registry/examples/01-approval-confirmed.json --level 0 --max-age 315360000 diff --git a/README.md b/README.md index a39c9b3f..ac2b46b6 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ TRACE only works as a standard if it is genuinely neutral. Integrations are list | [WCM Key Release to TRACE](integrations/wcm-trace/) | agentrust-io | wcm, trace | community | | [WCM Triton Repository Staging](integrations/wcm-triton/) | agentrust-io | wcm | verified | | [WCM Serving Guard for vLLM](integrations/wcm-vllm/) | agentrust-io | wcm | verified | +| [xChk registry](integrations/xchk-registry/) | xChk | trace | community | | [agentrust-codex](plugins/agentrust-codex/) | agentrust-io | agent-manifest, trace | community | | [scheduled-agents](scheduled-agents/) | agentrust-io | trace | community | diff --git a/integrations/xchk-registry/LICENSE b/integrations/xchk-registry/LICENSE new file mode 100644 index 00000000..0130f690 --- /dev/null +++ b/integrations/xchk-registry/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 OntoGuard AI + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/integrations/xchk-registry/NOTICE b/integrations/xchk-registry/NOTICE new file mode 100644 index 00000000..046b6123 --- /dev/null +++ b/integrations/xchk-registry/NOTICE @@ -0,0 +1,8 @@ +xchk-registry integration: Apache-2.0, copyright (c) 2026 Sean MacGuire. + +examples/verify_receipt.py: the xChk registry's offline receipt verifier, copyright (c) 2026 Sean MacGuire, +MIT License (the notice in the file's header), vendored unchanged from https://in.xchk.io/verify/verify_receipt.py. + +examples/*.json: records minted and signed by the registry at https://in.xchk.io for plate ENG-WNP3, run 3057 +(18 October 2026); the bundle of case 08 is the registry's own fixture generator's crossing of two of that run's +approvals. The fixtures carry the plate owner's email and a public approval-photo URL; he chose to publish them. diff --git a/integrations/xchk-registry/README.md b/integrations/xchk-registry/README.md new file mode 100644 index 00000000..7d531fe2 --- /dev/null +++ b/integrations/xchk-registry/README.md @@ -0,0 +1,67 @@ +# xChk registry integration with TRACE + +An xChk registry licenses AI agents under a verified person and seals a record of each gated act, with the +person's own passkey assertion on the acts they answered. This integration is the relying party's side of the +[`xchk-sealed-receipts` crosswalk](https://github.com/agentrust-io/trace-spec/pull/560): it takes a Trust +Record the registry minted, resolves each `approval-outcome` reference to the sealed receipt it names, and +recomputes what may be concluded about the approval, offline, with the registry's own standard-library verifier. + +## What the registry produces + +- **A Trust Record per run**, software-only (Level 0, `origin` absent: the gate is in the execution path), + signed with the registry's seal key, whose `cnf.jwk.kid` is the key id published at + `https://in.xchk.io/.well-known/xchk-keys.json`. One `approval-outcome` reference per act a person was asked + about, approved or not. Route: `GET /api/agents//runs//trace-record` (plate owner's token). +- **A receipt bundle per record**, at `GET /api/receipt//bundle` (public): the record, its Merkle path to + the registry's seal, the seal's hybrid signatures and published keys, the Ravencoin anchor when confirmed, and, + when a person answered, the approver block: the WebAuthn assertion, the credential's public key, the registry's + signed binding of that credential to a named, identity-proofed person for that record, and (ceremonies from + 18 October 2026) the ask the device signed over, from which the challenge is recomputed. + +## What this code does + +``` +python xchk_trace.py record.json # resolve each reference at its resolver +python xchk_trace.py record.json --bundle bundle.json --jwk examples/expected.json # offline +``` + +1. Verifies the Trust Record with released `agentrust-trace` (the record's own `cnf.jwk`, or a pinned key). +2. For each `approval-outcome` reference, fetches `/api/receipt//bundle`, or uses a saved one. +3. Canonicalizes `bundle.record` with RFC 8785 and compares the digest with the reference. +4. Runs `examples/verify_receipt.py` on the bundle as a subprocess: record hash, Merkle path, seal hash, registry + signatures, the assertion (ECDSA P-256, `webauthn.get`, user verified), the binding (names this record, this + credential, this public key), the challenge (sha256 over the ask, recomputed from the bundle's `decision` + block), the anchor when present. +5. Reports a verdict per reference: `approval-confirmed`, `approval-confirmed-as-amended`, `not-an-approval`, + `approval-contradicted`, or `approval-unconfirmed` when the reference does not resolve (the record stands, + spec section 3.1.2 rule 3). + +`examples/` holds one record and two bundles from a live run: the approval as served, and the same person's +assertion from a different approval placed in it (every key and signature genuine; the challenge names another +ask). `tests/` recomputes both verdicts from the committed bytes and runs the CLI offline; set `REGISTRY_LIVE=1` to +also resolve the reference at the registry. + +## Run it + +``` +pip install -e "integrations/xchk-registry[test]" +pytest integrations/xchk-registry/tests -q +trace-tests verify --record integrations/xchk-registry/examples/01-approval-confirmed.json --level 0 +``` + +## What it does not claim + +- **Ordering.** A Trust Record is issued per execution and carries no commitment that the approval was checked + before the act ran; the registry's gate holds the act until the ask is answered, but that claim is the gate's, + not the record's. `authorized-intent` through the PIC bridge is the relation for a pre-execution authorization. +- **That the executed act is the approved one.** `record.paramsHash` binds the ask to the parameters the gate + saw; comparing that with what a runtime executed is the runtime's job. +- **Hardware.** Nothing here is attested; `runtime.platform` is software-only and `appraisal` is none. +- **Policy or person.** `basis` says which rule asked and `authLevel` how the person was proofed; whether either + suffices is the relying party's policy. +- **Bundles from before 18 October 2026** carry no `approver.challenge`; the verifier reports that step as pending + and the registry's binding is then the only thing pairing the assertion with the record. +- The registry's server code is not public; what is public is the receipt endpoint, the published keys, the + verifier and the fixtures. Regenerating fixtures is the registry operator's. + +Maintained by the registry operator (see `integration.yaml`). Personal research project. diff --git a/integrations/xchk-registry/examples/01-approval-confirmed.bundle.json b/integrations/xchk-registry/examples/01-approval-confirmed.bundle.json new file mode 100644 index 00000000..8a6a92a1 --- /dev/null +++ b/integrations/xchk-registry/examples/01-approval-confirmed.bundle.json @@ -0,0 +1,368 @@ +{ + "format": "xchk-receipt-bundle/2", + "issuer": { + "name": "xChk registry", + "base": "https://in.xchk.io", + "servedAt": "2026-10-17T15:54:51.987Z", + "headline": "ENG-WNP3: APPROVED terminal in global at 2026-10-17T15:46:00.571Z — approved by sean@maclawran.ca." + }, + "recordHash": "aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5", + "record": { + "v": 1, + "agentId": "6acbf860a627414b66045478", + "plate": "ENG-WNP3", + "seq": 9819, + "ts": "2026-10-17T15:46:00.571Z", + "harness": "claude", + "runSeq": 3057, + "runRef": "ENG WNP3.3057", + "area": "global", + "tool": "terminal", + "summary": "P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "paramsHash": "a791bccfa14f0666b7082aab7586455f1898da48651cbbbb643e0df3402655c6", + "amount": null, + "disposition": "approved", + "code": null, + "reason": null, + "deskCondition": "normal", + "continuityClass": "standard", + "authRef": "c2486b7f4874e5d08aba0725", + "prevHash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f", + "rvnBlock": "000000000001303bd2857ce3c400c92e8bcf8b97c14eda4f85650e03f91ef3e3@4585364", + "approverEmail": "sean@maclawran.ca", + "authMethod": "webauthn", + "attestationId": "6ad398295f7026f62ae7b863", + "attestationHash": "d7896ca4608bcdd8581ce6c0bb5917e541d40d635334e1a8746d7fc1bc230d5e", + "photoUrl": "https://xchk-main.s3.us-east-005.backblazeb2.com/xchk/xchk/baseline/att_6ad398295f7026f62ae7b863/0.jpg", + "model": "claude-fable-5-1" + }, + "canonicalization": { + "profile": "JSON with object keys sorted (code-unit order), no whitespace, scalars as JSON.stringify would emit them, undefined omitted, integers only; hash = sha256 over the UTF-8 bytes", + "hashedConditionalKeys": [ + "rvnBlock", + "approverEmail", + "authMethod", + "requiredAuthMethod", + "attestationId", + "attestationHash", + "photoUrl", + "labelsHash", + "delegationHash", + "model", + "tokensIn", + "tokensOut", + "tokensCacheRead", + "contextPct", + "actor", + "chargeCode", + "workspace", + "policyHash", + "outcomeOf", + "resultHash", + "endedAt", + "artifactHash" + ], + "note": "conditional keys are present only when the record carried them; absent keys are absent from the bytes, not null" + }, + "merkle": { + "proof": [ + { + "side": "L", + "hash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f" + }, + { + "side": "R", + "hash": "61cbbb71667c7ad8ed2fb67feadf3454a801ced41e3d303ba7b8297524c0e86b" + }, + { + "side": "R", + "hash": "c7f7fb52d081b787566876416919dca904cef18613bb63efa3e28b0ab8eeced5" + }, + { + "side": "R", + "hash": "30e986cfeef36f573d77f3f49948c8bc3248ee8e068d439c149ee72dbe2dad8b" + }, + { + "side": "R", + "hash": "b1f52c8bef88f4187114b4cfab6922468a0e2f3775dc9b6fa7d1479458d02019" + }, + { + "side": "R", + "hash": "b8de3bc958dcdcd07dfc74b47a39ede41e6c649d39f09505b88802fbfb5e7739" + } + ], + "leafCount": 37, + "rule": "node = sha256(left_hex + right_hex) over the hex strings; an odd node is promoted unhashed; the leaf is the record hash" + }, + "seal": { + "v": 1, + "agentId": "6acbf860a627414b66045478", + "plate": "ENG-WNP3", + "sealSeq": 1496, + "windowStart": "2026-10-17T15:44:25.565Z", + "windowEnd": "2026-10-17T15:53:48.404Z", + "count": 37, + "issuedCount": 37, + "seqStart": 9818, + "seqEnd": 9854, + "merkleRoot": "2662a87b9401541c4917428bda73596a608ad5ea556acdc110639e2e618092e9", + "deskCondition": "normal", + "prevSealHash": "82904359e52743e49d8c822814094b1519e02214578adef89487469b555c68c5", + "rvnBlock": "00000000000081b33c166cf1961e598cb91d40a6fef817b0d2d8aaf3b6b62753@4585373", + "profileHash": "9bbe0635c0a05cc8107d40ad5bb2d8b227c8125d395f41620bcd371123319701", + "sealHash": "a00007915e398e7a5f6fdd0863a7ebc0f3eac99da656b85a4fad0d06a8c15710", + "rule": "sealHash = sha256(canonical({v, agentId, plate, sealSeq, windowStart, windowEnd, count, issuedCount, seqStart, seqEnd, merkleRoot, deskCondition, prevSealHash[, rvnBlock][, profileHash]}))" + }, + "registrySignature": { + "alg": "ed25519+ml-dsa-65", + "keyId": "5ecb3da505414c4f", + "signature": "v+SHukcW2TmkZbafId5igQnOU75fbb51UNUJFRwp5DdQd1ebk+9Z7wZ9BXfBpmZUWLBgm3ZuNlP+IbVZrKH3Bg==", + "signedAt": "2026-10-17T15:53:48.458Z", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "v+SHukcW2TmkZbafId5igQnOU75fbb51UNUJFRwp5DdQd1ebk+9Z7wZ9BXfBpmZUWLBgm3ZuNlP+IbVZrKH3Bg==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ], + "signedOver": "the UTF-8 bytes of seal.sealHash", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=\n-----END PUBLIC KEY-----\n", + "wellKnown": "https://in.xchk.io/.well-known/xchk-keys.json", + "keys": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "publicKeyRaw": "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" + } + ], + "retired": [], + "publicKeyRawPq": "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", + "keyIdPq": "8bb85bc1f41c568c", + "root": { + "alg": "slh-dsa-sha2-128s", + "standard": "FIPS 205", + "keyId": "acc7ad1aeb3ef177", + "publicKeyRaw": "4aIhDuj3TZPwqb9HT8TzVJnkvle4jVUMhWkn/BfmypM=", + "statement": { + "v": 1, + "registry": "https://in.xchk.io", + "keys": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "publicKeyRaw": "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" + } + ], + "retired": [], + "oidc": [ + { + "kid": "XWUv0uGESGdIG1MjOzbVjcwxB0CYtK0oZ9SZxNkH7h8", + "kty": "RSA", + "alg": "RS256", + "n": "zQwdJyTuyqQvB5zIXp9ULjX22k-R95Ls5j3IVUwLcI87I2VPUjsvy9oDGl5cq8Jl7Rt21d966AakANr8J1vT7gMpcmPOcEGVDdgdCprRs8cWNDo5uJ4ejt1LW65zRqW8xsxqHPm1aOxvjeUQfNgZFLn3VlSX01zgjggBLSwL18stLbyIzA2zcj4lpV-IrnBO3S6VUusoaO__kXjHv1-aH64wpVcsNLrBX6MWsnHM-KptbhKrk_qqXMJ7dokBxiIAH32VjZ73kTbVcSXoU2FshM_eQaonhYcRoD8PTXN3rxMaisj2pka9XZ1Apqy-EHf1dCoUphYxd_--FcT5PDQZaQ", + "e": "AQAB" + } + ], + "issuedAt": "2026-10-17T15:48:57.541Z" + }, + "signature": "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", + "rule": "SLH-DSA-SHA2-128s verify(publicKeyRaw, canonical(statement) bytes, signature); statement.keys are the keys this registry signs with" + }, + "rule": "HYBRID: for each entry in signatures, verify(keys[alg,keyId].publicKeyRaw, sealHash bytes, signature) — Ed25519 for alg ed25519, ML-DSA-65 (FIPS 204) for alg ml-dsa-65; ALL must verify. keyId = sha256(SPKI or raw key)[:16]. root, when present, is the SLH-DSA-SHA2-128s statement of these keys." + }, + "approver": { + "email": "sean@maclawran.ca", + "authLevel": "didit", + "authMethod": "webauthn", + "attestationId": "6ad398295f7026f62ae7b863", + "credentialId": "Yqa88hkURHnPqSTiV3o1jA", + "publicKey": { + "format": "ec-p256-uncompressed", + "alg": "ES256", + "raw": "BIWcVSzcT4ocdVb/bHtVrZNK/4RKRn1xhg6SE8TSy3QGpnJUufsDz2z+G1PqK3yP/7q5IPI9PEfjC77W/OxqD5k=" + }, + "assertion": { + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNUVMY0t2aUhybWxuUElaME9Ca0hMZE9tM05aalVKdEN5U3d5QUItcElNYyIsIm9yaWdpbiI6Imh0dHBzOi8vaW4ueGNoay5pbyIsImNyb3NzT3JpZ2luIjpmYWxzZX0", + "authenticatorData": "dcRETaEWF68z8NRCzR6PN99N5Mn8aYw-RadnYwZSFr0dAAAAAA", + "signature": "MEUCIQDCuR26urn4n7AYJ2_Ohd9_KJ7hRp6pEm2qG1mqjnjMbAIgdUuoz3nO2St6MasEmX3TvKYiv8MoFn1UA9_li9nWCB8", + "encoding": "base64url" + }, + "challenge": { + "rule": "ask-v1", + "fields": { + "v": 1, + "attestationId": "6ad398295f7026f62ae7b863", + "prompt": "ENG-WNP3.3057 (via Claude Code) wants to P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "actionParamsHash": "5eee3b37a63eb957436a2bd51633d8a3c9a54f96c66e71f45fbd2d458373ca68" + }, + "expected": "5ELcKviHrmlnPIZ0OBkHLdOm3NZjUJtCySwyAB-pIMc", + "note": "the challenge the approver's device signed = base64url(sha256(canonical(fields))); fields are the ask as issued, before any answer; recompute, do not trust `expected`" + }, + "binding": { + "fields": { + "v": 1, + "recordHash": "aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5", + "attestationId": "6ad398295f7026f62ae7b863", + "credentialId": "Yqa88hkURHnPqSTiV3o1jA", + "publicKey": "BIWcVSzcT4ocdVb/bHtVrZNK/4RKRn1xhg6SE8TSy3QGpnJUufsDz2z+G1PqK3yP/7q5IPI9PEfjC77W/OxqD5k=", + "approverEmail": "sean@maclawran.ca", + "authLevel": "didit", + "authMethod": "webauthn", + "verifiedAt": "2026-10-17T15:46:00.404Z" + }, + "signature": "LpDId3mnpJVZn4ntyVIwrgy/sT1vdKmomgr8eoShKHRJhvh7xr7qvVD3ZhKaaYxa5rcL/E0JQX8ICh+Y/1vSDA==", + "keyId": "5ecb3da505414c4f", + "signedAt": "2026-10-17T15:54:51.971Z", + "alg": "ed25519+ml-dsa-65", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "LpDId3mnpJVZn4ntyVIwrgy/sT1vdKmomgr8eoShKHRJhvh7xr7qvVD3ZhKaaYxa5rcL/E0JQX8ICh+Y/1vSDA==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ], + "rule": "registry signature(s) over canonical(fields): each of `signatures` with the registrySignature.keys entry of the same alg/keyId (hybrid: all must verify); `signature` is the Ed25519 half" + }, + "rules": [ + "assertion: WebAuthn signature = ECDSA-P256/SHA-256 over authenticatorData || sha256(clientDataJSON), verified with publicKey.raw (uncompressed point); clientDataJSON.type must be \"webauthn.get\"; authenticatorData flags must include UV (user verified)", + "binding: verify each of binding.signatures (or binding.signature with alg ed25519) over canonical(binding.fields) with the matching registrySignature.keys entry — Ed25519 and, when present, ML-DSA-65; all must verify; binding.fields.recordHash must equal recordHash, binding.fields.credentialId must equal the assertion credential, and binding.fields.publicKey must equal approver.publicKey.raw (17 Oct 2026: an assertion re-signed by another key, with that key offered, passes every other check)", + "challenge: when approver.challenge is present (rule ask-v1, ceremonies from 18 Oct 2026), clientDataJSON.challenge must equal base64url(sha256(canonical(challenge.fields))) with fields.attestationId = approver.attestationId, fields.prompt = decision.prompt and fields.actionParamsHash = decision.actionParamsHash: the device signed over the ask, so the assertion itself names the act it answered. Absent, the challenge was a nonce and only the binding pairs the assertion with the record (an assertion by the same credential over another ask passes every other check)" + ] + }, + "basis": { + "asked": { + "seq": 9818, + "ts": "2026-10-17T15:45:45.315Z", + "area": "global", + "tool": "terminal", + "summary": "P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "paramsHash": "a791bccfa14f0666b7082aab7586455f1898da48651cbbbb643e0df3402655c6", + "code": "hook_escalation", + "reason": "destructive filesystem command outside scratch roots", + "recordHash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f", + "model": "claude-fable-5-1" + }, + "grant": { + "area": "global", + "disposition": "allow", + "policyId": null, + "maxAmount": null, + "resources": [] + }, + "authorizeDisposition": "attest", + "area": "global", + "objective": null, + "rule": "asked = the attest record this ceremony answers (same plate, same action summary, raised just before the ceremony; cite it by recordHash); grant = the plate's grant rows as they stood when the ceremony was raised; asked.reason is the registry's stated reason for asking" + }, + "decision": { + "prompt": "ENG-WNP3.3057 (via Claude Code) wants to P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "response": "approved", + "status": "completed", + "createdAt": "2026-10-17T15:45:45.553Z", + "respondedAt": "2026-10-17T15:46:00.404Z", + "by": "sean@maclawran.ca", + "authMethod": "webauthn", + "requiredAuthMethod": null, + "amendment": null, + "actionParamsHash": "5eee3b37a63eb957436a2bd51633d8a3c9a54f96c66e71f45fbd2d458373ca68", + "attestationHash": "d7896ca4608bcdd8581ce6c0bb5917e541d40d635334e1a8746d7fc1bc230d5e", + "attestationHashRule": "sha256 over the registry's canonical attestation JSON (utils/attestationHashUtils.computeAttestationHash: id, prompt, response, status, respondedAt, approver, method, amendment, delivery, chain …); recomputable on the registry, cited here so the record and this decision are bound" + }, + "legalHolds": null, + "anchor": { + "rvnTxId": null, + "rvnAnchoredAt": null, + "rvnBlock": "00000000000081b33c166cf1961e598cb91d40a6fef817b0d2d8aaf3b6b62753@4585373", + "explorer": null, + "otsProof": "8AgHtW3WwcdWHAjwIDmHgtDWxTNE7J5/xgGaHDFnrQrc3inlHM3qhjw4tw0GCPAQXdqsoXnzk3MwpP23KriFggjxIPLwuvDSO69qmiS9474S+Avisk6Nj3GHF2cAlCMhKZEgCPAgcg99+b7AHf2ZK3gpBTOQs3DbDNA7V9MlZXNzGCHcjq4I8CBgv3qhup4MMtQ5Z6CeKEgdkDbyf3ckONgeLlOyncp01gjxBGrTmg3wCL37YWeTW8icAIPf4w0u+QyOLi1odHRwczovL2FsaWNlLmJ0Yy5jYWxlbmRhci5vcGVudGltZXN0YW1wcy5vcmc=", + "otsCalendar": "https://a.pool.opentimestamps.org", + "otsBlockHeight": null, + "rule": "the anchor transaction carries sealHash in OP_RETURN; its raw bytes are captured once the transaction confirms (a few minutes) — download the bundle again for the offline anchor check, or use the explorer link" + }, + "howToVerify": [ + "1. canonicalize `record` per `canonicalization.profile`, sha256 it, compare to `recordHash`", + "2. walk `merkle.proof` from `recordHash` (L: sha256(step+cur), R: sha256(cur+step)); the result must equal `seal.merkleRoot`", + "3. canonicalize the seal fields per `seal.rule`, sha256 it, compare to `seal.sealHash`", + "4. verify every entry of `registrySignature.signatures` over the seal hash with the key of the same alg/keyId in `registrySignature.keys` (Ed25519 + ML-DSA-65 when hybrid); older bundles: Ed25519-verify `signature` with `publicKeyRaw`", + "5. if `approver` is present: verify the WebAuthn assertion with `approver.publicKey.raw`, then Ed25519-verify `approver.binding.signature` over canonical(`approver.binding.fields`) with the registry key, and require `approver.binding.fields.publicKey` to equal `approver.publicKey.raw`: the binding, not the bundle, names the key the assertion may be checked with; when `approver.challenge` is present, recompute base64url(sha256(canonical(`challenge.fields`))) and require it to equal `clientDataJSON.challenge`, with the fields equal to `approver.attestationId`, `decision.prompt` and `decision.actionParamsHash`", + "6. if `anchor.proof` is present: check the OP_RETURN, the txid, the header hash and the merkleblock; else look up `anchor.rvnTxId` on any Ravencoin explorer", + "8. if `seal.profileHash` is present: `profile.document` must be present, sha256(canonical(document)) must equal it, and `profile.signature` must verify over the hash with the registry keys; absent profileHash = sealed before profiles", + "7. if `basis`/`decision` are present: `basis.asked.recordHash` is another record on the same chain (fetch its bundle and run steps 1–4); `decision.attestationHash` equals `record.attestationHash`; `decision.amendment.command`, when present, is what ran" + ], + "profile": { + "profileHash": "9bbe0635c0a05cc8107d40ad5bb2d8b227c8125d395f41620bcd371123319701", + "document": { + "v": 1, + "name": "org", + "registry": "in.xchk.io", + "dials": { + "identity": [ + "email", + "domain" + ], + "registration": "person", + "anchoring": [ + "rvn", + "ots" + ], + "signing": "ed25519+ml-dsa-65", + "actSummary": { + "kept": "registry", + "maxChars": 400 + }, + "inbound": "on", + "operatorAccess": "recorded", + "actRecords": "kept", + "publicRecord": "plate", + "principalDomains": [ + "maclawran.ca" + ] + } + }, + "signature": { + "alg": "ed25519+ml-dsa-65", + "keyId": "5ecb3da505414c4f", + "signature": "3wM2ISHYyVQSahc6iEv70t47+skkAxg7sS7Nu6p82ZVT/WgN6iUdc+A+padJl1DJRcTJQErs3ppXsM8ZTQlEAQ==", + "signedAt": "2026-10-17T15:48:48.412Z", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "3wM2ISHYyVQSahc6iEv70t47+skkAxg7sS7Nu6p82ZVT/WgN6iUdc+A+padJl1DJRcTJQErs3ppXsM8ZTQlEAQ==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ] + }, + "rule": "sha256(canonical(document)) = seal.profileHash; verify signature over the UTF-8 bytes of profileHash with the same registrySignature.keys as the seal; document.dials are the rules this node ran under when it sealed the window" + }, + "verifier": { + "html": "https://in.xchk.io/verify-receipt.html", + "python": "https://in.xchk.io/verify/verify_receipt.py", + "license": "MIT" + } +} diff --git a/integrations/xchk-registry/examples/01-approval-confirmed.json b/integrations/xchk-registry/examples/01-approval-confirmed.json new file mode 100644 index 00000000..ca850981 --- /dev/null +++ b/integrations/xchk-registry/examples/01-approval-confirmed.json @@ -0,0 +1,47 @@ +{ + "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2", + "iat": 1792252491, + "subject": "did:web:in.xchk.io:plate:ENG-WNP3", + "model": { + "provider": "anthropic", + "model_id": "claude-fable-5-1" + }, + "runtime": { + "platform": "software-only", + "measurement": "sha256:c60a771a8f6e4fda60fbb2b745397c800c38da090f153f72e302147a1851746a" + }, + "policy": { + "bundle_hash": "sha256:8e1e29b1e2b1470b57fba453991d3c9ef0d0ae19ba59927c014525177ee741ae", + "enforcement_mode": "enforce" + }, + "data_class": "internal", + "tool_transcript": { + "hash": "sha256:54ea01281f1acf13dec96c509cb243c5ee87b826524044ac8c2700a63cd9f2bb", + "call_count": 101 + }, + "references": [ + { + "rel": "approval-outcome", + "id": "aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5", + "resolver": "https://in.xchk.io", + "digest": "sha256:aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5" + } + ], + "build_provenance": { + "slsa_level": 0, + "digest": "sha256:c60a771a8f6e4fda60fbb2b745397c800c38da090f153f72e302147a1851746a" + }, + "appraisal": { + "status": "none", + "verifier": "https://in.xchk.io" + }, + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k", + "kid": "5ecb3da505414c4f" + } + }, + "signature": "Vjgt2d7DVF_KmYdb3UXuOaL4YxRO1k46Uj91QkI5vc0EC5NraQ7U7S58DuD35d8GMy70wW-I08OtkRiCKTfPAQ" +} diff --git a/integrations/xchk-registry/examples/08-assertion-over-another-ask.bundle.json b/integrations/xchk-registry/examples/08-assertion-over-another-ask.bundle.json new file mode 100644 index 00000000..03bbf4c1 --- /dev/null +++ b/integrations/xchk-registry/examples/08-assertion-over-another-ask.bundle.json @@ -0,0 +1,368 @@ +{ + "format": "xchk-receipt-bundle/2", + "issuer": { + "name": "xChk registry", + "base": "https://in.xchk.io", + "servedAt": "2026-10-17T15:54:51.987Z", + "headline": "ENG-WNP3: APPROVED terminal in global at 2026-10-17T15:46:00.571Z — approved by sean@maclawran.ca." + }, + "recordHash": "aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5", + "record": { + "v": 1, + "agentId": "6acbf860a627414b66045478", + "plate": "ENG-WNP3", + "seq": 9819, + "ts": "2026-10-17T15:46:00.571Z", + "harness": "claude", + "runSeq": 3057, + "runRef": "ENG WNP3.3057", + "area": "global", + "tool": "terminal", + "summary": "P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "paramsHash": "a791bccfa14f0666b7082aab7586455f1898da48651cbbbb643e0df3402655c6", + "amount": null, + "disposition": "approved", + "code": null, + "reason": null, + "deskCondition": "normal", + "continuityClass": "standard", + "authRef": "c2486b7f4874e5d08aba0725", + "prevHash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f", + "rvnBlock": "000000000001303bd2857ce3c400c92e8bcf8b97c14eda4f85650e03f91ef3e3@4585364", + "approverEmail": "sean@maclawran.ca", + "authMethod": "webauthn", + "attestationId": "6ad398295f7026f62ae7b863", + "attestationHash": "d7896ca4608bcdd8581ce6c0bb5917e541d40d635334e1a8746d7fc1bc230d5e", + "photoUrl": "https://xchk-main.s3.us-east-005.backblazeb2.com/xchk/xchk/baseline/att_6ad398295f7026f62ae7b863/0.jpg", + "model": "claude-fable-5-1" + }, + "canonicalization": { + "profile": "JSON with object keys sorted (code-unit order), no whitespace, scalars as JSON.stringify would emit them, undefined omitted, integers only; hash = sha256 over the UTF-8 bytes", + "hashedConditionalKeys": [ + "rvnBlock", + "approverEmail", + "authMethod", + "requiredAuthMethod", + "attestationId", + "attestationHash", + "photoUrl", + "labelsHash", + "delegationHash", + "model", + "tokensIn", + "tokensOut", + "tokensCacheRead", + "contextPct", + "actor", + "chargeCode", + "workspace", + "policyHash", + "outcomeOf", + "resultHash", + "endedAt", + "artifactHash" + ], + "note": "conditional keys are present only when the record carried them; absent keys are absent from the bytes, not null" + }, + "merkle": { + "proof": [ + { + "side": "L", + "hash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f" + }, + { + "side": "R", + "hash": "61cbbb71667c7ad8ed2fb67feadf3454a801ced41e3d303ba7b8297524c0e86b" + }, + { + "side": "R", + "hash": "c7f7fb52d081b787566876416919dca904cef18613bb63efa3e28b0ab8eeced5" + }, + { + "side": "R", + "hash": "30e986cfeef36f573d77f3f49948c8bc3248ee8e068d439c149ee72dbe2dad8b" + }, + { + "side": "R", + "hash": "b1f52c8bef88f4187114b4cfab6922468a0e2f3775dc9b6fa7d1479458d02019" + }, + { + "side": "R", + "hash": "b8de3bc958dcdcd07dfc74b47a39ede41e6c649d39f09505b88802fbfb5e7739" + } + ], + "leafCount": 37, + "rule": "node = sha256(left_hex + right_hex) over the hex strings; an odd node is promoted unhashed; the leaf is the record hash" + }, + "seal": { + "v": 1, + "agentId": "6acbf860a627414b66045478", + "plate": "ENG-WNP3", + "sealSeq": 1496, + "windowStart": "2026-10-17T15:44:25.565Z", + "windowEnd": "2026-10-17T15:53:48.404Z", + "count": 37, + "issuedCount": 37, + "seqStart": 9818, + "seqEnd": 9854, + "merkleRoot": "2662a87b9401541c4917428bda73596a608ad5ea556acdc110639e2e618092e9", + "deskCondition": "normal", + "prevSealHash": "82904359e52743e49d8c822814094b1519e02214578adef89487469b555c68c5", + "rvnBlock": "00000000000081b33c166cf1961e598cb91d40a6fef817b0d2d8aaf3b6b62753@4585373", + "profileHash": "9bbe0635c0a05cc8107d40ad5bb2d8b227c8125d395f41620bcd371123319701", + "sealHash": "a00007915e398e7a5f6fdd0863a7ebc0f3eac99da656b85a4fad0d06a8c15710", + "rule": "sealHash = sha256(canonical({v, agentId, plate, sealSeq, windowStart, windowEnd, count, issuedCount, seqStart, seqEnd, merkleRoot, deskCondition, prevSealHash[, rvnBlock][, profileHash]}))" + }, + "registrySignature": { + "alg": "ed25519+ml-dsa-65", + "keyId": "5ecb3da505414c4f", + "signature": "v+SHukcW2TmkZbafId5igQnOU75fbb51UNUJFRwp5DdQd1ebk+9Z7wZ9BXfBpmZUWLBgm3ZuNlP+IbVZrKH3Bg==", + "signedAt": "2026-10-17T15:53:48.458Z", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "v+SHukcW2TmkZbafId5igQnOU75fbb51UNUJFRwp5DdQd1ebk+9Z7wZ9BXfBpmZUWLBgm3ZuNlP+IbVZrKH3Bg==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ], + "signedOver": "the UTF-8 bytes of seal.sealHash", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=\n-----END PUBLIC KEY-----\n", + "wellKnown": "https://in.xchk.io/.well-known/xchk-keys.json", + "keys": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "publicKeyRaw": "e7eet5v+xU8RgatEwvNj1cMMdHutsiouMGJ1Q32cMarb3CsMTIAY3JSVXWhmPqk+5q7aAwyI0nvSyGpPuK/C7uq5dv1ARQ/qaHRzwRnVN75jHTdyiL7qwHXWhYkqFL9rpHuo3hViiWhk20EWprNbHKLE8k9BKP+FBrqaFlvJ7NLIVbg/GmHuD09NGpT1H9MGTRjwlrw1FXZvMS9ZLhUdY3zjGec1KsPRiteJuosdgiWeYkWxPkzsmVgyRQDVj2Wi7zZjqOrYWuhi2b3DVb9hZ7zeuzxX7CY7Lm1Qmy//Ix4v0+DaTbcjNB+s+U2ChXUbu2Z1mqqgklZle5YY35MXbZ0UyRvVIN9Rd7AAYavNrPNS9idb25oqifkQWn5d1Q9+ztlLecAf1dD8oHhV6C03zOj+smmGpWkys11DqODLXX+0/Kp4ESFvN4qMzhlmRO6I2ECyIb/n+0AvMxK97afyP0m/PdaAE94Rn4+0oAN+xQwKv8CXnYI9xTAOmOtkGcMUK6dUt+Um+RHyz7gF4MZ381SfBDk0q1/s8ZLjZfq+LJdKUkgX1D+frAHvw97zmwA4gTbTE4+GHxlTlr3+PAbDgfeSPCGWljW74miMNW/+S07jpnUW4Amd5yOR1QNThtA8qHmvJd19F03kndxsLGpH1PwdUAsNcrSkvOl2p9X4dv57nOzXxFJtIP/JX72AB+neT0+ORSbtx5GdmaqaLvOpzeYFOAbzGbjqGw9EGJ/eAOR//CZiX0T2/qPQjntadvlJHM2DQYD8CwYNX4EDOZ/sczVtOQ6h205CzLo6jWAs3P6YkaEd0XLNSnj/dyQDYN0sfJn3FJWgEh0DsPfbXynGfb6I2SVdp+ht7dWPEzoEpmC3RUjw3Bi2P7k9gxDkZ62+qjY5Sb23PRuu3cO4BPD7XYj5a2B8C3Hv7kjkruiYUofJKf19H3iYQFUmtBzzdX8fx1/0NO+jchIUGApOhIjLe9/6Go66a5bjTnqNvyXNuJycOWyQTBDaumAqQilY9Yq8vW4k3HupWnzbYTEz18rBorb+0hbCzPGivpOuMgTxE6LdRUVN77FmddyQP2gN0iaIlNzXtJc8sfjT7th8JznIq5jjVhBK2cgVxB7ySGnNUXsbRJ+p+uiOTMf7O6o/T/6eb+79+XCxPunIiwW5JOQvE6GPS63bvuiYchsae2dgcsdaYUXsoe0FEAMqM+G+LsPaoYJildycC27UflXMMpbLUB0N3j1F5RTYGQLsORpzi/oEB9ptYK25hBICYryB4xteIhQcVWu32Ntt47M/2fN0lumWAFfoP593UtKNGcUM7G9zhSJfn6m+0R+sMixGvYw0NesnWp9fKnLAAIUrPvW7mnvYfSlUeVS076hzVR5JOEsZbrkRQg5/9njU0GaB22kYCEo5u0B6WiePz9OTWfTxz7xu6PaRTqOG2B0qLn81J12UTegOYPAZUlAj42i/llqpOfEWrlreIaT8kWLtxoafpaXl7kfX8a0ydzzD8dQ7JuswDMqRRbNnCcNIMyvU71v9LJW+rlAvrAVVQP7vhflJA7mPVNt6+bXRv1aMVfRVFM4kALWzYK2/6qoaDmC7OLHQuNqT44CFbj/6Z2gNcQO72tfLB005GvdbqiT8PJylpZ+TOFH7eqQFmmgIeQbeSVQZnqlq0KMtElArNRK8a4J+Zdh+yKNJ85xcFrcaFKlRPQdpetlvZYdRtFPf7iFMr3ubqYM7aNtvHrbXuaeMmZT9w35RJRtBSxQH43eNpf3zXh/VzkaKTSw7T9Un4FnTNG59O7rVYzxf0RKVI0wzwO9nRHGNa/sDjyCBu/vL7ElJ7FwbQudGLJQL07iBFJESKzfNm8heFO2lME8O05EBmsbLo3VMsTXyhi4M75cSGc5GaEt6FXuPh+ycboROYzNEpzffT2DYS3yZK+KF5hS0/lMm3NEyp4lYpk1cLydljNm/XONhGaSnEEBZLNicFKT4wWKZbRLzU1r87UQm//lWNRDxK3sFaegcjScGCewbXghEjsHRlQlIqnqAIFUTyvHiQVJqk+qnGDGRnGn47hmvZ3/RUvx6gMX/PYul5aDYcBihXh88SzwV88rB1tIRscPte4XroBNoqB2Q97OCpSm1aQ1R52ESz+bWdELdpDZpn/q+wx+vXp2NswIHncmz5VtfA9dHIAgcM7n7tUl/U6BVjmAW7X93Eh/YYxXzAU4/ZCxLYcluDhrg73H+le1BpIZ7j/MrV+fbHsQEtiUZzg/RjrwSaabCXakfKMlxDZ6ODwBE0OkuiNlkztrQCqR6c1/Pvg7fMUP0vxlsIyd47aBCgdC/OFUmAVBC1nq7HOxJ5wlkKeIQTL/q8QWwyhrDTsK/7CHjJ9awUhO8uCbrx7X8Yc77q6QJg7KEHm6gTQSEhq8cvnmtn40iQv8mzTDg7HqFOuPNLFLsjdDF7KByOIYsPGw1A8JJ+ATpcHo0sHjjA8/ye5uayNhtGpaGdsOG7HxhB1e98uSvr1fUCmzs4orc/HJuDbWCaVMjP5TdOTVmcKoylzEY9adZpFibKJV89j5iX46fxS4uURxIaLiYENo/2IS4+StY0sBbFTMRfy/9jWw3oQQ=" + } + ], + "retired": [], + "publicKeyRawPq": "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", + "keyIdPq": "8bb85bc1f41c568c", + "root": { + "alg": "slh-dsa-sha2-128s", + "standard": "FIPS 205", + "keyId": "acc7ad1aeb3ef177", + "publicKeyRaw": "4aIhDuj3TZPwqb9HT8TzVJnkvle4jVUMhWkn/BfmypM=", + "statement": { + "v": 1, + "registry": "https://in.xchk.io", + "keys": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "publicKeyRaw": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k=" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "publicKeyRaw": "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" + } + ], + "retired": [], + "oidc": [ + { + "kid": "XWUv0uGESGdIG1MjOzbVjcwxB0CYtK0oZ9SZxNkH7h8", + "kty": "RSA", + "alg": "RS256", + "n": "zQwdJyTuyqQvB5zIXp9ULjX22k-R95Ls5j3IVUwLcI87I2VPUjsvy9oDGl5cq8Jl7Rt21d966AakANr8J1vT7gMpcmPOcEGVDdgdCprRs8cWNDo5uJ4ejt1LW65zRqW8xsxqHPm1aOxvjeUQfNgZFLn3VlSX01zgjggBLSwL18stLbyIzA2zcj4lpV-IrnBO3S6VUusoaO__kXjHv1-aH64wpVcsNLrBX6MWsnHM-KptbhKrk_qqXMJ7dokBxiIAH32VjZ73kTbVcSXoU2FshM_eQaonhYcRoD8PTXN3rxMaisj2pka9XZ1Apqy-EHf1dCoUphYxd_--FcT5PDQZaQ", + "e": "AQAB" + } + ], + "issuedAt": "2026-10-17T15:48:57.541Z" + }, + "signature": "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", + "rule": "SLH-DSA-SHA2-128s verify(publicKeyRaw, canonical(statement) bytes, signature); statement.keys are the keys this registry signs with" + }, + "rule": "HYBRID: for each entry in signatures, verify(keys[alg,keyId].publicKeyRaw, sealHash bytes, signature) — Ed25519 for alg ed25519, ML-DSA-65 (FIPS 204) for alg ml-dsa-65; ALL must verify. keyId = sha256(SPKI or raw key)[:16]. root, when present, is the SLH-DSA-SHA2-128s statement of these keys." + }, + "approver": { + "email": "sean@maclawran.ca", + "authLevel": "didit", + "authMethod": "webauthn", + "attestationId": "6ad398295f7026f62ae7b863", + "credentialId": "Yqa88hkURHnPqSTiV3o1jA", + "publicKey": { + "format": "ec-p256-uncompressed", + "alg": "ES256", + "raw": "BIWcVSzcT4ocdVb/bHtVrZNK/4RKRn1xhg6SE8TSy3QGpnJUufsDz2z+G1PqK3yP/7q5IPI9PEfjC77W/OxqD5k=" + }, + "assertion": { + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWk4yQ2hBNUFta2d4ZFlRTTBmQmt2dUNodlN3MlRGYkNiUTFleDYtZV9nTSIsIm9yaWdpbiI6Imh0dHBzOi8vaW4ueGNoay5pbyIsImNyb3NzT3JpZ2luIjpmYWxzZX0", + "authenticatorData": "dcRETaEWF68z8NRCzR6PN99N5Mn8aYw-RadnYwZSFr0dAAAAAA", + "signature": "MEQCIE-K_G0S-XzSq-cPL06uEI15VLwHx-BuSh4tIz53DBTrAiB-4X-cBD_216b-Vpkkx5QIEGE-LaY_UOebBSrC-CekMQ", + "encoding": "base64url" + }, + "challenge": { + "rule": "ask-v1", + "fields": { + "v": 1, + "attestationId": "6ad398295f7026f62ae7b863", + "prompt": "ENG-WNP3.3057 (via Claude Code) wants to P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "actionParamsHash": "5eee3b37a63eb957436a2bd51633d8a3c9a54f96c66e71f45fbd2d458373ca68" + }, + "expected": "5ELcKviHrmlnPIZ0OBkHLdOm3NZjUJtCySwyAB-pIMc", + "note": "the challenge the approver's device signed = base64url(sha256(canonical(fields))); fields are the ask as issued, before any answer; recompute, do not trust `expected`" + }, + "binding": { + "fields": { + "v": 1, + "recordHash": "aa2bfd5b1d9b6307ed22eb0daa4213383beecb57d7d31ec011060ac98f7fe0b5", + "attestationId": "6ad398295f7026f62ae7b863", + "credentialId": "Yqa88hkURHnPqSTiV3o1jA", + "publicKey": "BIWcVSzcT4ocdVb/bHtVrZNK/4RKRn1xhg6SE8TSy3QGpnJUufsDz2z+G1PqK3yP/7q5IPI9PEfjC77W/OxqD5k=", + "approverEmail": "sean@maclawran.ca", + "authLevel": "didit", + "authMethod": "webauthn", + "verifiedAt": "2026-10-17T15:46:00.404Z" + }, + "signature": "LpDId3mnpJVZn4ntyVIwrgy/sT1vdKmomgr8eoShKHRJhvh7xr7qvVD3ZhKaaYxa5rcL/E0JQX8ICh+Y/1vSDA==", + "keyId": "5ecb3da505414c4f", + "signedAt": "2026-10-17T15:54:51.971Z", + "alg": "ed25519+ml-dsa-65", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "LpDId3mnpJVZn4ntyVIwrgy/sT1vdKmomgr8eoShKHRJhvh7xr7qvVD3ZhKaaYxa5rcL/E0JQX8ICh+Y/1vSDA==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ], + "rule": "registry signature(s) over canonical(fields): each of `signatures` with the registrySignature.keys entry of the same alg/keyId (hybrid: all must verify); `signature` is the Ed25519 half" + }, + "rules": [ + "assertion: WebAuthn signature = ECDSA-P256/SHA-256 over authenticatorData || sha256(clientDataJSON), verified with publicKey.raw (uncompressed point); clientDataJSON.type must be \"webauthn.get\"; authenticatorData flags must include UV (user verified)", + "binding: verify each of binding.signatures (or binding.signature with alg ed25519) over canonical(binding.fields) with the matching registrySignature.keys entry — Ed25519 and, when present, ML-DSA-65; all must verify; binding.fields.recordHash must equal recordHash, binding.fields.credentialId must equal the assertion credential, and binding.fields.publicKey must equal approver.publicKey.raw (17 Oct 2026: an assertion re-signed by another key, with that key offered, passes every other check)", + "challenge: when approver.challenge is present (rule ask-v1, ceremonies from 18 Oct 2026), clientDataJSON.challenge must equal base64url(sha256(canonical(challenge.fields))) with fields.attestationId = approver.attestationId, fields.prompt = decision.prompt and fields.actionParamsHash = decision.actionParamsHash: the device signed over the ask, so the assertion itself names the act it answered. Absent, the challenge was a nonce and only the binding pairs the assertion with the record (an assertion by the same credential over another ask passes every other check)" + ] + }, + "basis": { + "asked": { + "seq": 9818, + "ts": "2026-10-17T15:45:45.315Z", + "area": "global", + "tool": "terminal", + "summary": "P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "paramsHash": "a791bccfa14f0666b7082aab7586455f1898da48651cbbbb643e0df3402655c6", + "code": "hook_escalation", + "reason": "destructive filesystem command outside scratch roots", + "recordHash": "25607c8e2e529dddce618b49b1c11da6b302a6687c6d44b335f304e904dc796f", + "model": "claude-fable-5-1" + }, + "grant": { + "area": "global", + "disposition": "allow", + "policyId": null, + "maxAmount": null, + "resources": [] + }, + "authorizeDisposition": "attest", + "area": "global", + "objective": null, + "rule": "asked = the attest record this ceremony answers (same plate, same action summary, raised just before the ceremony; cite it by recordHash); grant = the plate's grant rows as they stood when the ceremony was raised; asked.reason is the registry's stated reason for asking" + }, + "decision": { + "prompt": "ENG-WNP3.3057 (via Claude Code) wants to P=\".ab1-probe-approve1-$(date +%s)\"; rm -f \"$P\" && echo \"probe 1 ran (nothing existed to remove)\"", + "response": "approved", + "status": "completed", + "createdAt": "2026-10-17T15:45:45.553Z", + "respondedAt": "2026-10-17T15:46:00.404Z", + "by": "sean@maclawran.ca", + "authMethod": "webauthn", + "requiredAuthMethod": null, + "amendment": null, + "actionParamsHash": "5eee3b37a63eb957436a2bd51633d8a3c9a54f96c66e71f45fbd2d458373ca68", + "attestationHash": "d7896ca4608bcdd8581ce6c0bb5917e541d40d635334e1a8746d7fc1bc230d5e", + "attestationHashRule": "sha256 over the registry's canonical attestation JSON (utils/attestationHashUtils.computeAttestationHash: id, prompt, response, status, respondedAt, approver, method, amendment, delivery, chain …); recomputable on the registry, cited here so the record and this decision are bound" + }, + "legalHolds": null, + "anchor": { + "rvnTxId": null, + "rvnAnchoredAt": null, + "rvnBlock": "00000000000081b33c166cf1961e598cb91d40a6fef817b0d2d8aaf3b6b62753@4585373", + "explorer": null, + "otsProof": "8AgHtW3WwcdWHAjwIDmHgtDWxTNE7J5/xgGaHDFnrQrc3inlHM3qhjw4tw0GCPAQXdqsoXnzk3MwpP23KriFggjxIPLwuvDSO69qmiS9474S+Avisk6Nj3GHF2cAlCMhKZEgCPAgcg99+b7AHf2ZK3gpBTOQs3DbDNA7V9MlZXNzGCHcjq4I8CBgv3qhup4MMtQ5Z6CeKEgdkDbyf3ckONgeLlOyncp01gjxBGrTmg3wCL37YWeTW8icAIPf4w0u+QyOLi1odHRwczovL2FsaWNlLmJ0Yy5jYWxlbmRhci5vcGVudGltZXN0YW1wcy5vcmc=", + "otsCalendar": "https://a.pool.opentimestamps.org", + "otsBlockHeight": null, + "rule": "the anchor transaction carries sealHash in OP_RETURN; its raw bytes are captured once the transaction confirms (a few minutes) — download the bundle again for the offline anchor check, or use the explorer link" + }, + "howToVerify": [ + "1. canonicalize `record` per `canonicalization.profile`, sha256 it, compare to `recordHash`", + "2. walk `merkle.proof` from `recordHash` (L: sha256(step+cur), R: sha256(cur+step)); the result must equal `seal.merkleRoot`", + "3. canonicalize the seal fields per `seal.rule`, sha256 it, compare to `seal.sealHash`", + "4. verify every entry of `registrySignature.signatures` over the seal hash with the key of the same alg/keyId in `registrySignature.keys` (Ed25519 + ML-DSA-65 when hybrid); older bundles: Ed25519-verify `signature` with `publicKeyRaw`", + "5. if `approver` is present: verify the WebAuthn assertion with `approver.publicKey.raw`, then Ed25519-verify `approver.binding.signature` over canonical(`approver.binding.fields`) with the registry key, and require `approver.binding.fields.publicKey` to equal `approver.publicKey.raw`: the binding, not the bundle, names the key the assertion may be checked with; when `approver.challenge` is present, recompute base64url(sha256(canonical(`challenge.fields`))) and require it to equal `clientDataJSON.challenge`, with the fields equal to `approver.attestationId`, `decision.prompt` and `decision.actionParamsHash`", + "6. if `anchor.proof` is present: check the OP_RETURN, the txid, the header hash and the merkleblock; else look up `anchor.rvnTxId` on any Ravencoin explorer", + "8. if `seal.profileHash` is present: `profile.document` must be present, sha256(canonical(document)) must equal it, and `profile.signature` must verify over the hash with the registry keys; absent profileHash = sealed before profiles", + "7. if `basis`/`decision` are present: `basis.asked.recordHash` is another record on the same chain (fetch its bundle and run steps 1–4); `decision.attestationHash` equals `record.attestationHash`; `decision.amendment.command`, when present, is what ran" + ], + "profile": { + "profileHash": "9bbe0635c0a05cc8107d40ad5bb2d8b227c8125d395f41620bcd371123319701", + "document": { + "v": 1, + "name": "org", + "registry": "in.xchk.io", + "dials": { + "identity": [ + "email", + "domain" + ], + "registration": "person", + "anchoring": [ + "rvn", + "ots" + ], + "signing": "ed25519+ml-dsa-65", + "actSummary": { + "kept": "registry", + "maxChars": 400 + }, + "inbound": "on", + "operatorAccess": "recorded", + "actRecords": "kept", + "publicRecord": "plate", + "principalDomains": [ + "maclawran.ca" + ] + } + }, + "signature": { + "alg": "ed25519+ml-dsa-65", + "keyId": "5ecb3da505414c4f", + "signature": "3wM2ISHYyVQSahc6iEv70t47+skkAxg7sS7Nu6p82ZVT/WgN6iUdc+A+padJl1DJRcTJQErs3ppXsM8ZTQlEAQ==", + "signedAt": "2026-10-17T15:48:48.412Z", + "signatures": [ + { + "alg": "ed25519", + "keyId": "5ecb3da505414c4f", + "signature": "3wM2ISHYyVQSahc6iEv70t47+skkAxg7sS7Nu6p82ZVT/WgN6iUdc+A+padJl1DJRcTJQErs3ppXsM8ZTQlEAQ==" + }, + { + "alg": "ml-dsa-65", + "keyId": "8bb85bc1f41c568c", + "signature": "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" + } + ] + }, + "rule": "sha256(canonical(document)) = seal.profileHash; verify signature over the UTF-8 bytes of profileHash with the same registrySignature.keys as the seal; document.dials are the rules this node ran under when it sealed the window" + }, + "verifier": { + "html": "https://in.xchk.io/verify-receipt.html", + "python": "https://in.xchk.io/verify/verify_receipt.py", + "license": "MIT" + } +} diff --git a/integrations/xchk-registry/examples/expected.json b/integrations/xchk-registry/examples/expected.json new file mode 100644 index 00000000..4293f2fb --- /dev/null +++ b/integrations/xchk-registry/examples/expected.json @@ -0,0 +1,13 @@ +{ + "trace_signer_jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "mn02E1XyTkaVN6DEOemU5OADMUAUjPHw67oR5KKIM3k", + "kid": "5ecb3da505414c4f" + }, + "resolver": "https://in.xchk.io", + "plate": "ENG-WNP3", + "runSeq": 3057, + "generated_at": "2026-10-17T15:54:52.151Z", + "note": "The signer key is the registry's seal key, published at /.well-known/xchk-keys.json; pinned here so the test runs offline." +} \ No newline at end of file diff --git a/integrations/xchk-registry/examples/verify_receipt.py b/integrations/xchk-registry/examples/verify_receipt.py new file mode 100644 index 00000000..2342537b --- /dev/null +++ b/integrations/xchk-registry/examples/verify_receipt.py @@ -0,0 +1,698 @@ +#!/usr/bin/env python3 +"""Verify an xChk receipt bundle with no network. Standard library only (post-quantum half optional, see below). + +MIT License. Copyright (c) 2026 Sean MacGuire. Permission is hereby granted, free of charge, to any person +obtaining a copy of this file, to deal in it without restriction, subject to this notice being included. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + +Usage: python3 verify_receipt.py xchk-receipt-.json [file] +Exit: 0 = every offline check passed; 1 = a check failed; 2 = could not read the bundle. + +With a second argument (5 Oct 2026), also checks that FILE is the one this receipt's action wrote: sha256 of its +bytes equals the record's artifactHash, which check 1 has already tied to the record hash. That proves which plate +and run left those bytes on the host, as the host reported it; it does not prove the content is true. + +Checks (each reproduces the rule the registry used to write the bytes; bundle formats /1 and /2): + 1. record hash = sha256(canonical(record)) canonical = sorted keys, no whitespace, JSON scalars, UTF-8 + 2. Merkle path from the record hash to the seal root node = sha256(left_hex + right_hex); odd node promoted unhashed + 3. seal hash = sha256(canonical(seal fields)) + 4. registry sig EVERY signature the seal carries, over the seal-hash bytes, each with the pinned key of its alg and + keyId: Ed25519 (pure Python, below) and — since 24 Sep 2026 — ML-DSA-65 (FIPS 204). A hybrid must + pass both. Unknown algs fail. Key ids are recomputed from the pinned keys. + The ML-DSA half needs `pip install dilithium-py` (pure Python, MIT); without it that half is + reported as WAIT, never as a pass, and the exit code is 1 only if something actually failed. + 4b. root the SLH-DSA-SHA2-128s (FIPS 205) key statement, when the bundle carries one: shown, and checked that + every pinned key is listed in it; the SLH-DSA signature itself needs a library this script does not + ship (WAIT) — pin the root's public key once and check it with any FIPS 205 implementation. + 5. approver the passkey assertion that answered: ECDSA P-256 over authenticatorData || sha256(clientDataJSON), + user-verified flag set; then the registry's identity binding (same rules as 4) tying that credential + to this person for THIS record hash + 6. anchor the raw Ravencoin transaction carries the seal hash in OP_RETURN; sha256d(tx) is the txid; the + merkleblock's partial Merkle tree reproduces its header's Merkle root and includes the txid. + Left for you to confirm on any explorer: that block hash + Merkle root are on the chain + (Ravencoin's block hash is a KAWPOW hash, which this script does not compute). +""" +import base64 +import hashlib +import json +import sys + +try: # optional post-quantum half: https://pypi.org/project/dilithium-py (pure Python) + from dilithium_py.ml_dsa import ML_DSA_65 as _ML_DSA_65 +except Exception: # noqa: BLE001 + _ML_DSA_65 = None + +# ----------------------------------------------------------------------------------------------------- canonical + +SEAL_KEYS = ["v", "agentId", "plate", "sealSeq", "windowStart", "windowEnd", "count", "issuedCount", + "seqStart", "seqEnd", "merkleRoot", "deskCondition", "prevSealHash", "rvnBlock", "profileHash"] +# rvnBlock and profileHash (6 Oct 2026) enter the hash only when the seal carries them + + +def canonical(value): + if isinstance(value, bool) or value is None or isinstance(value, (int, str)): + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + if isinstance(value, float): + raise ValueError(f"non-integer number {value} is outside the sealed value profile") + if isinstance(value, list): + return "[" + ",".join(canonical(v) for v in value) + "]" + if isinstance(value, dict): + return "{" + ",".join(json.dumps(k, ensure_ascii=False, separators=(",", ":")) + ":" + canonical(value[k]) + for k in sorted(value.keys())) + "}" + raise ValueError(f"unsupported value type {type(value).__name__}") + + +def sha256_hex(s: str) -> str: + return hashlib.sha256(s.encode("utf-8")).hexdigest() + + +def sha256d(b: bytes) -> bytes: + return hashlib.sha256(hashlib.sha256(b).digest()).digest() + + +def b64d(s) -> bytes: + if isinstance(s, dict): + s = s.get("signature", "") + s = str(s).replace("-", "+").replace("_", "/") + return base64.b64decode(s + "=" * (-len(s) % 4)) + +# ------------------------------------------------------------------------------------------------------- Ed25519 +# Reference algorithm (Bernstein et al.), affine arithmetic; a few hundred ms per signature, no dependencies. + +_q = 2 ** 255 - 19 +_l = 2 ** 252 + 27742317777372353535851937790883648493 + + +def _inv(x): + return pow(x, _q - 2, _q) + + +_d = (-121665 * _inv(121666)) % _q +_I = pow(2, (_q - 1) // 4, _q) + + +def _xrecover(y): + xx = (y * y - 1) * _inv(_d * y * y + 1) + x = pow(xx, (_q + 3) // 8, _q) + if (x * x - xx) % _q != 0: + x = (x * _I) % _q + if x % 2 != 0: + x = _q - x + return x + + +_By = (4 * _inv(5)) % _q +_B = (_xrecover(_By), _By) + + +def _add(P, Q): + x1, y1 = P + x2, y2 = Q + x3 = (x1 * y2 + x2 * y1) * _inv(1 + _d * x1 * x2 * y1 * y2) + y3 = (y1 * y2 + x1 * x2) * _inv(1 - _d * x1 * x2 * y1 * y2) + return (x3 % _q, y3 % _q) + + +def _mul(P, e): + Q = (0, 1) + while e: + if e & 1: + Q = _add(Q, P) + P = _add(P, P) + e >>= 1 + return Q + + +def _decodepoint(b: bytes): + y = int.from_bytes(b, "little") & ((1 << 255) - 1) + x = _xrecover(y) + if (x & 1) != (b[31] >> 7): + x = _q - x + if (-x * x + y * y - 1 - _d * x * x * y * y) % _q != 0: + raise ValueError("point not on curve") + return (x, y) + + +def ed25519_verify(pub_raw: bytes, msg: bytes, sig: bytes) -> bool: + if len(sig) != 64 or len(pub_raw) != 32: + return False + try: + R = _decodepoint(sig[:32]) + A = _decodepoint(pub_raw) + except ValueError: + return False + S = int.from_bytes(sig[32:], "little") + if S >= _l: + return False + h = int.from_bytes(hashlib.sha512(sig[:32] + pub_raw + msg).digest(), "little") % _l + return _mul(_B, S) == _add(R, _mul(A, h)) + +# ---------------------------------------------------------------------------------------- key ids + registry sigs + +ED_SPKI_PREFIX = bytes.fromhex("302a300506032b6570032100") + + +def key_id_for(alg: str, pub_raw: bytes) -> str: + """Ed25519 ids hash the SPKI DER; post-quantum ids hash the raw public key. 16 hex chars.""" + data = ED_SPKI_PREFIX + pub_raw if alg == "ed25519" else pub_raw + return hashlib.sha256(data).hexdigest()[:16] + + +def ml_dsa_65_verify(pub_raw: bytes, msg: bytes, sig: bytes): + """True/False, or None when dilithium-py is not installed.""" + if _ML_DSA_65 is None: + return None + try: + return bool(_ML_DSA_65.verify(pub_raw, msg, sig)) + except Exception: # noqa: BLE001 + return False + + +def pinned_keys(rs: dict) -> list: + keys = rs.get("keys") + if isinstance(keys, list) and keys: + return keys + if rs.get("publicKeyRaw"): # pre-hybrid bundle: one Ed25519 key + alg = rs.get("alg") if rs.get("alg") and rs.get("alg") != "ed25519+ml-dsa-65" else "ed25519" + return [{"alg": alg, "keyId": rs.get("keyId"), "publicKeyRaw": rs["publicKeyRaw"]}] + return [] + + +def registry_verify(ok, sig_obj: dict, msg: bytes, keys: list, what: str) -> int: + """Verify every signature the object lists. Returns the number of WAITs (unverifiable halves); FAILs go through ok().""" + sigs = sig_obj.get("signatures") if isinstance(sig_obj.get("signatures"), list) and sig_obj.get("signatures") else None + if not sigs and sig_obj.get("signature"): + alg = sig_obj.get("alg") if sig_obj.get("alg") and sig_obj.get("alg") != "ed25519+ml-dsa-65" else "ed25519" + sigs = [{"alg": alg, "keyId": sig_obj.get("keyId"), "signature": sig_obj["signature"]}] + if not sigs: + ok(False, f"{what}: no signature to check") + return 0 + waits = 0 + for sig in sigs: + alg = sig.get("alg") + key = next((k for k in keys if k.get("alg") == alg and (not sig.get("keyId") or not k.get("keyId") or k.get("keyId") == sig.get("keyId"))), None) + if not key: + ok(False, f"{what}: no pinned key for alg {alg!r} keyId {sig.get('keyId')!r}") + continue + pub = b64d(key["publicKeyRaw"]) + kid = key_id_for(alg, pub) + if key.get("keyId") and kid != key.get("keyId"): + ok(False, f"{what}: pinned {alg} key's id recomputes to {kid}, not {key.get('keyId')}") + continue + if alg == "ed25519": + r = ed25519_verify(pub, msg, b64d(sig["signature"])) + elif alg == "ml-dsa-65": + r = ml_dsa_65_verify(pub, msg, b64d(sig["signature"])) + else: + r = False + if r is None: + waits += 1 + print(f" WAIT {what}: {alg} signature present (keyId {kid}) — install dilithium-py to verify the post-quantum half") + else: + ok(r, f"{what}: {alg} signature verifies with pinned key {kid}" + (" (FIPS 204, post-quantum)" if alg == "ml-dsa-65" else "")) + if len(sigs) > 1 and waits == 0: + print(f" .. {what}: HYBRID — {' + '.join(s.get('alg', '?') for s in sigs)}, every half checked") + return waits + +# --------------------------------------------------------------------------------------------------- ECDSA P-256 + +_p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff +_n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 +_Gx = 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296 +_Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5 +_b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b + + +def _p256_add(P, Q): + if P is None: + return Q + if Q is None: + return P + x1, y1 = P + x2, y2 = Q + if x1 == x2 and (y1 + y2) % _p == 0: + return None + if P == Q: + lam = (3 * x1 * x1 - 3) * pow(2 * y1, _p - 2, _p) % _p + else: + lam = (y2 - y1) * pow(x2 - x1, _p - 2, _p) % _p + x3 = (lam * lam - x1 - x2) % _p + return (x3, (lam * (x1 - x3) - y1) % _p) + + +def _p256_mul(P, e): + Q = None + while e: + if e & 1: + Q = _p256_add(Q, P) + P = _p256_add(P, P) + e >>= 1 + return Q + + +def _der_sig(sig: bytes): + if len(sig) < 8 or sig[0] != 0x30 or sig[2] != 0x02: + raise ValueError("not a DER ECDSA signature") + rl = sig[3] + r = int.from_bytes(sig[4:4 + rl], "big") + if sig[4 + rl] != 0x02: + raise ValueError("not a DER ECDSA signature") + sl = sig[5 + rl] + s = int.from_bytes(sig[6 + rl:6 + rl + sl], "big") + return r, s + + +def p256_verify(pub_uncompressed: bytes, msg: bytes, der_sig: bytes) -> bool: + if len(pub_uncompressed) != 65 or pub_uncompressed[0] != 4: + return False + Qx = int.from_bytes(pub_uncompressed[1:33], "big") + Qy = int.from_bytes(pub_uncompressed[33:65], "big") + if (Qy * Qy - (Qx ** 3 - 3 * Qx + _b)) % _p != 0: + return False + try: + r, s = _der_sig(der_sig) + except ValueError: + return False + if not (1 <= r < _n and 1 <= s < _n): + return False + e = int.from_bytes(hashlib.sha256(msg).digest(), "big") + w = pow(s, _n - 2, _n) + u1 = (e * w) % _n + u2 = (r * w) % _n + X = _p256_add(_p256_mul((_Gx, _Gy), u1), _p256_mul((Qx, Qy), u2)) + return X is not None and X[0] % _n == r + +# ------------------------------------------------------------------------------------------------- anchor proof + + +def _varint(b: bytes, i: int): + v = b[i] + if v < 0xfd: + return v, i + 1 + if v == 0xfd: + return int.from_bytes(b[i + 1:i + 3], "little"), i + 3 + if v == 0xfe: + return int.from_bytes(b[i + 1:i + 5], "little"), i + 5 + return int.from_bytes(b[i + 1:i + 9], "little"), i + 9 + + +def tx_op_return_payloads(tx: bytes): + """Every OP_RETURN payload in a legacy (non-witness) transaction.""" + i = 4 + n_in, i = _varint(tx, i) + for _ in range(n_in): + i += 36 + sl, i = _varint(tx, i) + i += sl + 4 + n_out, i = _varint(tx, i) + out = [] + for _ in range(n_out): + i += 8 + sl, i = _varint(tx, i) + script = tx[i:i + sl] + i += sl + if script and script[0] == 0x6a: + j = 1 + if j < len(script): + op = script[j] + if op <= 0x4b: + out.append(script[j + 1:j + 1 + op]) + elif op == 0x4c: + out.append(script[j + 2:j + 2 + script[j + 1]]) + elif op == 0x4d: + ln = int.from_bytes(script[j + 1:j + 3], "little") + out.append(script[j + 3:j + 3 + ln]) + return out + + +def merkleblock_check(mb: bytes, header_len: int, txid_hex: str): + """Parse a Bitcoin-style merkleblock; return (merkle_root_hex, txid_included, computed_root_matches_header).""" + header = mb[:header_len] + root_in_header = header[36:68][::-1].hex() + i = header_len + ntx = int.from_bytes(mb[i:i + 4], "little") + i += 4 + nh, i = _varint(mb, i) + hashes = [mb[i + 32 * k:i + 32 * (k + 1)] for k in range(nh)] + i += 32 * nh + nf, i = _varint(mb, i) + flags = mb[i:i + nf] + bits = [] + for byte in flags: + for k in range(8): + bits.append((byte >> k) & 1) + height = 0 + while (1 << height) < ntx: + height += 1 + state = {"h": 0, "b": 0, "matched": []} + + def width(h): + return (ntx + (1 << h) - 1) >> h + + def walk(h, pos): + flag = bits[state["b"]] + state["b"] += 1 + if h == 0 or flag == 0: + hsh = hashes[state["h"]] + state["h"] += 1 + if h == 0 and flag: + state["matched"].append(hsh) + return hsh + left = walk(h - 1, pos * 2) + right = walk(h - 1, pos * 2 + 1) if pos * 2 + 1 < width(h - 1) else left + return sha256d(left + right) + + computed = walk(height, 0)[::-1].hex() + included = any(m[::-1].hex() == txid_hex for m in state["matched"]) + return root_in_header, included, computed == root_in_header + +# --------------------------------------------------------------------------------------------------------- main + + +def file_sha256(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +# ------------------------------------------------------------------------------------------ OpenTimestamps (6 Oct 2026) +# A second, independent anchor: the seal hash was submitted to an OpenTimestamps calendar, which commits it to Bitcoin. +# Wire format of python-opentimestamps (core/op.py, notary.py, timestamp.py), dependency-free here: +# timestamp := (0xff op timestamp)* (0x00 attestation | op timestamp); op := tag [varbytes arg] +# 0xf0 append · 0xf1 prepend · 0xf2 reverse · 0xf3 hexlify · 0x02 sha1 · 0x03 ripemd160 · 0x08 sha256 +# attestation := 8-byte tag, varbytes payload; 83dfe30d2ef90c8e pending (varbytes uri) · 0588960d73d71901 bitcoin (varuint height) +# The message at a Bitcoin attestation is the block's Merkle root in header byte order: the one fact left to the reader. +OTS_OPS = {0xF0: "append", 0xF1: "prepend", 0xF2: "reverse", 0xF3: "hexlify", 0x02: "sha1", 0x03: "ripemd160", 0x08: "sha256"} +OTS_PENDING = bytes.fromhex("83dfe30d2ef90c8e") +OTS_BITCOIN = bytes.fromhex("0588960d73d71901") + + +class _OtsReader: + def __init__(self, b: bytes): + self.b, self.i = b, 0 + + def byte(self) -> int: + if self.i >= len(self.b): + raise ValueError("ots: truncated") + v = self.b[self.i]; self.i += 1 + return v + + def take(self, n: int) -> bytes: + if self.i + n > len(self.b): + raise ValueError("ots: truncated") + v = self.b[self.i:self.i + n]; self.i += n + return v + + def varuint(self) -> int: + v, shift = 0, 0 + while True: + c = self.byte(); v |= (c & 0x7F) << shift + if not c & 0x80: + return v + shift += 7 + if shift > 56: + raise ValueError("ots: varuint too long") + + def varbytes(self, mx: int = 4096) -> bytes: + n = self.varuint() + if n > mx: + raise ValueError("ots: varbytes too long") + return self.take(n) + + +def _ots_apply(op: str, arg, msg: bytes) -> bytes: + if op == "append": + return msg + arg + if op == "prepend": + return arg + msg + if op == "reverse": + return msg[::-1] + if op == "hexlify": + return msg.hex().encode("ascii") + if op == "sha1": + return hashlib.sha1(msg).digest() + if op == "ripemd160": + return hashlib.new("ripemd160", msg).digest() + if op == "sha256": + return hashlib.sha256(msg).digest() + raise ValueError(f"ots: unsupported op {op}") + + +def ots_attestations(proof: bytes, digest: bytes) -> list: + """Replay a calendar proof from the digest: [(kind, value, msg)] with kind 'pending' (value = uri) or 'bitcoin' (value = height).""" + r = _OtsReader(proof); out = [] + + def timestamp(msg: bytes, depth: int): + if depth > 256: + raise ValueError("ots: too deep") + tag = r.byte() + while tag == 0xFF: + node(msg, r.byte(), depth); tag = r.byte() + node(msg, tag, depth) + + def node(msg: bytes, tag: int, depth: int): + if tag == 0x00: + att = r.take(8); payload = r.varbytes(8192) + if att == OTS_PENDING: + out.append(("pending", _OtsReader(payload).varbytes(1000).decode("utf-8", "replace"), msg)) + elif att == OTS_BITCOIN: + out.append(("bitcoin", _OtsReader(payload).varuint(), msg)) + else: + out.append(("unknown", att.hex(), msg)) + return + op = OTS_OPS.get(tag) + if not op: + raise ValueError(f"ots: unknown op tag 0x{tag:02x}") + arg = r.varbytes() if op in ("append", "prepend") else None + timestamp(_ots_apply(op, arg, msg), depth + 1) + + timestamp(digest, 0) + if r.i != len(proof): + raise ValueError("ots: trailing bytes") + return out + + +def main(path: str, artifact_path: str = None) -> int: + try: + with open(path, "r", encoding="utf-8") as f: + b = json.load(f) + except Exception as exc: # noqa: BLE001 + print(f"could not read bundle: {exc}") + return 2 + if b.get("format") not in ("xchk-receipt-bundle/1", "xchk-receipt-bundle/2"): + print(f"not an xChk receipt bundle (format={b.get('format')!r})") + return 2 + + failed = 0 + pending = 0 + + def ok(cond, text): + nonlocal failed + print((" OK " if cond else " FAIL ") + text) + if not cond: + failed += 1 + + rec = b.get("record") or {} + print(f"xChk receipt {b.get('recordHash')} ({b.get('format')})") + print(f" {b.get('issuer', {}).get('headline', '')}") + + # 1. record hash + mine = sha256_hex(canonical(rec)) + ok(mine == b.get("recordHash"), f"record hash recomputed: {mine}") + print(f" .. {'chained to previous record ' + rec['prevHash'] if rec.get('prevHash') else 'first record on its chain'}") + + # 1b. the file, when one was given: its bytes against the digest sealed in the record + if artifact_path: + want = rec.get("artifactHash") + if not want: + ok(False, "this record carries no artifactHash: it is not the outcome record of a file write") + else: + try: + got = file_sha256(artifact_path) + ok(got == want, f"file {artifact_path} is the one this action wrote: sha256 {got}" if got == want + else f"file {artifact_path} is NOT the one this action wrote: sha256 {got}, record says {want}") + except OSError as exc: + ok(False, f"could not read {artifact_path}: {exc}") + if rec.get("outcomeOf"): + print(f" .. written under decision {rec['outcomeOf']} by {rec.get('plate')}, run {rec.get('runRef') or rec.get('runSeq')}") + + # 2. merkle + seal = b.get("seal") + merkle = b.get("merkle") + if seal and merkle and isinstance(merkle.get("proof"), list): + cur = b["recordHash"] + for step in merkle["proof"]: + cur = sha256_hex(step["hash"] + cur) if step.get("side") == "L" else sha256_hex(cur + step["hash"]) + ok(cur == seal.get("merkleRoot"), f"Merkle path ({len(merkle['proof'])} steps) reaches seal root {seal.get('merkleRoot')}") + else: + pending += 1 + print(" WAIT not sealed when the bundle was made — download it again later") + + # 3. seal hash + if seal: + fields = {k: seal[k] for k in SEAL_KEYS if k in seal and not (k in ("rvnBlock", "profileHash") and seal[k] is None)} + sh = sha256_hex(canonical(fields)) + ok(sh == seal.get("sealHash"), f"seal #{seal.get('sealSeq')} hash recomputed: {sh}") + + # 3b. the node profile the seal names (6 Oct 2026): which rules produced this record, checkable here, offline + ph = seal.get("profileHash") + if ph: + prof = b.get("profile") or {} + doc = prof.get("document") + if not isinstance(doc, dict): + ok(False, f"seal names profile {ph[:12]}… but the bundle does not carry it ({prof.get('note') or 'missing'})") + else: + dh = sha256_hex(canonical(doc)) + ok(dh == ph, f"profile document hashes to the seal's profileHash: {dh}" if dh == ph + else f"profile document does NOT hash to the seal's profileHash: {dh}, seal says {ph}") + psig = prof.get("signature") or {} + pkeys = pinned_keys(b.get("registrySignature") or {}) + if (psig.get("signature") or psig.get("signatures")) and pkeys: + pending += registry_verify(ok, psig, ph.encode("utf-8"), pkeys, "registry signature over the profile hash") + else: + ok(False, "profile carries no registry signature") + print(f" .. sealed under profile \"{doc.get('name')}\" of {doc.get('registry')} ({ph[:12]}…): " + + ", ".join(f"{k}={json.dumps(v, separators=(',', ':'))}" for k, v in sorted((doc.get("dials") or {}).items()))) + else: + print(" .. sealed before profiles (no profileHash in the seal)") + + # 4. registry signature(s) — Ed25519, or hybrid Ed25519 + ML-DSA-65 + rs = b.get("registrySignature") or {} + keys = pinned_keys(rs) + if seal and (rs.get("signature") or rs.get("signatures")) and keys: + pending += registry_verify(ok, rs, seal["sealHash"].encode("utf-8"), keys, "registry signature over the seal hash") + print(f" .. keys pinned in the bundle: {', '.join(f'{k.get('alg')}/{k.get('keyId')}' for k in keys)}; confirm at {rs.get('wellKnown')} when online") + elif seal: + pending += 1 + print(f" WAIT no registry signature in this bundle ({rs.get('note') or 'unsigned'})") + + # 4b. root key statement (SLH-DSA-SHA2-128s) + root = rs.get("root") + if isinstance(root, dict) and root.get("statement") and root.get("signature"): + stated = root["statement"].get("keys") or [] + listed = all(any(x.get("alg") == k.get("alg") and x.get("keyId") == k.get("keyId") and x.get("publicKeyRaw") == k.get("publicKeyRaw") for x in stated) for k in keys) + ok(listed, "every key pinned in this bundle is listed in the root's key statement") + rid = key_id_for(root.get("alg", ""), b64d(root.get("publicKeyRaw", ""))) + pending += 1 + print(f" WAIT root {root.get('alg')} signature over canonical(statement) — pin root keyId {rid} and verify with any FIPS 205 implementation (not shipped in this script)") + + # 5. approver + ap = b.get("approver") + if ap: + a = ap.get("assertion") or {} + pk = ap.get("publicKey") or {} + if a.get("signature") and a.get("authenticatorData") and a.get("clientDataJSON") and pk.get("format") == "ec-p256-uncompressed": + auth = b64d(a["authenticatorData"]) + cdj = b64d(a["clientDataJSON"]) + try: + cd = json.loads(cdj.decode("utf-8")) + except Exception: # noqa: BLE001 + cd = {} + uv = len(auth) > 32 and bool(auth[32] & 0x04) + sig_ok = p256_verify(b64d(pk["raw"]), auth + hashlib.sha256(cdj).digest(), b64d(a["signature"])) + ok(sig_ok, f"passkey assertion signature verifies with the enrolled credential {str(ap.get('credentialId'))[:16]}…") + ok(cd.get("type") == "webauthn.get", f"assertion is a WebAuthn 'get' ceremony (type={cd.get('type')!r})") + ok(uv, "authenticator reports USER VERIFIED (biometric/PIN) for that ceremony") + else: + pending += 1 + print(" WAIT approver assertion or an ES256 public key not present in this bundle — the ceremony cannot be re-run here") + bd = ap.get("binding") or {} + if (bd.get("signature") or bd.get("signatures")) and keys and bd.get("fields"): + f = bd["fields"] + pending += registry_verify(ok, bd, canonical(f).encode("utf-8"), keys, f"identity binding: credential ↔ {f.get('approverEmail')} (level {f.get('authLevel')})") + ok(f.get("recordHash") == b.get("recordHash"), "binding names this record hash") + ok(f.get("credentialId") == ap.get("credentialId"), "binding names the credential that signed the assertion") + # 17 Oct 2026: the key the assertion was checked with must be the key the registry bound to that credential, + # or an attacker who swaps both the key and the assertion for their own passes check 5 with every registry + # signature intact (TRACE fixture 06-assertion-from-another-credential). + ok(f.get("publicKey") == pk.get("raw"), "binding names the public key the assertion was checked with") + else: + pending += 1 + print(" WAIT no signed identity binding (registry signing not configured when the bundle was made)") + # 18 Oct 2026 (AB-1): what the device signed over. With rule ask-v1 the challenge is sha256 over the ask as + # issued (attestation id, the prompt the person saw, the digest of the action's parameters), so the assertion + # itself names the act it answered; recompute it from the bundle's own decision block, never from `expected`. + # Without it the challenge was a nonce and only the registry's binding pairs the assertion with the record: + # an assertion by the same credential over another ask passes every other check. + ch = ap.get("challenge") or {} + if ch.get("rule") == "ask-v1" and isinstance(ch.get("fields"), dict) and a.get("clientDataJSON"): + f = ch["fields"] + dec = b.get("decision") or {} + try: + cd = json.loads(b64d(a["clientDataJSON"]).decode("utf-8")) + except Exception: # noqa: BLE001 + cd = {} + recomputed = base64.urlsafe_b64encode(hashlib.sha256(canonical(f).encode("utf-8")).digest()).decode("ascii").rstrip("=") + ok(cd.get("challenge") == recomputed, "the challenge the device signed is sha256 over the ask (rule ask-v1)") + ok(f.get("attestationId") == ap.get("attestationId"), "the signed ask names this ceremony") + ok(f.get("prompt") == dec.get("prompt"), "the signed ask carries the prompt the person saw") + ok(f.get("actionParamsHash") == dec.get("actionParamsHash"), "the signed ask carries the digest of the action's parameters") + else: + pending += 1 + print(" WAIT challenge not derived from the ask (ceremony before 18 Oct 2026): the binding alone pairs this assertion with the record") + + # 6. anchor + anchor = b.get("anchor") or {} + proof = anchor.get("proof") + if proof and proof.get("txHex") and seal: + tx = bytes.fromhex(proof["txHex"]) + payloads = tx_op_return_payloads(tx) + carries = any(p[:3] == b"xCk" and p[9:41].hex() == seal["sealHash"] for p in payloads) + ok(carries, "the raw anchor transaction's OP_RETURN carries this seal hash") + ok(sha256d(tx)[::-1].hex() == proof.get("txid"), f"sha256d(transaction) = txid {proof.get('txid')}") + hdr = bytes.fromhex(proof["headerHex"]) if proof.get("headerHex") else b"" + mb = bytes.fromhex(proof["merkleBlockHex"]) if proof.get("merkleBlockHex") else b"" + if hdr and mb: + root_hdr, included, root_ok = merkleblock_check(mb, len(hdr), proof["txid"]) + ok(mb[:len(hdr)] == hdr, "merkleblock header is the block header shipped with the bundle") + ok(root_ok and included, f"partial Merkle tree reproduces the header's Merkle root {root_hdr} and includes the txid") + print(f" WAIT one outside fact left: Ravencoin block {proof.get('blockHash')} (height {proof.get('blockHeight')}) has Merkle root {root_hdr}. Any explorer confirms it; the block hash itself is a KAWPOW hash this script does not compute.") + else: + pending += 1 + print(" WAIT anchor proof lacks header/merkleblock") + elif anchor.get("rvnTxId"): + pending += 1 + print(f" WAIT anchor: Ravencoin tx {anchor['rvnTxId']} — raw bytes not yet captured in this bundle (download again after a confirmation), or confirm its OP_RETURN on an explorer:") + print(f" {anchor.get('explorer') or 'https://ravencoin.network/tx/' + anchor['rvnTxId']}") + elif seal: + pending += 1 + print(" WAIT seal not yet anchored when the bundle was made") + + # 3c. legal holds (7 Oct 2026): existence and scope only; the reason and the names are not on a public receipt + for h in (b.get("legalHolds") or []): + print(f" .. under a legal hold on the {h.get('scope')} since {h.get('placedAt')}" + + (f", until {h.get('until')}" if h.get("until") else ", until released") + f" (hold {str(h.get('id'))[:8]}…)") + + # 6b. OpenTimestamps (6 Oct 2026): the second anchor, replayed from the seal hash to its attestations + if seal and anchor.get("otsProof"): + try: + atts = ots_attestations(base64.b64decode(anchor["otsProof"]), bytes.fromhex(seal["sealHash"])) + btc = [a for a in atts if a[0] == "bitcoin"] + pend = [a for a in atts if a[0] == "pending"] + if btc: + for _, height, root in btc: + ok(len(root) == 32, f"OpenTimestamps proof replays from the seal hash to a 32-byte Merkle root at Bitcoin block {height}") + pending += 1 + print(f" WAIT one outside fact left: Bitcoin block {height} has Merkle root {root[::-1].hex()}. Any Bitcoin explorer confirms it.") + elif pend: + pending += 1 + print(f" WAIT OpenTimestamps: the seal hash is committed at {', '.join(sorted({a[1] for a in pend}))}, not yet in a Bitcoin block when the bundle was made — download again later") + else: + ok(False, "OpenTimestamps proof carries no attestation this script knows") + except Exception as exc: # noqa: BLE001 — a proof that does not replay is a failed check, not a crash + ok(False, f"OpenTimestamps proof does not replay: {exc}") + + if failed: + print(f"\nRESULT: does NOT verify ({failed} check(s) failed)") + return 1 + print("\nRESULT: verifies offline" + (f" — {pending} step(s) pending (see WAIT)" if pending else "") + "; only the block's presence on the chain is left to you") + return 0 + + +if __name__ == "__main__": + if len(sys.argv) not in (2, 3): + print(__doc__) + sys.exit(2) + sys.exit(main(*sys.argv[1:])) diff --git a/integrations/xchk-registry/integration.yaml b/integrations/xchk-registry/integration.yaml new file mode 100644 index 00000000..e5bc0d19 --- /dev/null +++ b/integrations/xchk-registry/integration.yaml @@ -0,0 +1,30 @@ +name: xChk registry +vendor: xChk +integrates_with: + - trace +description: >- + A Trust Record per run from an agent-licensing registry, each approval-outcome + reference resolving to a sealed receipt that verifies offline down to the + approver's passkey assertion over the ask. +maintainer: + github: minitru + email: sean@maclawran.ca +repository: https://github.com/agentrust-io/integrations +homepage: https://in.xchk.io +license: Apache-2.0 +tier: community +marketplace: + category: Evidence & receipts + mark: XC + keywords: [approval-outcome, receipts, passkey, human-of-record, offline-verify] +# Role. The registry mints and signs the Trust Record (record-producer, software-only, Level 0). +# The receipt a reference resolves to is the registry's own sealed evidence, in its own +# format, verified by its own standard-library script: external-evidence-source. The code +# here is the relying party's side: it resolves, recomputes and reports; it issues nothing. +trace_roles: + - record-producer + - external-evidence-source +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.11.0" + agentrust-trace-tests: "0.6.2" diff --git a/integrations/xchk-registry/pyproject.toml b/integrations/xchk-registry/pyproject.toml new file mode 100644 index 00000000..df8f6ede --- /dev/null +++ b/integrations/xchk-registry/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "xchk-registry-integration" +version = "0.1.0" +description = "Resolve a TRACE approval-outcome reference to an xChk sealed receipt and recompute the relying party's verdict" +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +dependencies = [ + "agentrust-trace>=0.11.0", + "rfc8785", +] + +[project.optional-dependencies] +test = ["pytest"] + +[tool.setuptools] +py-modules = ["xchk_trace"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/integrations/xchk-registry/tests/test_xchk_registry.py b/integrations/xchk-registry/tests/test_xchk_registry.py new file mode 100644 index 00000000..7dce9463 --- /dev/null +++ b/integrations/xchk-registry/tests/test_xchk_registry.py @@ -0,0 +1,79 @@ +"""Recomputed from the committed bytes: the Trust Record with released agentrust-trace, the digest with rfc8785, +the bundle with the registry's own verifier. Nothing is read from a generator's summary. The live resolution test +runs only with REGISTRY_LIVE=1, because CI must not depend on a third party's uptime.""" +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import xchk_trace # noqa: E402 + +EX = Path(__file__).resolve().parents[1] / "examples" +EXPECTED = json.loads((EX / "expected.json").read_text(encoding="utf-8")) +RECORD = json.loads((EX / "01-approval-confirmed.json").read_text(encoding="utf-8")) +BUNDLE = json.loads((EX / "01-approval-confirmed.bundle.json").read_text(encoding="utf-8")) +CROSSED = json.loads((EX / "08-assertion-over-another-ask.bundle.json").read_text(encoding="utf-8")) + + +def reference() -> dict: + refs = [r for r in RECORD["references"] if r["rel"] == "approval-outcome"] + assert len(refs) == 1 + return refs[0] + + +def test_the_record_verifies_with_the_registry_key_and_with_its_own_cnf(): + assert xchk_trace.record_verifies(RECORD, EXPECTED["trace_signer_jwk"]) + assert xchk_trace.record_verifies(RECORD, None) + assert RECORD["cnf"]["jwk"]["kid"] == EXPECTED["trace_signer_jwk"]["kid"] + + +def test_the_reference_digest_is_the_registry_record_hash(): + ref = reference() + assert ref["resolver"] == EXPECTED["resolver"] + assert xchk_trace.jcs_sha256(BUNDLE["record"]) == ref["digest"] == "sha256:" + BUNDLE["recordHash"] + + +def test_a_confirmed_approval_names_the_person_and_the_ask_they_signed_over(): + got = xchk_trace.assess(reference(), BUNDLE) + assert got["verdict"] == "approval-confirmed" + assert got["bundle_verifies"] and got["digest_matches"] + assert got["approver"] == BUNDLE["approver"]["email"] + assert got["challenge_rule"] == "ask-v1" + + +def test_the_same_persons_assertion_over_another_ask_is_contradicted(): + """Key, binding and user verification are all genuine; the challenge the device signed names another ask.""" + assert CROSSED["approver"]["publicKey"] == BUNDLE["approver"]["publicKey"] + assert CROSSED["approver"]["assertion"] != BUNDLE["approver"]["assertion"] + got = xchk_trace.assess(reference(), CROSSED) + assert got["verdict"] == "approval-contradicted" + assert got["digest_matches"] is True and got["bundle_verifies"] is False + + +def test_an_unresolvable_reference_is_unconfirmed_not_rejected(): + got = xchk_trace.assess(reference(), None) + assert got["verdict"] == "approval-unconfirmed" + assert xchk_trace.record_verifies(RECORD, EXPECTED["trace_signer_jwk"]) + + +def test_the_cli_runs_offline(tmp_path, capsys): + jwk = tmp_path / "jwk.json" + jwk.write_text(json.dumps(EXPECTED), encoding="utf-8") + rc = xchk_trace.main([str(EX / "01-approval-confirmed.json"), "--bundle", str(EX / "01-approval-confirmed.bundle.json"), "--jwk", str(jwk)]) + assert rc == 0 + out = json.loads(capsys.readouterr().out) + assert out["references"][0]["verdict"] == "approval-confirmed" + + +@pytest.mark.skipif(os.environ.get("REGISTRY_LIVE") != "1", reason="set REGISTRY_LIVE=1 to resolve the reference at the registry") +def test_live_resolution_matches_the_committed_bundle(): + ref = reference() + live = xchk_trace.fetch_bundle(ref["resolver"], ref["id"]) + assert live is not None + assert live["record"] == BUNDLE["record"] + assert xchk_trace.assess(ref, live)["verdict"] == "approval-confirmed" diff --git a/integrations/xchk-registry/xchk_trace.py b/integrations/xchk-registry/xchk_trace.py new file mode 100644 index 00000000..d0584a5c --- /dev/null +++ b/integrations/xchk-registry/xchk_trace.py @@ -0,0 +1,120 @@ +"""Resolve a TRACE Trust Record's approval-outcome references against an xChk registry and report what a +relying party may conclude about each. Apache-2.0. + + python xchk_trace.py record.json # resolves each reference at its resolver + python xchk_trace.py record.json --bundle b.json # offline: use this bundle for the one reference + python xchk_trace.py record.json --jwk expected.json # verify the record with a pinned signer key + +The Trust Record is verified with agentrust-trace. Each `approval-outcome` reference is resolved at +`/api/receipt//bundle`, the bundle's record is canonicalized with RFC 8785 and its digest +compared with the reference, and the bundle is handed to the registry's own offline verifier, +`examples/verify_receipt.py`, as a subprocess. The verdict vocabulary is the one in trace-spec's +`docs/crosswalks/xchk-sealed-receipts.md`: + + approval-confirmed the bundle verifies and the record's disposition is `approved` + approval-confirmed-as-amended as above, with `decision.amendment` present: the approval covers that command + not-an-approval the bundle verifies; the person declined, or nobody answered in time + approval-contradicted the bundle fails a check, or its record is not the one the reference names + approval-unconfirmed the reference does not resolve (the record still stands: spec 3.1.2 rule 3) +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +import tempfile +import urllib.request +from pathlib import Path + +import rfc8785 +from agentrust_trace import verify_record +from cryptography.exceptions import InvalidSignature + +VERIFIER = Path(__file__).resolve().parent / "examples" / "verify_receipt.py" + + +def jcs_sha256(obj: object) -> str: + return "sha256:" + hashlib.sha256(rfc8785.dumps(obj)).hexdigest() + + +def record_verifies(record: dict, jwk: dict | None) -> bool: + key = jwk or record.get("cnf", {}).get("jwk") + if not key: + return False + try: + verify_record(record, key, max_age_seconds=None) + except (InvalidSignature, ValueError): + return False + return True + + +def fetch_bundle(resolver: str, ref_id: str, timeout: float = 20.0) -> dict | None: + url = f"{resolver.rstrip('/')}/api/receipt/{ref_id}/bundle" + try: + with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 (the resolver is the record's own claim) + return json.load(r) + except Exception: # noqa: BLE001 + return None + + +def bundle_verifies(bundle: dict, verifier: Path = VERIFIER) -> bool: + """Exit 0 from the registry's verifier is every offline check passed (pending steps allowed).""" + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f: + json.dump(bundle, f) + tmp = Path(f.name) + try: + proc = subprocess.run([sys.executable, "-I", str(verifier), str(tmp)], capture_output=True, text=True, check=False, timeout=120) + finally: + tmp.unlink(missing_ok=True) + return proc.returncode == 0 + + +def assess(reference: dict, bundle: dict | None) -> dict: + """What a relying party concludes about one approval-outcome reference.""" + if bundle is None: + return {"reference": reference["id"], "resolves": False, "digest_matches": None, "bundle_verifies": None, + "disposition": None, "verdict": "approval-unconfirmed"} + digest_matches = jcs_sha256(bundle["record"]) == reference["digest"] + ok = bundle_verifies(bundle) + disposition = bundle["record"].get("disposition") + amended = bool(((bundle.get("decision") or {}).get("amendment") or {}).get("command")) + if not (digest_matches and ok): + verdict = "approval-contradicted" + elif disposition != "approved": + verdict = "not-an-approval" + elif amended: + verdict = "approval-confirmed-as-amended" + else: + verdict = "approval-confirmed" + approver = bundle.get("approver") or {} + return {"reference": reference["id"], "resolves": True, "digest_matches": digest_matches, "bundle_verifies": ok, + "disposition": disposition, "verdict": verdict, + "approver": approver.get("email"), "auth_level": approver.get("authLevel"), + "challenge_rule": (approver.get("challenge") or {}).get("rule")} + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Resolve a Trust Record's approval-outcome references against an xChk registry.") + ap.add_argument("record") + ap.add_argument("--bundle", help="offline: a saved bundle for the record's single approval-outcome reference") + ap.add_argument("--jwk", help="a JSON file with trace_signer_jwk (or a bare JWK) to verify the record with") + a = ap.parse_args(argv) + record = json.loads(Path(a.record).read_text(encoding="utf-8")) + jwk = None + if a.jwk: + j = json.loads(Path(a.jwk).read_text(encoding="utf-8")) + jwk = j.get("trace_signer_jwk", j) + out = {"trace_record_verifies": record_verifies(record, jwk), "references": []} + refs = [r for r in record.get("references", []) if r.get("rel") == "approval-outcome"] + local = json.loads(Path(a.bundle).read_text(encoding="utf-8")) if a.bundle else None + for ref in refs: + bundle = local if local is not None else fetch_bundle(ref["resolver"], ref["id"]) + out["references"].append(assess(ref, bundle)) + print(json.dumps(out, indent=2)) + return 0 if out["trace_record_verifies"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/marketplace/catalog.json b/marketplace/catalog.json index 07de0e09..d7067c85 100644 --- a/marketplace/catalog.json +++ b/marketplace/catalog.json @@ -1,6 +1,6 @@ { "catalog_version": 1, - "count": 47, + "count": 48, "integrations": [ { "name": "Claude Code", @@ -1075,6 +1075,30 @@ "key-release", "evidence" ] + }, + { + "name": "xChk registry", + "package_name": "xChk registry", + "vendor": "xChk", + "description": "A Trust Record per run from an agent-licensing registry, each approval-outcome reference resolving to a sealed receipt that verifies offline down to the approver's passkey assertion over the ask.", + "path": "integrations/xchk-registry", + "url": "https://github.com/agentrust-io/integrations/tree/main/integrations/xchk-registry", + "homepage": "https://in.xchk.io", + "repository": "https://github.com/agentrust-io/integrations", + "tier": "community", + "stack": [ + "TRACE" + ], + "category": "Evidence & receipts", + "mark": "XC", + "featured": null, + "keywords": [ + "approval-outcome", + "receipts", + "passkey", + "human-of-record", + "offline-verify" + ] } ] } From d3e96f40bddf26771b5982bce6f19162c1f0a977 Mon Sep 17 00:00:00 2001 From: minitru Date: Sat, 17 Oct 2026 19:09:54 -0400 Subject: [PATCH 2/4] xchk-registry: verify the Trust Record against a pinned or published key, never its own The adapter fell back to the key embedded in the record when none was pinned, and a test asserted that passed. A record verified against its own cnf.jwk is self-consistent, not verified: any self-signed record passes that, which the spec names as the operator-forgeable case. Now the key comes from --jwk, or from the record's resolver's published keys under the record's kid; the embedded key is used only with --allow-embedded-key and reported as self-consistent with a non-zero exit. Tests cover all three sources and a tampered record; the live test also checks the published key document. Co-Authored-By: Claude Fable 5.1 Signed-off-by: minitru --- integrations/xchk-registry/README.md | 4 +- .../xchk-registry/tests/test_xchk_registry.py | 35 +++++++++- integrations/xchk-registry/xchk_trace.py | 68 ++++++++++++++++--- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/integrations/xchk-registry/README.md b/integrations/xchk-registry/README.md index 7d531fe2..87d195b4 100644 --- a/integrations/xchk-registry/README.md +++ b/integrations/xchk-registry/README.md @@ -21,11 +21,11 @@ recomputes what may be concluded about the approval, offline, with the registry' ## What this code does ``` -python xchk_trace.py record.json # resolve each reference at its resolver +python xchk_trace.py record.json # key from the resolver's published keys; references resolved there python xchk_trace.py record.json --bundle bundle.json --jwk examples/expected.json # offline ``` -1. Verifies the Trust Record with released `agentrust-trace` (the record's own `cnf.jwk`, or a pinned key). +1. Verifies the Trust Record with released `agentrust-trace` against a key the relying party trusts: one pinned with `--jwk`, or the key the record's resolver publishes at `/.well-known/xchk-keys.json` under the record's `kid`. The record's own `cnf.jwk` is used only with `--allow-embedded-key` and reported as *self-consistent*, never as verified, because a self-signed record always passes against its own key. 2. For each `approval-outcome` reference, fetches `/api/receipt//bundle`, or uses a saved one. 3. Canonicalizes `bundle.record` with RFC 8785 and compares the digest with the reference. 4. Runs `examples/verify_receipt.py` on the bundle as a subprocess: record hash, Merkle path, seal hash, registry diff --git a/integrations/xchk-registry/tests/test_xchk_registry.py b/integrations/xchk-registry/tests/test_xchk_registry.py index 7dce9463..48d75ad9 100644 --- a/integrations/xchk-registry/tests/test_xchk_registry.py +++ b/integrations/xchk-registry/tests/test_xchk_registry.py @@ -26,10 +26,35 @@ def reference() -> dict: return refs[0] -def test_the_record_verifies_with_the_registry_key_and_with_its_own_cnf(): +def test_the_record_verifies_with_the_pinned_registry_key(): assert xchk_trace.record_verifies(RECORD, EXPECTED["trace_signer_jwk"]) - assert xchk_trace.record_verifies(RECORD, None) assert RECORD["cnf"]["jwk"]["kid"] == EXPECTED["trace_signer_jwk"]["kid"] + tampered = json.loads(json.dumps(RECORD)) + tampered["iat"] += 1 + assert not xchk_trace.record_verifies(tampered, EXPECTED["trace_signer_jwk"]) + + +def test_the_embedded_key_is_never_reported_as_verified(): + """A record checked against its own cnf.jwk is self-consistent, not verified: any self-signed record passes that.""" + no_fetch = lambda resolver, kid: None # noqa: E731 + assert xchk_trace.verify_with_key_source(RECORD, None, False, fetch=no_fetch)["status"] == "no-key" + got = xchk_trace.verify_with_key_source(RECORD, None, True, fetch=no_fetch) + assert got["status"] == "self-consistent" and got["key_source"] == "embedded" + pinned = xchk_trace.verify_with_key_source(RECORD, EXPECTED["trace_signer_jwk"], False, fetch=no_fetch) + assert pinned["status"] == "verified" and pinned["key_source"] == "pinned" + + +def test_the_published_key_document_converts_to_the_pinned_jwk(): + import base64 + jwk = EXPECTED["trace_signer_jwk"] + raw = base64.urlsafe_b64decode(jwk["x"] + "=" * (-len(jwk["x"]) % 4)) + doc = {"keys": [{"keyId": jwk["kid"], "alg": "ed25519", "publicKeyRaw": base64.b64encode(raw).decode("ascii")}, + {"keyId": "other", "alg": "ml-dsa-65", "publicKeyRaw": "AAAA"}]} + got = xchk_trace.key_from_published(doc, jwk["kid"]) + assert got == {"kty": "OKP", "crv": "Ed25519", "x": jwk["x"], "kid": jwk["kid"]} + assert xchk_trace.key_from_published(doc, "other") is None + served = xchk_trace.verify_with_key_source(RECORD, None, False, fetch=lambda resolver, kid: xchk_trace.key_from_published(doc, kid)) + assert served["status"] == "verified" and served["key_source"] == "published" and served["resolver"] == EXPECTED["resolver"] def test_the_reference_digest_is_the_registry_record_hash(): @@ -67,12 +92,18 @@ def test_the_cli_runs_offline(tmp_path, capsys): rc = xchk_trace.main([str(EX / "01-approval-confirmed.json"), "--bundle", str(EX / "01-approval-confirmed.bundle.json"), "--jwk", str(jwk)]) assert rc == 0 out = json.loads(capsys.readouterr().out) + assert out["trace_record"]["key_source"] == "pinned" assert out["references"][0]["verdict"] == "approval-confirmed" + rc = xchk_trace.main([str(EX / "01-approval-confirmed.json"), "--bundle", str(EX / "01-approval-confirmed.bundle.json"), "--allow-embedded-key"]) + out = json.loads(capsys.readouterr().out) + assert out["trace_record"]["status"] in ("self-consistent", "verified") # verified only if the resolver's keys were reachable + assert rc == (0 if out["trace_record"]["status"] == "verified" else 1) @pytest.mark.skipif(os.environ.get("REGISTRY_LIVE") != "1", reason="set REGISTRY_LIVE=1 to resolve the reference at the registry") def test_live_resolution_matches_the_committed_bundle(): ref = reference() + assert xchk_trace.published_key(ref["resolver"], RECORD["cnf"]["jwk"]["kid"]) == {**EXPECTED["trace_signer_jwk"], "kid": RECORD["cnf"]["jwk"]["kid"]} live = xchk_trace.fetch_bundle(ref["resolver"], ref["id"]) assert live is not None assert live["record"] == BUNDLE["record"] diff --git a/integrations/xchk-registry/xchk_trace.py b/integrations/xchk-registry/xchk_trace.py index d0584a5c..6ac967c8 100644 --- a/integrations/xchk-registry/xchk_trace.py +++ b/integrations/xchk-registry/xchk_trace.py @@ -1,9 +1,14 @@ """Resolve a TRACE Trust Record's approval-outcome references against an xChk registry and report what a relying party may conclude about each. Apache-2.0. - python xchk_trace.py record.json # resolves each reference at its resolver + python xchk_trace.py record.json # key from the resolver's published keys; references resolved there + python xchk_trace.py record.json --jwk expected.json # offline: a pinned signer key python xchk_trace.py record.json --bundle b.json # offline: use this bundle for the one reference - python xchk_trace.py record.json --jwk expected.json # verify the record with a pinned signer key + python xchk_trace.py record.json --allow-embedded-key # self-consistency only (exit 1): the record's own cnf.jwk + +The Trust Record is verified with agentrust-trace against a key the caller trusts: pinned, or the one the record's +resolver publishes under the record's kid. The record's own `cnf.jwk` is used only on request and reported as +self-consistent, never as verified: a self-signed record always passes against its own key. The Trust Record is verified with agentrust-trace. Each `approval-outcome` reference is resolved at `/api/receipt//bundle`, the bundle's record is canonicalized with RFC 8785 and its digest @@ -20,6 +25,7 @@ from __future__ import annotations import argparse +import base64 import hashlib import json import subprocess @@ -39,17 +45,58 @@ def jcs_sha256(obj: object) -> str: return "sha256:" + hashlib.sha256(rfc8785.dumps(obj)).hexdigest() -def record_verifies(record: dict, jwk: dict | None) -> bool: - key = jwk or record.get("cnf", {}).get("jwk") - if not key: - return False +def record_verifies(record: dict, jwk: dict) -> bool: + """True when the record's signature verifies with `jwk`. The caller chooses the key; this never reads one + out of the record, because a record verified against its own `cnf.jwk` is only self-consistent (any + self-signed record passes that), which the TRACE spec names as the operator-forgeable case.""" try: - verify_record(record, key, max_age_seconds=None) + verify_record(record, jwk, max_age_seconds=None) except (InvalidSignature, ValueError): return False return True +def published_key(resolver: str, kid: str, timeout: float = 20.0) -> dict | None: + """The registry's published Ed25519 key with this id, as a JWK, from `/.well-known/xchk-keys.json`; + None when the document is unreachable or names no such key.""" + url = f"{resolver.rstrip('/')}/.well-known/xchk-keys.json" + try: + with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 + doc = json.load(r) + except Exception: # noqa: BLE001 + return None + return key_from_published(doc, kid) + + +def key_from_published(doc: dict, kid: str) -> dict | None: + for k in doc.get("keys") or []: + if k.get("keyId") == kid and k.get("alg") == "ed25519" and k.get("publicKeyRaw"): + raw = base64.b64decode(k["publicKeyRaw"]) + if len(raw) != 32: + return None + return {"kty": "OKP", "crv": "Ed25519", "x": base64.urlsafe_b64encode(raw).decode("ascii").rstrip("="), "kid": kid} + return None + + +def verify_with_key_source(record: dict, pinned: dict | None, allow_embedded: bool, fetch=published_key) -> dict: + """Pick the key and say where it came from. Order: a pinned key; the key the record's resolver publishes under + the record's kid; the record's own cnf.jwk only when the caller asked for it, reported as self-consistent.""" + cnf = (record.get("cnf") or {}).get("jwk") or {} + kid = cnf.get("kid") + if pinned: + return {"status": "verified" if record_verifies(record, pinned) else "invalid", "key_source": "pinned", "kid": pinned.get("kid") or kid} + refs = [r for r in record.get("references", []) if r.get("resolver")] + resolver = refs[0]["resolver"] if refs else None + if resolver and kid: + key = fetch(resolver, kid) + if key: + return {"status": "verified" if record_verifies(record, key) else "invalid", "key_source": "published", "kid": kid, "resolver": resolver} + if allow_embedded and cnf: + return {"status": "self-consistent" if record_verifies(record, cnf) else "invalid", "key_source": "embedded", "kid": kid, + "note": "verified against the key the record carries: integrity only, not origin"} + return {"status": "no-key", "key_source": None, "kid": kid, "note": "pass --jwk, or let the record's resolver publish the key"} + + def fetch_bundle(resolver: str, ref_id: str, timeout: float = 20.0) -> dict | None: url = f"{resolver.rstrip('/')}/api/receipt/{ref_id}/bundle" try: @@ -99,14 +146,17 @@ def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description="Resolve a Trust Record's approval-outcome references against an xChk registry.") ap.add_argument("record") ap.add_argument("--bundle", help="offline: a saved bundle for the record's single approval-outcome reference") - ap.add_argument("--jwk", help="a JSON file with trace_signer_jwk (or a bare JWK) to verify the record with") + ap.add_argument("--jwk", help="a JSON file with trace_signer_jwk (or a bare JWK): the pinned key to verify the record with") + ap.add_argument("--allow-embedded-key", action="store_true", + help="with no pinned or published key, verify against the record's own cnf.jwk and report it as self-consistent (integrity only)") a = ap.parse_args(argv) record = json.loads(Path(a.record).read_text(encoding="utf-8")) jwk = None if a.jwk: j = json.loads(Path(a.jwk).read_text(encoding="utf-8")) jwk = j.get("trace_signer_jwk", j) - out = {"trace_record_verifies": record_verifies(record, jwk), "references": []} + signer = verify_with_key_source(record, jwk, a.allow_embedded_key) + out = {"trace_record": signer, "trace_record_verifies": signer["status"] == "verified", "references": []} refs = [r for r in record.get("references", []) if r.get("rel") == "approval-outcome"] local = json.loads(Path(a.bundle).read_text(encoding="utf-8")) if a.bundle else None for ref in refs: From 1fd27f08eb4873d7a81cf00666be7049228d25bf Mon Sep 17 00:00:00 2001 From: minitru Date: Sat, 17 Oct 2026 21:14:46 -0400 Subject: [PATCH 3/4] xchk-registry: re-vendor verify_receipt.py (runs on Python 3.11; ruff E702 clean) Signed-off-by: minitru --- .../xchk-registry/examples/verify_receipt.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/integrations/xchk-registry/examples/verify_receipt.py b/integrations/xchk-registry/examples/verify_receipt.py index 2342537b..c3cb7a68 100644 --- a/integrations/xchk-registry/examples/verify_receipt.py +++ b/integrations/xchk-registry/examples/verify_receipt.py @@ -396,19 +396,22 @@ def __init__(self, b: bytes): def byte(self) -> int: if self.i >= len(self.b): raise ValueError("ots: truncated") - v = self.b[self.i]; self.i += 1 + v = self.b[self.i] + self.i += 1 return v def take(self, n: int) -> bytes: if self.i + n > len(self.b): raise ValueError("ots: truncated") - v = self.b[self.i:self.i + n]; self.i += n + v = self.b[self.i:self.i + n] + self.i += n return v def varuint(self) -> int: v, shift = 0, 0 while True: - c = self.byte(); v |= (c & 0x7F) << shift + c = self.byte() + v |= (c & 0x7F) << shift if not c & 0x80: return v shift += 7 @@ -442,19 +445,22 @@ def _ots_apply(op: str, arg, msg: bytes) -> bytes: def ots_attestations(proof: bytes, digest: bytes) -> list: """Replay a calendar proof from the digest: [(kind, value, msg)] with kind 'pending' (value = uri) or 'bitcoin' (value = height).""" - r = _OtsReader(proof); out = [] + r = _OtsReader(proof) + out = [] def timestamp(msg: bytes, depth: int): if depth > 256: raise ValueError("ots: too deep") tag = r.byte() while tag == 0xFF: - node(msg, r.byte(), depth); tag = r.byte() + node(msg, r.byte(), depth) + tag = r.byte() node(msg, tag, depth) def node(msg: bytes, tag: int, depth: int): if tag == 0x00: - att = r.take(8); payload = r.varbytes(8192) + att = r.take(8) + payload = r.varbytes(8192) if att == OTS_PENDING: out.append(("pending", _OtsReader(payload).varbytes(1000).decode("utf-8", "replace"), msg)) elif att == OTS_BITCOIN: @@ -474,7 +480,7 @@ def node(msg: bytes, tag: int, depth: int): return out -def main(path: str, artifact_path: str = None) -> int: +def main(path: str, artifact_path: Optional[str] = None) -> int: try: with open(path, "r", encoding="utf-8") as f: b = json.load(f) @@ -563,7 +569,8 @@ def ok(cond, text): keys = pinned_keys(rs) if seal and (rs.get("signature") or rs.get("signatures")) and keys: pending += registry_verify(ok, rs, seal["sealHash"].encode("utf-8"), keys, "registry signature over the seal hash") - print(f" .. keys pinned in the bundle: {', '.join(f'{k.get('alg')}/{k.get('keyId')}' for k in keys)}; confirm at {rs.get('wellKnown')} when online") + pinned = ", ".join(f"{k.get('alg')}/{k.get('keyId')}" for k in keys) # 18 Oct 2026: no nested same-quote f-string (3.12-only syntax) + print(f" .. keys pinned in the bundle: {pinned}; confirm at {rs.get('wellKnown')} when online") elif seal: pending += 1 print(f" WAIT no registry signature in this bundle ({rs.get('note') or 'unsigned'})") From 854aa577f21e90ba15c9aa2a5a48d4740dcf9bf9 Mon Sep 17 00:00:00 2001 From: minitru Date: Sat, 17 Oct 2026 21:16:11 -0400 Subject: [PATCH 4/4] xchk-registry: the verifier's missing typing import; lint-clean under the repository's rule set Signed-off-by: minitru --- integrations/xchk-registry/examples/verify_receipt.py | 1 + integrations/xchk-registry/tests/test_xchk_registry.py | 5 +++-- integrations/xchk-registry/xchk_trace.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/integrations/xchk-registry/examples/verify_receipt.py b/integrations/xchk-registry/examples/verify_receipt.py index c3cb7a68..c9e0e61b 100644 --- a/integrations/xchk-registry/examples/verify_receipt.py +++ b/integrations/xchk-registry/examples/verify_receipt.py @@ -36,6 +36,7 @@ import hashlib import json import sys +from typing import Optional try: # optional post-quantum half: https://pypi.org/project/dilithium-py (pure Python) from dilithium_py.ml_dsa import ML_DSA_65 as _ML_DSA_65 diff --git a/integrations/xchk-registry/tests/test_xchk_registry.py b/integrations/xchk-registry/tests/test_xchk_registry.py index 48d75ad9..4fd40ceb 100644 --- a/integrations/xchk-registry/tests/test_xchk_registry.py +++ b/integrations/xchk-registry/tests/test_xchk_registry.py @@ -11,7 +11,7 @@ import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -import xchk_trace # noqa: E402 +import xchk_trace EX = Path(__file__).resolve().parents[1] / "examples" EXPECTED = json.loads((EX / "expected.json").read_text(encoding="utf-8")) @@ -36,7 +36,8 @@ def test_the_record_verifies_with_the_pinned_registry_key(): def test_the_embedded_key_is_never_reported_as_verified(): """A record checked against its own cnf.jwk is self-consistent, not verified: any self-signed record passes that.""" - no_fetch = lambda resolver, kid: None # noqa: E731 + def no_fetch(resolver, kid): + return None assert xchk_trace.verify_with_key_source(RECORD, None, False, fetch=no_fetch)["status"] == "no-key" got = xchk_trace.verify_with_key_source(RECORD, None, True, fetch=no_fetch) assert got["status"] == "self-consistent" and got["key_source"] == "embedded" diff --git a/integrations/xchk-registry/xchk_trace.py b/integrations/xchk-registry/xchk_trace.py index 6ac967c8..a64fedb1 100644 --- a/integrations/xchk-registry/xchk_trace.py +++ b/integrations/xchk-registry/xchk_trace.py @@ -61,7 +61,7 @@ def published_key(resolver: str, kid: str, timeout: float = 20.0) -> dict | None None when the document is unreachable or names no such key.""" url = f"{resolver.rstrip('/')}/.well-known/xchk-keys.json" try: - with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 + with urllib.request.urlopen(url, timeout=timeout) as r: doc = json.load(r) except Exception: # noqa: BLE001 return None @@ -100,7 +100,7 @@ def verify_with_key_source(record: dict, pinned: dict | None, allow_embedded: bo def fetch_bundle(resolver: str, ref_id: str, timeout: float = 20.0) -> dict | None: url = f"{resolver.rstrip('/')}/api/receipt/{ref_id}/bundle" try: - with urllib.request.urlopen(url, timeout=timeout) as r: # noqa: S310 (the resolver is the record's own claim) + with urllib.request.urlopen(url, timeout=timeout) as r: return json.load(r) except Exception: # noqa: BLE001 return None