diff --git a/.github/workflows/xchk-registry-conformance.yml b/.github/workflows/xchk-registry-conformance.yml new file mode 100644 index 0000000..9f357e0 --- /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 a39c9b3..ac2b46b 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 0000000..0130f69 --- /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 0000000..046b612 --- /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 0000000..87d195b --- /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 # 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` 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 + 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 0000000..8a6a92a --- /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": "Ku1IkguGrTeVOA0OuzpgAMTW/aO8CFbq/LATFrbdUeWhA5nc+2WlpaMPNVBkEfFvcQN9eCwitvBsvd2eDv4mDSCccM9kGF+ivnB8bNGHqtFXWwekAeZf3N6niHa7miik2kJuuIyCOhLbI8Y3epv+TOC4Oi3rRlDyiQAXl1DuNF8tTb7LGV8CzpxhbRDar/zbI+M8yqJJWPh99VDBzFuHrTNej9zX142zpHMYG6efUgu4JJKsS0g0BsFCBBBPcCuau+qKfd4vr/8x2yZ/RSWddYcGgIeQ5hBDzZH+vqYUIKczdQwsMyuirPIWGUHaqSsa87Gjbu+ab79IZYYVWhEAv85l0sLG6ZsVhYMB+2zgEc9h6MNk8Vu36ek360btN2qOR0tm7KOSLiSQkjgZ7aAzVlGqoDtkvO50PewzyM34dqLo4iF1/JIKHk2EgcXrni+5V+4kM4D3akJkpMBLQ2hA46NPR3kTeiGygdEr6qv1kPTMvoVEjlTeYznVBbc5VinW7fsig8KY9Y9vEaIpu9UL6moULY9QvDnTSQ6R+0lt2IwIltND82nIaZZsCdSGUxVd+xwFsQrAji0St6EioLLRScJCOTXmzFVDxIg4OVOnCAOGbXxcrYnbSDcqCVD52un9Iw80MaxYLj4D/DWUvS5MdU+0FkKe4mxx0TQb54IdTucd+m+1FutSZHL3k3vTELmwj/qJ7vlpsjslBnj6cgqEiVFvDOTVkrcrf3+j7GHYwpejqJgvKifLbD209jTwW1BQuqWXw7vOPt6wIlWN4jPIqJEznEbK5Kn6AIiP9dCfquNGd7D8YVqypvQ8vx9vD5Dbe+HyWisw7UHAq0zSNIXFjTHDCw4hfb42KjHiz1FVEfhacP61nFxH+lcLZsil/RU37wE33RoUDnGucQ7dObmal+ftn4BHhD5gWTFy8N3OQATuWcAb2kMRJ6JUoGio8vLZx7B8XWiV5tEsfQ8OTL9snfj13ny7GyDJ5II3sFN6R/mxjKIBlSe+/S58LO7S7qD9fmasTmUyrpV/CtF8PF0jQQibHEXCNgTv4M4M4y47ZoLSFJYOsBgW/oP3u0jLz7pfez9iJMzFHOTzTkH0tX0ldx0RmwSR0yN8eY3G3eyKRFNLS83qJAQQcEDzR4TCZ+9K7K5TFvI0uV8nh+wTo0gYJxQY4L45N89LHbRI/emCcbG3HfdgyAlb6jp6kajxdc0dMHycPACio11Cepcqsml8ek/nM3JN3jDEGDhxEfEy5hNWFAF1C5t/ku+gmrm6m0WYqt8DN9FSqAYGWIHYoWxj7ilGBhcYccYw5elD85KbO1eSEGdHPblDWq9AxfbI465g4Ftr2umTZPOlT44T3ZeKWU6g4t81S+Nvz1TorGgsWCxitQ1JtnDNB3q8r3BrqHEVrwT1IAZDp/A+3gEJKoAa/h7Fum8XemhfQ+eESLtS+C9bFWSh4IsdPud+xduHwsyqkzLRvJQdm3GBT8ve2l3WH54ykBfvUGvi/rtOUhVbAFw6hqClMq9pA4J/52AyR4JTNVkUICCYWUNyCgKovl8d/Bn5VO1mVIEJso0qZUGKaUDK9FpCWc+FJ/cmQs+/inUa8LVoArnTGyTSyTP0mt6MYO/n8H0RoXzYZ1JsqwIdMeQFtmc5KxYeUUt2mzQN0YiMcIX/Pk6DrdjGMR/cQKeDwOYFZ5yeaJlHGhq/UMRC9lRsA0/xCczLjvq05bMJ/M0Gj8z5OI7+zOXTCEJOibY1jkQf7xYTCZaJ9EzsYwN8LusK+e7mA5tQnXgbsUg4Rf/cUh9DKvMxsOio8BtRxNX/fjkkTFvBIM84nexRTfzTZemcw8sy/43Gb6gutqdbiINIcm6T8VAYJ2/FokVMRcMvwl2cYR7IRf+SbBD6lUNR9aVqFHf/0i8DOA/1Xk260ooeDtVabHqe0HuqMtz34QG4Snd+nnOxapxAU6qfg1RCIaxIuR7EmGoeNsh0CVZWwpNAB3RaedwtFsQ+n9Lfc88Cwf4pYZmmqnnR3F1Kcgdu/UMXXwM2oggFURpLzb4/qpAgzlHf71P7rR8X8FClHOZ8mp29VrswG914tz/Miy04ydvMaQt29KRrU3Y2Ls9AcyxHFyabuFl7HZXFEnZc4jQHgvrGEi9sGitlrfS23Dg338TZL8s78ONE1X6I48djto3eAPGA7kR/N4ZcPLwhZMJIDmik412bjl14X0e4MkQJnLImaK91t0RHm6Qqzav6lhpXTjXmb2qZbj9sVZqy4THKsnlokyLAaeQkEHFddHPzxkeMvXBZcPt+9E3hck2ZMLjNVr3qFa7031A/32oCw8gwK/NOj/Rzxio1H+DPG8NUZCW7IU67TQ56B1z9dwSvxw3DjouBWKLoi5lbmN36u9qy3lja0VvgymkELHbNQeGLk4t9AQkNt/4st3RJKe/Sbzdl84ePHuF0LELe0LjW9r+cHqgWkTrHWK/mrC+cJlVTaqeSm78S3025HU6+7TfZJrz+7BaJ7PCqcA/OUQuBHX5gf0JDiJzd0XBV/ajq1rENxnm5orRYIUNghgRCl3o2/4JHlQfH3rPNLiIQPiGrfkHOQlVclDcZ0Rue2ap9YumeYao00w56CMLstJ8cWuaLXbqi56wOcX8LtwjTuR4gleDG3YX4UCEB2hDA5GYem+Iigs6ecVgj/WF3pD5Kp6RMWB/djBuBYjtl07NZzPf+D3bfCVTHUeEMOu42pH7ANV6zH6Uc+tUkzcGCJWLfoRmn9SaRkX1d4VhKPv8WFXRv7G82sFF4gp6ykqwTHMEPkIDUDTVDOHBa7gPjpJpylWg4VxSko9nQrPW5pDrWA9Hz09nq56yl6pDE2H9aNya3uhAz8lYWfyiRcuFlQWih6VpkRiAOMQiQHeVZqBDZw1tQACod3AZHuFBrlDo2g+9FXem3IBMqjTjYbRi4kqCLBj8bJMwmUxISA8kqOvzjnlwvLQOV/y61N8qAJYbbbdWUV/uiV3HbFyOrX6Hpd6uoFxEMWLsuiGWuIHF2hICVeAxWmwWOQLxiSN+QH6j8q8/4PiSELk2kmPt+bKT5LJBEZ+2CuKyGIAj0L7wG13gxlCY5lB43wcpZmlqSUAOhTUSqdGvaWUfoeOXUA5swoI+hz3WepkbkKz5jfVCj86KsB3b1cglKSLFPNFVR3OivHWQTbKkrdmvCAUSgP8nWkINgIarih89mCKn1F1hRhSrJav3FXtQM43SuIA4ZlPuvayBeh/k9PiXt+OUcQZaguPgpzIRhY4AxKuoj6XIZHb4OkVNrE79mj15oMktaMXx3Setqti08TwADk4LeRygSzGqR9Wph416GuidMMRL6w7f5iymaw59YPBwEXHDMeUg6EEJRLrFA1gW+IituOqb+zMI+d6pmxydTJ6bynDTHq3CY1R87ndtHQiCHLax590SEaX2nvU5qao5ZqTFmx/nEUlF3P3kKeFu2oBLE1YHHuOq3lP9iwsYtXJCwFtzz1UMX41VBgBonN/EYMgw4+Mw0Ch0mXwWqgykOBYLBox51S6JPwnu0qOIQw2Fgvad6ASfZ2F1YcLNfYmoMB4JKBRkfw7KIs2Gbvt4lvLXqTb5O2rjDaYENfPMm+oI4puoZHZOcWLCROlasVRnDVWGei/dAxvu6gI4iB7TLl3P7C7UknJNwV50Nqm/K6TEtpUw9vmEOTTZlrj7ee920T9s41aWfagWMdBvtSQ+blQYU35Tnk6vU9n8Mfs/YiHgy62JK89X8St/20LmSTES70DFe3uBwEvMmAgVxqY+zsTJeX7gQPRzKyis62BuhktuEbVrRVF8CVEsPMfEq5XCAgYBIPMAKA+vn9CySiEmT6D08RAFL0o5DxzXxK7VXocgMI6rkqB2XETus+5896Hikai0mTWRmxRd4NG9aqGwaxJj2SLGhk6t+iU3K699fjHmU9msh4nDGyZdlvCU/V06XpAKlZ/TeSpPI5Gn9QfJJYPjuUTGGinBh/9MIObrhGq/AL2A0RRsPWozY+KPGMp4EZAmMsvMf85X2pUmlBVsVIap1BW+L+GB1mJx3fTiOG1wGnGBhOYmdq3UaKuqsDirUPKNvU03E9bPeSH0ZPD6u7+/Fj0ro3B5W8ez/A+nukQcWx5ONyuv2zLCiX3or0UZNmJO09BFYo5agu1z+BQYOvku9iJTZC2cksrzuoAd06hx29cGoeUvivL6mK2X0hu8FyOOe9pJb0FPmYIlnpn90L49by0SXujg4Fv6q7l4f2aYqXQe3z4/vrfByDvs9it2rjwQ1asX/ZDBPmK4WmFDZ4JtLywHuTR7GgcVBsB4YilA0Kc5WNMxm7b33Pm8/qzkYW3uTpN7rQJSmsrXNL159l/YPeX6HvMjWMVNwcb2/wuj4/wQOncbr9v4AAAAAAAAAAAAAAAAABw0SGSMq" + } + ], + "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 0000000..ca85098 --- /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 0000000..03bbf4c --- /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": "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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": "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 0000000..4293f2f --- /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 0000000..c9e0e61 --- /dev/null +++ b/integrations/xchk-registry/examples/verify_receipt.py @@ -0,0 +1,706 @@ +#!/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 +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 +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: Optional[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") + 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'})") + + # 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 0000000..e5bc0d1 --- /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 0000000..df8f6ed --- /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 0000000..4fd40ce --- /dev/null +++ b/integrations/xchk-registry/tests/test_xchk_registry.py @@ -0,0 +1,111 @@ +"""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 + +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_pinned_registry_key(): + assert xchk_trace.record_verifies(RECORD, EXPECTED["trace_signer_jwk"]) + 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.""" + 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" + 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(): + 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["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"] + 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 0000000..a64fedb --- /dev/null +++ b/integrations/xchk-registry/xchk_trace.py @@ -0,0 +1,170 @@ +"""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 # 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 --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 +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 base64 +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) -> 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, 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: + 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: + with urllib.request.urlopen(url, timeout=timeout) as r: + 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): 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) + 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: + 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 07de0e0..d7067c8 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" + ] } ] }