# AI Catalog Trust Manifest reference implementation A runnable signer and verifier for the [AI Catalog](https://ai-catalog.io) Level 3 "Trusted Catalog" trust layer: signed subject binding, identity anchoring, an algorithm allowlist, freshness, substantive-member enforcement, and SSRF-safe fetching. It ships a **substitution-attack vector suite** that demonstrates, with running code, that the attack the spec's threat model describes succeeds without subject binding and is closed by it. The spec states the threat in prose. This repository is a reproducible demonstration of it. Contributed by [TomeVault](https://tomevault.io), which publishes quality and consistency attestations for AI instruction files. The cryptography here mirrors what TomeVault runs in production: detached EdDSA JWS over RFC 8785 JCS, keys resolved by `kid` from a published `.well-known/jwks.json`, and a signed subject bound to the served bytes. ## Tracking the specification Aligned with spec head `63909bf` (2026-08-20). Three normative changes landed in the summer of 2026 and are implemented here: - **Subject binding is now normative.** A Trust Manifest that carries a `signature` MUST include a `subject`, and a consumer MUST confirm `subject.type` equals the entry's `type` and that the fetched bytes match `subject.digest`. The member is `type`, not `mediaType`. The decision is [ADR-0019](https://github.com/Agent-Card/ai-catalog/blob/main/adr/0019-trust-manifest-artifact-binding.md) (dated 2026-06-20, from Luca Muscariello's STRIDE threat model of the Trust Manifest), merged in PR #47 on 2026-08-02. This library implemented the binding from 2026-07-02, while it was still in flight, and described it at the time as its own proposed extension. That framing was wrong and is removed: the decision record predates this library. What this repository contributes is a running demonstration of the threat and a verifier that enforces the fix. - **Substantive members** (ADR-0020). Beyond `identity`, a Trust Manifest MUST carry a signature, a non-empty `attestations`, a non-empty `provenance`, or a `trustSchema`. One that would carry only informational members MUST be omitted rather than served empty, and a consumer SHOULD read it as absent. - **Extensions replace `metadata`** (ADR-0017). Custom data lives in a single `extensions` map keyed by a URL or a reverse-DNS string, on the catalog, on an entry, and on a Trust Manifest. `verify_manifest(require_subject_binding=False)` is kept only to reproduce the pre-2026-08-02 behaviour for the attack demonstration. It is a teaching switch, not a conformance mode. ## What it implements | Spec element | Module | Notes | | --- | --- | --- | | Detached JWS over JCS (RFC 7515 + 8785) | `jws.py`, `jcs.py` | empty-payload compact serialization `header..signature` | | Algorithm allowlist (RFC 8725) | `jws.py` | rejects `none` and all HMAC; permits EdDSA, ES\*, PS\*, RS256 | | Subject binding (ADR-0019) | `manifest.py` | signed `subject {url?, type, digest}` closes substitution | | Substantive members (ADR-0020) | `manifest.py` | `is_substantive()`, reported as a verification check | | Identity anchoring | `manifest.py` | `identity` domain MUST align with the entry `urn:air:` publisher | | Freshness | `manifest.py` | required `issuedAt`, optional `expiresAt` | | Extensions map (ADR-0017) | `catalog.py`, `manifest.py` | `extensions` on catalog, entry and manifest | | SSRF-safe fetch | `fetch.py` | https only, blocks loopback, link-local, private and metadata addresses | | Key resolution | `keys.py`, `resolve.py` | HTTPS JWKS and `did:web`, selected by `kid` | | Level 3 catalog assembly | `catalog.py` | `specVersion`, `host`, entry `trustManifest` | | Scan attestation type | `attestations.py` | `io.tomevault.consistency-scan`, evidence not verdict | | Sealed discovery index | `conformance.py` | a signed index commits to shard CONTENT, never to bare URLs | | in-toto Statement v1 in DSSE | `intoto.py` | the same signed content in the envelope cosign and the in-toto stack read | ## The scan attestation: evidence, not a verdict The spec deliberately does not bless a scanner or define a quality verdict, and leaves `attestations[].type` open. Following the reverse-DNS namespacing the extensions rules mandate, this library carries a content-addressed scan attestation (`io.tomevault.consistency-scan`). It asserts only that a content-addressed scan report is published at a URI. It never asserts the artifact is safe or compliant. A consumer dereferences the report, recomputes the digest, and decides. The cross-model consistency signal is carried as a `prediction` of divergence risk, never a verified medal. ## A second envelope, so an outside tool can check us `intoto.py` re-expresses the same signed content as an [in-toto Statement v1](https://in-toto.io/Statement/v1) inside a [DSSE](https://github.com/secure-systems-lab/dsse) envelope, so a consumer holding an off-the-shelf verifier can check a claim against our published key with no code of ours. The DSSE Pre-Authentication Encoding is implemented from the spec text rather than by importing a DSSE library, so the emitter and any third-party verifier stay genuinely independent implementations of one wire format. Neither envelope is privileged. Both sign the same content digest with the same key, so they cannot disagree about what was checked. ## Run it ```bash pip install -e . python make_vectors.py pytest ``` `make_vectors.py` signs a conformant Level 3 entry, runs eight scenarios (happy path, drift, substitution with and without subject binding, `alg:none`, identity mismatch, expiry, JWKS resolution), asserts each verdict, and writes canonical artifacts to `vectors/`. It exits non-zero if any scenario does not match its expected verdict. The substitution scenarios are the point. Scenario 3 shows a validly-signed manifest with no subject accepting a swapped malicious artifact. Scenario 4 shows the same swap rejected once `subject` is signed. The test suite is adversarial: 128 tests over hostile and malformed inputs, SSRF bypass attempts (redirects, DNS rebinding, private and metadata addresses), `did:web` resolution edge cases, structural conformance against the spec's CDDL shapes, and `tests/test_spec_alignment_2026_08.py`, which pins each of the three 2026 changes above so a regression to the older shape fails rather than passing against a stale reading of the spec. ## Reusing the vectors `vectors/` and the conformance tests are meant to be reused. Any implementation can sign the vector inputs with the fixed-seed test key and check its verdicts against ours, including the substitution vector, which is the case an implementation is most likely to get wrong while its own tests stay green. If a shared conformance suite is useful to the working group, these are offered as a starting point. ## Note on keys `make_vectors.py` uses a fixed-seed TEST key so the vectors are reproducible. It is not a TomeVault production key and must not be used to sign anything real. TomeVault's production public key is published at `https://tomevault.io/.well-known/jwks.json`.