Skip to content

integrations: xChk registry, a Trust Record per run (record-producer, Level 0) - #338

Merged
imran-siddique merged 14 commits into
agentrust-io:mainfrom
minitru:xchk-trace-record
Oct 19, 2026
Merged

imran-siddique merged 14 commits into
agentrust-io:mainfrom
minitru:xchk-trace-record

Conversation

@minitru

@minitru minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

The narrow resubmission suggested on #337. One thing only: the software-only Level 0 Trust Record an xChk registry mints per run, with one approval-outcome reference per act a person was asked about, and how a relying party verifies it with released agentrust-trace against the key the registry publishes: pinned, or fetched under the record's kid from /.well-known/xchk-keys.json, never the key the record itself carries.

Contents: integration.yaml (record-producer, Level 0, community tier), a README that says what the record is and what it does not claim (nothing about what a reference resolves to; no hardware; no ordering), one committed record from a live run with the pinned signer key, five offline tests, an opt-in live key check, and a floating/fixed workflow that runs the tests and trace-tests verify --level 0. The receipt verifier, the bundles and the claims about the approver's assertion are not in this PR; they come separately once the private findings are settled with the maintainers.

Reproduction:

pip install -e "integrations/xchk-registry[test]" agentrust-trace-tests
pytest integrations/xchk-registry/tests -q
trace-tests verify --record integrations/xchk-registry/examples/01-trust-record.json --level 0

🤖 Generated with Claude Code

… Level 0)

An xChk registry licenses AI agents under a verified person and gates each act. This listing covers one thing: the software-only Level 0 Trust Record the registry mints per run, with one approval-outcome reference per act a person was asked about, and how a relying party verifies it with released agentrust-trace against the key the registry publishes (pinned, or fetched under the record's kid; never the key the record itself carries). One committed record from a live run, a pinned key, five offline tests, an opt-in live key check, and the Level 0 conformance suite in the workflow. What a reference resolves to is out of scope here and said so; that is a separate integration.

Signed-off-by: minitru <bbunix@gmail.com>

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 5ae0b47df7f34a74f03d7e5b0ea01ed34aa63a0a. This is a useful reduction from #337: it limits itself to a software-only Level 0 record producer and explicitly excludes receipt verification and ordering claims. The five offline tests pass (one live check skipped); the opt-in live key check passes; Level 0 conformance passes. The reported hosted check is green, while the maintainer approval gate awaits a current-head approval.

Requesting focused changes for the freshness and reproducibility findings below. A separate signer-key trust concern persists in the default published-key path; I documented its current-head evidence in the existing private security advisory, without asserting the old receipt-verifier findings against this narrower PR. Please keep this PR open for those bounded fixes and negative regressions.

Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
Comment thread integrations/xchk-registry/README.md Outdated
…a stale-record test (review of agentrust-io#338, 18 Oct 2026)

The verifier passed max_age_seconds=None, which switched off the library's 24-hour check; section 3.2.2 requires a
finite maximum age. Now MAX_AGE_SECONDS = 86400 (the library's default), --max-age overrides it and must be positive,
and --now re-verifies a retained record at its own time instead of widening the bound. The output carries the
revocation outcome as the library reports it, no_check_performed here since no bundle or store is given (3.2.3),
and the reason for a refusal. Tests: a record one second past the bound is invalid, the bound cannot be 0, negative
or None, the CLI refuses --max-age 0, a month-old record is stale at the default, the revocation outcome is printed;
the offline tests pin now at the fixture's own minute. README: the age bound and --now, and the conformance
command gets the explicit --max-age the workflow already uses, because the example record is historical.
@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both points taken, fixed at 9d97b87.

P1, record age. max_age_seconds=None is gone. The verifier now has MAX_AGE_SECONDS = 86400, the library's
default and §3.2.2's, overridable with --max-age, which must be positive: the bound cannot be switched off. A
retained record is re-verified at its own time with --now <epoch seconds> rather than by widening the bound, which
is also how the offline tests now pin the historical fixture (at the minute it was minted). Negative tests added: a
record one second past the bound is invalid with the library's reason; --max-age 0, a negative value and None
are refused; a month-old record is stale at the default.

P1, revocation. The output now carries revocation as the library reports it. This tool passes no bundle and no
store, so it prints no_check_performed, and the README says that this is not an affirming appraisal (§3.2.3). A
refused record carries reason.

P2, README. The conformance command carries --max-age 315360000, the same as the workflow, with a sentence
saying why: the example record is from 17 Oct 2026, and the verifier's own default stays 24 hours.

On the signer-key concern in the advisory: I still cannot see it as the PR author, so I have not tried to address
it here. If a maintainer can forward it or add me, I will take it up in the same bounded way.

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 9d97b879a796e3b46e11b1ec742c7b81f249fd2e. The finite 24-hour age bound, revocation outcome, stale negative tests, and historical-fixture command address my prior public findings. Local checks passed: eight tests including the opt-in live key check, Level 0 conformance, and 1,000 seeded age/clock cases; three tampered records were rejected. The hosted workflow is still action_required, so I am not treating it as a green CI result.

Requesting two bounded output fixes below. Separately, the signer-key trust concern already documented in the private advisory remains on this head's published-key path. @carloshvp has been added to that advisory for maintainer triage; please coordinate the details privately. This is based on a synthetic local check and does not assert production exposure.

Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
…er from a server the record names (18 Oct 2026)

The default path fetched the key document from the resolver the record itself carried, so a record could name the
server its key would be fetched from: the embedded-key problem one hop away. Now the trusted origin is the relying
party's (--registry) or the origin under the record's did:web subject; the record's resolver must agree with it and
is never followed. The fetch is https only, follows no redirect, checks the status and reads at most 256 KB. The
output names the trusted origin, and a refusal says why. Tests: a resolver pointing elsewhere, a subject moved to
another host, an http origin, a redirect and an oversize document all end in no-key with nothing fetched from the
record's server; did:web parsing incl. an encoded port. Live check (REGISTRY_LIVE=1) passes against in.xchk.io.
@noah-ing

Copy link
Copy Markdown
Collaborator

Correction to my review above: I checked the collaborator list and @carloshvp has not yet been added to this particular private advisory. I had conflated its access with separate reports. The confidential finding is documented there, and I am checking the appropriate maintainer access path. I also see new head 858aa7f and am re-reviewing it now; I am not yet claiming it resolves the concern.

@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

One more change, at 858aa7f, on the default (published-key) path, since it is the one the review named. The tool
used to fetch the key document from the resolver the record itself carried, so a record could name the server its
key would be fetched from. Now the trusted origin is the relying party's (--registry) or the origin under the
record's did:web subject; the record's resolver must agree with it and is never followed. The fetch is https
only, follows no redirect, checks the status and reads at most 256 KB; the output names the trusted origin and a
refusal says why. Tests: a resolver pointing elsewhere, a subject moved to another host, an http origin, a redirect
and an oversize document all end in no-key with nothing fetched from the record's server. If that is the concern
in the advisory, good; if it is something else, I would still like to see it.

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 858aa7fc6337f41d0926475a7683a152869eac49. The new origin check, HTTPS-only fetch, redirect refusal and size bound are useful improvements. In isolated checks, nine offline tests passed (one opt-in live check skipped), and Level 0 conformance passed. The hosted gate failure is the independent maintainer-approval requirement; I have not inferred a successful technical workflow from it.

The default published-key path still lacks a trust origin supplied by the relying party, as documented with a synthetic current-head control in the existing private advisory; @carloshvp now has access to assess it. Please keep the reproduction private. I am requesting the focused trust-anchor and malformed-key-document fixes below. My prior current-head review's profile/accepted_profiles and historical --now output comments remain open and are not duplicated here. No production exposure or released affected version is established.

Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
… fields and the as-of time in every verdict; a malformed key document is no key (review of agentrust-io#338, round 3, 18 Oct 2026)

Deriving the published-key origin from the record's did:web subject (858aa7f) only showed the record agreed with
itself: a record signed under a publisher of its own choosing names that publisher in its subject too. Now
--registry is the one source of the origin; without it or --jwk there is no trust anchor and the answer is no-key.
The record's subject and resolver must agree with the origin given. Negative control: a record signed end to end
by a key the attacker made, with its own subject and resolver and its host publishing the key, is no-key without an
anchor, no-key against the relying party's registry, and invalid against the pinned real key; nothing is fetched
from the attacker's host in any case. A verified result carries the profile and the complete accepted_profiles set
(section 3.3) and the trusted key's thumbprint. Every verdict carries verified_at, the instant it was made against,
and verified_at_overridden, so a historical verdict is never the same JSON as a current one; tested as a
stale-current / historical-verified pair. key_from_published refuses every malformed document shape without
raising, and the fetch wraps it besides. 12 tests pass including the live key fetch.
…ed origin, removed; the docstring says what a verdict carries; one instant for the as-of time (18 Oct 2026)
@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 at bfd4dcf, all four open items, checked against every thread on the PR.

P1, trust anchor. You are right that deriving the origin from the record's own subject only showed the record
agreed with itself. --registry is now the one source of the published-key origin; without it or --jwk there is
no trust anchor and the answer is no-key. The record's subject and resolver must agree with the origin given.
Negative control added as you described: a record signed end to end by a key of the attacker's making, with its own
subject and resolver under the attacker's host and that host publishing the key. It is no-key without an anchor,
no-key against the relying party's registry, and invalid against the pinned real key; nothing is fetched from
the attacker's host in any case.

P2, profile fields. A verified result carries profile and the complete accepted_profiles from the library
result (§3.3), and the trusted key's thumbprint; asserted in the verified-output test.

P2, as-of time. Every verdict, no-key included, carries verified_at, the instant passed to verify_record,
and verified_at_overridden. Tested as the stale-current / historical-verified pair.

P2, malformed key document. key_from_published refuses every malformed shape without raising, the fetch wraps
it besides, and the negative test runs sixteen bad documents through it. A bad document is no-key, exit 1.

13 tests pass locally under 3.11 including the live key fetch. Also run here, from the workflow definitions: the
Validate steps (manifest schema, compatibility floors, integration index and Marketplace catalog --check), the
repository-wide Lint command, and every link in the README and manifest; all pass. On the advisory: understood that
@carloshvp has it now; I will keep anything about the reproduction private.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public-scope review of exact head bfd4dcf. The published-key path now requires an explicit relying-party origin/key, and 14 additional malformed-key-document controls refuse without a parsing exception. The no-origin control performs zero key fetches. The committed integration suite passes 12 tests with its one opt-in live test skipped; root validation passes 28 tests, and the committed record passes the released 0.6.2 Level 0 selection (not hardware appraisal).

One remaining public reporting defect: verify_with_key_source samples as_of_fields(now) into bounds, then verification samples it again when now is omitted. The final **bounds overwrites the instant actually used by verify_record. With a two-value clock [100, 101] and a spy on verify_record, the library receives now=101 while the result says verified_at=100. This is a deterministic metadata-binding discrepancy, not a signature forgery or a claim of production exposure.

Please freeze one effective instant at the wrapper boundary, use it both for library verification and every emitted verdict, and preserve whether the caller explicitly overrode time. Add a no-override two-clock-value regression plus an explicit --now control; both should assert the recorded instant equals the one supplied to the verifier.

All returned technical workflows on this head currently require maintainer authorization, so I am not calling hosted CI green. This public review neither assesses nor resolves the separate private report, and does not establish what an approval-outcome reference proves.

…ew of agentrust-io#338 by dinakarjs, 18 Oct 2026)

With --now absent the clock was read twice: once in verify_with_key_source for the verdict's verified_at and again
in verification for the instant handed to verify_record, and the verdict's value overwrote the library's. A clock
answering 100 then 101 gave the library 101 while the verdict said 100. Now an Instant is frozen once where the
request enters (verify_with_key_source, or main for the CLI) and the same object reaches the library call and every
emitted verdict, no-key included; the clock is read at most once per verdict; whether the caller overrode the time
is preserved. Regressions: the two-value clock with a spy on verify_record on the pinned, published and no-key
paths, and through the CLI, with and without --now; each asserts the recorded instant equals the one supplied to
the verifier. 15 tests pass including the live key fetch.
…tion, not one review at a time (18 Oct 2026)

Four review rounds had found input shapes one at a time (a stale record, a malformed key document, two clock
reads), so this time every top-level field of the record, its key fields and its reference fields were set to each
of fourteen odd values and run through main() on the pinned, published and no-anchor paths: 40 mutations crashed,
all on the pinned or published path (a record that is not an object, cnf or cnf.jwk not an object, references not
a list, a resolver that is not a string), and the pinned-key file crashed on anything but an object. Now: a record
that is not an object is `invalid`; a pinned key that is not an Ed25519 JWK is `no-key`, as is a key document of
any other shape; references and resolver are read only when shaped; any exception the library raises on a shape
it did not foresee is `invalid` with the exception named; a record or key file that is not JSON is a verdict, not
a traceback; --registry must be an https origin and nothing more (no credentials, path, query or fragment); --now
must be non-negative. The verdict's `kid` is always the key the record names, `pinned_kid` the pinned key's own,
and the thumbprint the key actually used. The mutation loop is a test (798 runs, every one a verdict with the frozen
instant), beside tests for each input. 18 tests pass under 3.11 and 3.12 including the live key fetch; Level 0
conformance passes; the repository's Validate and Lint steps pass.
@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

First, an apology. The verified_at defect was introduced by my own previous round, in the change meant to fix the
as-of reporting, and I then said the head had been checked against every thread. It had not been checked well
enough. Thank you both for the scrutiny, and for the patience across four rounds in one day; the reviews have been
precise every time and the integration is better for each of them.

The clock, fixed at 122d959. One Instant is frozen where the request enters (verify_with_key_source, or
main for the CLI) and the same object reaches the library call and every emitted verdict, no-key included; the
clock is read at most once per verdict, and whether the caller supplied the time is preserved. Your two-value
control is now a test, with a spy on verify_record, on the pinned, published and no-key paths and through the
CLI with and without --now; each asserts the recorded instant equals the one given to the verifier.

Then, rather than wait for the next shape to be found one review at a time, at 2dcb159 every top-level field of
the record, its key fields and its reference fields were set to each of fourteen odd values and run through
main() on all three paths: 40 mutations crashed (a record that is not an object, cnf or cnf.jwk not an
object, references not a list, a resolver that is not a string), and the pinned-key file crashed on anything but
an object. All of it now ends in a JSON verdict and an exit code: a non-object record is invalid; a pinned key
that is not an Ed25519 JWK is no-key; references and resolver are read only when shaped; an exception the library
raises on a shape it did not foresee is invalid with the exception named; a record or key file that is not JSON
is a verdict, not a traceback; --registry must be an https origin and nothing more; --now must be non-negative.
The verdict's kid is always the key the record names, pinned_kid the pinned key's own, and the thumbprint the
key actually used. The mutation loop is committed as a test (798 runs), beside a test per input.

18 tests pass under Python 3.11 and 3.12 including the live key fetch; Level 0 conformance passes; the
repository's Validate and Lint steps pass locally. I have not run 3.13 here.

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 2dcb15961f28d6581e8735b50f82e6fef0abdf4a. The out-of-band trust anchor, one frozen verification instant, and fail-closed key-document handling address my earlier findings on those paths. The local integration suite passes (17 passed, one opt-in live check skipped), and the committed record passes Level 0 conformance. Independent freshly signed synthetic controls exposed the bounded origin-contract and malformed-URL issues below. Please cover these cases with negative tests. This review does not reassert the earlier private signer-key finding on this head, and it does not claim production exposure. The hosted gate currently fails for independent maintainer approval; I am not treating that as a green technical CI run.

Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head 2dcb159. The verification-instant finding is resolved: four additional clock controls pass, including equality with the actual verify_record argument and preservation of explicit versus implicit time. The committed integration suite plus root tests pass 45 tests with one opt-in live test skipped. The released 0.6.2 Level 0 selection passes (15 checks, four skipped); this is software/historical verification, not receipt or hardware appraisal.

One remaining bounded CLI error-handling defect: https_origin calls urlsplit and then u.port without handling their ValueError refusals. With the committed record and --now 1792252551, each of --registry https://in.xchk.io:bad, --registry https://in.xchk.io:65536, and --registry 'https://[broken' raises an uncaught ValueError before emitting any JSON. The malformed bracket case fails in urlsplit; the other two fail while reading u.port. Three independent CLI controls reproduce this, while a valid-origin control verifies with the expected fetched key and all four clock controls pass (three failures, five passes in that additional selection). These cases require no network request.

Please handle origin parsing/port validation as an explicit refusal, retaining a nonzero exit and a reason (consistent with this head's documented verdict contract), and add malformed-port/range/bracket controls with a valid-origin twin and zero-fetch assertions. This is a caller-option crash, not a signature bypass or production-exposure claim.

Ruff at the repository's E4/E7/E9/F scope and whitespace checks pass. All returned current-head hosted workflows still require maintainer authorization; I have not called hosted CI green. This public review does not assess or close the existing private report.

…nder the origin required; every approval-outcome resolver checked, order-independent (review of 2dcb159 by noah-ing and dinakarjs, 18 Oct 2026)

Three findings on the last head, each outside the shapes the previous sweep covered. (1) https_origin let urlsplit and
.port raise on a bad bracket, a non-numeric or out-of-range port: a traceback instead of the promised verdict. Both
are caught and refused with the reason; the netloc must also be exactly host or host:port, so a trailing colon or
an empty port is refused too. (2) A record whose subject was absent or not did:web passed the origin check, because
only a parseable, different did:web origin was refused; a signed synthetic record with subject did:key verified.
The subject must now be a did:web under the trusted origin, and a signed control for each shape says so. (3) Only
the first approval-outcome resolver was checked, so a second, off-origin reference verified and the result depended
on the order; every approval-outcome reference must now resolve at the origin, one that names no resolver is a
refusal, references of other kinds are ignored, and a signed control covers each ordering with a valid twin. Then
the sweep that should have come first: 43 origin strings x 19 subjects x 8 reference sets on signed records, every
run a verdict, a fetch only when the origin is well formed and the record agrees with it; the same origins through
the CLI, pinned and published. 22 tests pass on 3.11 and 3.12 including the live key fetch; Validate and Lint
steps pass; Level 0 conformance passes.
…FC 6454); a malformed --registry is refused even beside a pinned key (18 Oct 2026)
@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thank you both. All three at 09c436b, each with a freshly signed synthetic control and a valid twin, and then the
sweep that should have come before this round.

Malformed --registry (both of you). https_origin now catches what urlsplit and .port raise, so a bad
bracket, a non-numeric or out-of-range port is a no-key refusal with the reason, exit 1, no fetch. The netloc must
also be exactly host or host:port, so a trailing colon or an empty port is refused; https://host:443 is the
same origin as https://host (RFC 6454); and a malformed --registry is refused even beside --jwk, since an
option that is given is checked. Controls: eight malformed origins through verify_with_key_source and the CLI,
with the valid twin fetching the key once.

Subject. The published-key path now requires a did:web subject under the trusted origin; absent, non-string,
did:key, did:web: and another host are each refused before any fetch, with signed controls (the did:key and
other-host ones verify under their own key, as yours did) and a twin under the origin that verifies with the
fetched key.

Every approval-outcome reference. All of them must resolve at the trusted origin, whatever their position; one
that names no resolver is a refusal (§3.1.2 obliges the registry to keep each one resolvable, so one with no
resolver is not a reference this tool can check); references of other kinds are ignored. Signed controls for
[on, off], [off, on], [on, on, off], [on, unnamed], and the twins [on, on], [on, other-kind], [].

The sweep. 43 origin strings × 19 subjects × 8 reference sets, each a signed record through
verify_with_key_source, asserting a verdict every time and a fetch only when the origin is well formed and the
record agrees with it; the same origins through the CLI, pinned and published. It is a committed test.

22 tests pass under 3.11 and 3.12 including the live key fetch; the repository's Validate and Lint steps and Level
0 conformance pass locally.

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 09c436b48f7787146b9844979ab43b9b62300181. The three findings from my preceding review are addressed on the published-key path: malformed origins produce a refusal, a did:web subject is required under the trusted origin, and every approval-outcome resolver is checked. Locally, 21 integration tests passed with one opt-in live test skipped; the committed record passed Level 0 conformance, and the diff check was clean. I am requesting one bounded fix for the combined --jwk + --registry mode below: the program accepts both options but currently enforces only the pinned key and the registry URL syntax, which can misstate the caller’s intended origin constraint. Either enforce both when both are supplied, or reject their combination and document the choice. The default-port observation is a separate, nonblocking false refusal. The hosted gate is still blocked on independent maintainer approval; I have not treated it as a green technical CI run. These are local controls, not a claim of production exposure.

Comment thread integrations/xchk-registry/xchk_trace_record.py
Comment thread integrations/xchk-registry/xchk_trace_record.py Outdated
…ust parsed; resolvers compared as origins; the sweep pins a key too and judges by an oracle of its own (review of 09c436b by noah-ing, 18 Oct 2026)

With --jwk the registry was only parsed, so a record for another registry verified under the pinned key while
--registry named a third. Now an origin that is given is enforced whichever way the key comes: the record's subject
must be a did:web under it and every approval-outcome resolver must be that origin, compared in canonical form, so
an explicit :443 or a different case is the same origin and a resolver with a path or another scheme is not one.
Controls: the committed record with its own registry and with another, pinned both times; signed records with a
non-did:web subject and an off-origin second reference, pinned, refused by the origin and verified by the key alone;
the explicit-443 twin. The sweep now runs every combination pinned as well as published, and its expectation of
what an origin string means is written from RFC 6454 in the test, not taken from https_origin; that oracle found
one more case on the way, a leading space, which urlsplit strips and the verifier accepted: whitespace and control
characters are now refused. 23 tests pass on 3.11 and 3.12 including the live key fetch; Validate and Lint steps
and Level 0 conformance pass.

@noah-ing noah-ing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head dd97be8cb54ece88604badf2d63fa68c0f2d730e. The combined --jwk + --registry path now enforces the supplied origin before pinned-key verification, and approval resolvers are compared as canonical HTTPS origins; the explicit :443 positive control verifies. Independent freshly signed controls also refused foreign subjects and first/second foreign approval resolvers before any key fetch, while matching pinned and published paths verified. Locally, all 24 integration tests passed with the opt-in live key check enabled; the committed record passed Level 0 conformance (15 checks, four optional skips), and the exact-head diff check was clean. I found no remaining blocking issue in this narrow Trust Record verifier scope, so this approval supersedes my earlier changes requests on #338. It does not assess the receipt verifier from closed #337, establish what an approval reference proves, or claim production/hardware assurance. I did not run the repository-wide validation/lint jobs locally; the hosted gate is still awaiting a named maintainer approval on this head, which my Reviewer approval does not satisfy.

@minitru

minitru commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Both at dd97be8, and the sweep rebuilt as you described.

P2, combined mode. An origin that is given is now enforced whichever way the key comes: with --jwk beside
--registry, the pinned key verifies the signature and the record's subject must be a did:web under the origin
and every approval-outcome resolver must be that origin; a disagreeing record is no-key before the key is
consulted. Controls: the committed record pinned with its own registry (verified, trusted_origin reported) and
with another (refused on the subject); signed records with a non-did:web subject and with an off-origin second
reference, pinned, refused by the origin and verified by the key alone; and the README says what the combination
means.

P3, resolver normalisation. Resolvers are compared as origins through the same canonical form as --registry,
so an explicit :443 or a different case is the same origin and a resolver with a path, another scheme or
credentials is refused as not an origin. Your %3A443 / :443 / --registry https://in.xchk.io:443 twin is a
test and verifies with the fetched key.

The sweep. It now runs every combination pinned as well as published (6,536 runs each way), and its
expectation of what an origin string means is written in the test from RFC 6454, sharing no code with the
verifier. That oracle found one more case on the way: a leading space, which urlsplit strips and the verifier
had accepted; whitespace and control characters in --registry are now refused.

24 tests pass under 3.11 and 3.12 including the live key fetch; the repository's Validate and Lint steps and Level
0 conformance pass locally.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head dd97be8 against the public, narrow Trust Record integration scope. My earlier verification-instant and malformed-origin findings are resolved, and the combined pinned-key/origin and all-resolver checks remain enforced. This approval supersedes my earlier public changes-requested reviews on #338 within that scope.

Fresh Linux/Python 3.12.14 with released agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2: committed integration tests 23 passed, 1 opt-in live check skipped; root repository tests 28 passed. The retained committed record passes released Level 0 conformance (15 checks, 4 optional skips) under the explicitly documented historical age allowance. This is not a fresh live run.

Nine separately written controls using a fixed synthetic Ed25519 signer pass: pinned/published/combined valid paths feed the library exactly the reported instant after one clock read; bad port, out-of-range port and broken bracket refuse with zero fetches; validly signed off-origin references in either position verify under the key alone but refuse in combined mode before fetching; explicit :443 subject/resolver/registry forms agree. An isolated mutant that skips origin enforcement only when a pin is supplied causes both resolver-position controls to fail on status assertions; restoring the source passes all nine controls. No source changes were retained, and the checkout is clean.

Repository integration-manifest validation (48 integrations, zero failures), compatibility validation, generated index/catalog checks, root tests, Ruff in CI's E4/E7/E9/F scope and whitespace checks pass. The sole returned current-head hosted check is the failed maintainer gate; no hosted technical success is inferred from it.

No remaining blocking finding in this bounded re-review. No live published-key fetch, receipt/bundle verification, proof of what an approval reference establishes, ordering, hardware or production assurance is claimed. This does not assess or resolve the separate private findings associated with the receipt work, and Reviewer approval does not substitute for the named maintainer acceptance gate. Reviewed with OpenAI Codex assistance.

Comment thread integrations/xchk-registry/xchk_trace_record.py Fixed
imran-siddique
imran-siddique previously approved these changes Oct 19, 2026
…rint; a pinned private key is refused and never echoed (19 Oct 2026)

CodeQL (py/clear-text-logging-sensitive-data, security-extended) flagged the final print in xchk_trace_record.py on
this head (dd97be8) and on agentrust-io#350's. Its "secret" heuristic matches any name containing "trusted", and two such names
reached the printed verdict: the library result's trusted_key_thumbprint attribute and this module's trusted_origin()
function. One is the RFC 7638 thumbprint of a PUBLIC key; the other is the https origin the caller passed with
--registry. Neither is secret. Changes:

- verification(): the thumbprint is computed with the library's own jwk_thumbprint() over the key verified with
  (agentrust-trace 0.11.0, the floor). A test asserts it equals the library result's thumbprint.
- trusted_origin() is renamed anchor_origin(), for the origin the relying party anchors trust in. Output field names
  are unchanged (trusted_origin, trusted_key_thumbprint).
- Real hardening found while tracing the flow: a pinned key file carrying the private member d is refused with
  fixed text ("pin the public key only") and nothing of it is echoed; a key file that does not decode is reported by
  error class only (a decode error's message can quote bytes of the file).

Same change as agentrust-io#350's 6013421 for these two files, so the stacked branch rebases onto this cleanly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
…erifier, moved here from trace-spec#560 (19 Oct 2026)

A crosswalk for one registry's receipts belongs with that registry's integration (maintainers, trace-spec#560). This is the replacement for the part of closed agentrust-io#337 that agentrust-io#338 left out, stacked on agentrust-io#338.

- CROSSWALK.md: field mapping, verifier obligations, boundary; two verifier rules added — the signed ask must name the ceremony the sealed record and the registry-signed binding commit to, and absence of failure is not confirmation (--require-approval).
- examples/approval-outcome/: the eight records and seven bundles from #560, README, expected.json.
- verify_receipt.py: the registry's offline verifier, vendored as served (MIT, in NOTICE), current version.
- tests: the fixture test reworked (finite explicit age bound; the approval verdict requires the approval evidence to have been present and checked; mutations for missing evidence, a same-credential cross-ceremony assertion, unsigned outer labels) and the verifier's own regressions.
- NOTICE, README (adopter line folded in; what is deliberately not here), pyproject test extras.

Not in this change: a packaged relying-party verdict; under the repository's private assessment, follows here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@minitru

minitru commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 7646077 on top of the approved dd97be8, one commit: the CodeQL clear-text-logging alert on xchk_trace_record.py's verdict print. The heuristic matched two names: trusted_key_thumbprint, an RFC 7638 thumbprint of the public key, now taken from the library's own jwk_thumbprint(); and trusted_origin(), the --registry origin, renamed anchor_origin(). Output field names are unchanged.

Found while tracing the flow, and included: a pinned key file that carries a private member d is refused with fixed text and nothing of it is echoed; a key file that does not decode is reported by error class only. Three tests (tests/test_trust_record.py).

26 passed, 1 skipped under Python 3.11; ruff --target-version py39 --select E4,E7,E9,F clean. No other change. The same two-file change is in #350, which is restacked on this head. Sorry for moving the head after your approval; I took that over merging with an open security alert.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the two-file delta from dd97be8 to 7646077. The fixed-text pinned-private-key refusal and error-class-only key-file decoding report have direct regressions; the public-key thumbprint parity test retains the existing output contract. No additional defect established in this delta.

The changed implementation and test_trust_record.py are byte-identical in stacked #350 head 9b1622e, where I ran the complete offline selection: 73 passed, 1 live check skipped with released agentrust-trace 0.11.0 / agentrust-trace-tests 0.6.2 on Python 3.12.14. This is shared-file validation, not a separate full #338 run or a live registry assessment.

All 14 returned current-head hosted workflows are action_required, including CodeQL and xchk-registry conformance. A maintainer needs to authorize them and review this new head; the earlier approval predates this commit. No assertion that the scanner finding is cleared until the current-head scan runs. Reviewed with OpenAI Codex assistance.

@imran-siddique
imran-siddique merged commit 4eb3464 into agentrust-io:main Oct 19, 2026
41 of 42 checks passed
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
…erifier, moved here from trace-spec#560 (19 Oct 2026)

A crosswalk for one registry's receipts belongs with that registry's integration (maintainers, trace-spec#560). This is the replacement for the part of closed agentrust-io#337 that agentrust-io#338 left out, stacked on agentrust-io#338.

- CROSSWALK.md: field mapping, verifier obligations, boundary; two verifier rules added — the signed ask must name the ceremony the sealed record and the registry-signed binding commit to, and absence of failure is not confirmation (--require-approval).
- examples/approval-outcome/: the eight records and seven bundles from #560, README, expected.json.
- verify_receipt.py: the registry's offline verifier, vendored as served (MIT, in NOTICE), current version.
- tests: the fixture test reworked (finite explicit age bound; the approval verdict requires the approval evidence to have been present and checked; mutations for missing evidence, a same-credential cross-ceremony assertion, unsigned outer labels) and the verifier's own regressions.
- NOTICE, README (adopter line folded in; what is deliberately not here), pyproject test extras.

Not in this change: a packaged relying-party verdict; under the repository's private assessment, follows here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
…val without it is approval-unsealed, never confirmed (20 Oct 2026)

Review of agentrust-io#350 at 9b1622e (noah-ing): with the approval evidence intact, removing only the seal, only the Merkle proof,
both, or only the registry's seal signature still let `verify_receipt.py --require-approval` exit 0 and the fixture
verdict return approval-confirmed. Approval evidence present does not make the receipt sealed.

- verify_receipt.py (byte-identical to the copy the registry serves): `sealChainMissing` in the --json report names
  the missing part (the seal, the Merkle path from the record to it, the registry's signature over it);
  --require-approval exits 3 when it is non-empty, result `seal-chain-incomplete`, kept apart from
  `approval-evidence-missing` (whose wording is unchanged). The default run is unchanged.
- Fixture verdict: approved, approval evidence present, seal chain incomplete -> `approval-unsealed`, distinct from
  `approval-unverified` and from `approval-confirmed`.
- Tests: the four deletion mutations as negative regressions, both at the verifier and through the fixture verdict;
  the sealed control still confirms.
- CROSSWALK: the "absence of failure is not confirmation" rule now states the seal-chain half.

Rebased on main after agentrust-io#338 merged (main's agentrust-io#338 tree is identical to the base this was built on). pytest
integrations/xchk-registry/tests: 78 passed, 1 skipped (3.11); ruff --target-version py39 --select E4,E7,E9,F clean;
lychee over the directory 0 errors; type-mutation sweep 14,625 mutations, 0 crashes; every earlier review-matrix row
keeps its verdict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants