From 58597c3a8d153b0ab20e46523b71fa1f5b5e69dc Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Sun, 21 Jun 2026 13:25:01 +0200 Subject: [PATCH 1/4] feat(spec): require substantive trust manifest; add OCI/xRegistry mappings - Require a present Trust Manifest to carry a substantive member (signature, attestations, provenance, or trustSchema); ADR-0015. - Add a substrate-neutral distribution-mapping contract with OCI and xRegistry bindings; record ADR-0014. Signed-off-by: Luca Muscariello --- adr/0014-trust-manifest-artifact-binding.md | 75 +++ adr/0015-substantive-trust-manifest.md | 63 ++ specification/ai-catalog.md | 603 +++++++++++++++++--- specification/examples/ai-catalog.json | 26 +- specification/respec-config.json | 20 +- 5 files changed, 712 insertions(+), 75 deletions(-) create mode 100644 adr/0014-trust-manifest-artifact-binding.md create mode 100644 adr/0015-substantive-trust-manifest.md diff --git a/adr/0014-trust-manifest-artifact-binding.md b/adr/0014-trust-manifest-artifact-binding.md new file mode 100644 index 0000000..6fcf03a --- /dev/null +++ b/adr/0014-trust-manifest-artifact-binding.md @@ -0,0 +1,75 @@ +# ADR-0014: Bind Trust Manifest Signatures to Artifact Content + +**Status:** Accepted + +**Date:** 2026-06-20 + +**Participants:** Luca Muscariello (Cisco) + +## Context + +[ADR-0009](0009-trust-manifest-substitution.md) acknowledged that Trust +Manifest substitution is a real threat and called for a security review. +That review was performed as a STRIDE threat model (see +[trust-manifest-threat-model.md](../specification/trust-manifest-threat-model.md)) +and confirmed the central weakness: the `signature` covered only the +Trust Manifest JSON, while the artifact reference (`url`, `data`, +`mediaType`) lived on the Catalog Entry *outside* the signed payload. +The only binding was `identity == identifier`. An attacker who controlled +the catalog document could keep a validly-signed manifest and repoint +`url` to a malicious artifact — the signature would still verify +(threat T1 / finding F1). + +The threat model also compared the design against the Sigstore +architecture from which it derives. Sigstore's Cosign signs the artifact +*digest*, Fulcio binds identity through a CA, and Rekor witnesses the +event in a transparency log. The Trust Manifest natively reproduced none +of these; most importantly it lacked Cosign's digest binding. + +## Decision + +Bind a signed Trust Manifest to the artifact it describes and harden the +verification story: + +1. **Subject binding.** A signed Trust Manifest MUST include a `subject` + object `{ url?, mediaType, digest }` committing to the artifact's + media type and content digest. `subject` is part of the signed + payload, so the artifact reference cannot be changed without + invalidating the signature. (Closes F1; mirrors Cosign.) +2. **Signatures required at Level 3.** A Trusted Catalog MUST carry a + `signature`, `subject`, and `issuedAt` on each relied-upon entry, and + consumers MUST verify them. (Closes F2.) +3. **Trust anchoring.** Verification MUST anchor `identity` to an + out-of-band trust root; a verified signature alone proves internal + consistency, not publisher authenticity. (Closes F3; substitutes for + Fulcio's CA-attested identity.) +4. **Algorithm allowlist.** Consumers MUST reject `alg: none` and + symmetric algorithms and MUST use asymmetric JWS algorithms. (F4) +5. **Freshness.** Add `issuedAt` (REQUIRED when signed) and `expiresAt`, + with anti-rollback and revocation guidance. (F5) +6. **Catalog-level integrity.** Add an OPTIONAL top-level catalog + `signature` and recommend content-addressed (OCI) distribution. (F6) +7. **Safe fetching.** Verification MUST defend against SSRF and + oversized responses. (F7) +8. **Provenance-statement and publisher verification** procedures are + defined; publisher fields are advisory unless bound. (F8, F9) + +## Sigstore convergence + +The Trust Manifest remains a *format that carries* trust evidence, not a +replacement for Sigstore services. The specification SHOULD allow +Sigstore evidence to be first-class: a `sigstore-bundle` attestation +(certificate + signature + Rekor inclusion proof), anchoring via the +Sigstore TUF root, and a preference for transparency-log inclusion and +keyless signing over long-lived publisher keys. This lets +trust-sensitive deployments inherit Sigstore's full chain instead of a +weaker re-implementation. Tracked as future work. + +## Consequences + +- The substitution attack from ADR-0009 is closed for signed manifests. +- Level 3 conformance is stricter: presence of a manifest is no longer + sufficient; it must be signed and bound. +- Residual gaps versus Sigstore remain (no native transparency log, + long-lived keys, trust-anchor bootstrapping) and are documented as + residual risks plus the Sigstore-convergence roadmap. diff --git a/adr/0015-substantive-trust-manifest.md b/adr/0015-substantive-trust-manifest.md new file mode 100644 index 0000000..d917a9f --- /dev/null +++ b/adr/0015-substantive-trust-manifest.md @@ -0,0 +1,63 @@ +# ADR-0015: Require a Substantive Trust Manifest + +**Status:** Accepted + +**Date:** 2026-06-21 + +**Participants:** Luca Muscariello (Cisco) + +## Context + +The Trust Manifest is an OPTIONAL companion to a Catalog Entry. Within +it, however, only `identity` was unconditionally REQUIRED, and `identity` +MUST equal the entry's `identifier`. Every other member — `signature`, +`attestations`, `provenance`, `trustSchema`, and the informational fields +— was independently OPTIONAL. + +As a result the smallest valid Trust Manifest was `{ "identity": "..." }`, +which merely restates the entry identifier and carries no trust signal. +Only Level 3 (Trusted Catalog) forced a real payload (`signature` + +`subject` + `issuedAt`). Between "no manifest" and Level 3 the format +permitted a present-but-empty manifest that looks like trust metadata +while asserting nothing — misleading to consumers and noise to tooling. + +If every field is optional, the unit of optionality is wrong: the +*manifest* should be the optional element, and when an author chooses to +include one it should be required to be meaningful. + +## Decision + +Define a validity floor for the Trust Manifest. Beyond the required +`identity`, a Trust Manifest MUST contain at least one *substantive* trust +member: + +- a `signature` (with its required `subject` and `issuedAt`), +- a non-empty `attestations` array, +- a non-empty `provenance` array, or +- a `trustSchema`. + +`identity` and `identityType` (which restate or describe the entry +identifier) and the informational members `privacyPolicyUrl`, +`termsOfServiceUrl`, and `metadata` do not satisfy this requirement. +`subject`, `issuedAt`, and `expiresAt` are not substantive on their own: +an unsigned `subject` digest is attacker-settable and unverifiable, so +they count only as part of a `signature`. + +A Trust Manifest that would carry only non-substantive members MUST be +omitted entirely. Consumers SHOULD treat a manifest that violates this +rule as if no Trust Manifest were present. + +## Consequences + +- A present Trust Manifest now always carries at least one verifiable or + governance-bearing claim; the degenerate identity-only manifest is no + longer valid. +- Authors who have nothing substantive to assert simply omit + `trustManifest`, which is already OPTIONAL — no information is lost. +- The rule is independent of conformance level: even a Level 1 catalog + that includes a manifest must make it substantive. Level 3 remains + stricter, requiring a `signature` specifically (see + [ADR-0014](0014-trust-manifest-artifact-binding.md)). +- Unsigned manifests remain valid when they carry attestations, + provenance, or a trust schema, preserving low-assurance use cases that + do not sign. diff --git a/specification/ai-catalog.md b/specification/ai-catalog.md index bca3f1b..c5f35cc 100644 --- a/specification/ai-catalog.md +++ b/specification/ai-catalog.md @@ -147,6 +147,14 @@ The following members are OPTIONAL: non-standard metadata. See [Metadata Extensibility](#metadata-extensibility) for key naming conventions. +`signature` +: A string containing a detached JWS [[RFC7515]] signature computed over + the JCS-canonicalized [[RFC8785]] catalog document (excluding the + `signature` member itself), providing catalog-level integrity over the + `entries` array and `host`. It is verified exactly as a Trust Manifest + signature (see [Trust Manifest Signatures](#trust-manifest-signatures)). + See [Trust Manifest Substitution](#trust-manifest-substitution). + ## Host Info The Host Info object identifies the operator of the catalog. It MUST @@ -366,11 +374,14 @@ A Trust Manifest MUST contain: identity schemes is open. When a Trust Manifest appears within a Catalog Entry, the `identity` -field's authority or trust domain MUST align with the publisher domain segment of -the entry's URN `identifier` field. This binding ensures trust claims are -cryptographically bound to the authorized publisher of the catalog entry. -Consumers MUST reject a Trust Manifest whose identity domain does not -align with the publisher domain of the entry's `identifier`. +field MUST match the entry's `identifier` field. This binding ensures trust +claims are unambiguously associated with the catalog artifact. +Consumers MUST reject a Trust Manifest whose `identity` does not +match the containing entry's `identifier`. The `identity` is restated +here — rather than read from the entry's `identifier` — so that it falls +within the signed payload; this is an intentional duplication, not +redundant metadata, and removing it would leave the signature uncommitted +to which identifier the trust claims apply. When a Trust Manifest appears on a Host Info object, `identity` SHOULD match the host's `identifier` field when present. @@ -380,6 +391,31 @@ values), each entry MAY carry its own Trust Manifest. There is no requirement that all versions carry identical trust metadata — trust properties may evolve across versions. +## Manifest Validity + +A Trust Manifest exists to carry verifiable trust evidence; an empty one +adds nothing and misleads consumers into believing trust metadata is +present. Beyond the required `identity`, a Trust Manifest MUST therefore +contain at least one *substantive* trust member: + +- a `signature` (with its required `subject` and `issuedAt`), +- a non-empty `attestations` array, +- a non-empty `provenance` array, or +- a `trustSchema`. + +The members `identity` and `identityType` (which restate or describe the +entry identifier) and the informational members `privacyPolicyUrl`, +`termsOfServiceUrl`, and `metadata` do NOT satisfy this requirement. +`subject`, `issuedAt`, and `expiresAt` are not substantive on their own: +an unsigned `subject` digest is attacker-settable and unverifiable, so +they count only as part of a `signature`. + +A Trust Manifest that would carry only non-substantive members MUST be +omitted entirely rather than included empty — the `trustManifest` member +is itself OPTIONAL, so no information is lost. Consumers SHOULD treat a +Trust Manifest that violates this rule as if no Trust Manifest were +present. + ## Optional Members The following members are OPTIONAL: @@ -409,14 +445,37 @@ The following members are OPTIONAL: `termsOfServiceUrl` : A string containing a URL to the terms of service. +`subject` +: A Subject object as defined in [Subject Binding](#subject-binding) that + cryptographically binds this Trust Manifest to the specific artifact it + describes. A Trust Manifest that carries a `signature` MUST include a + `subject`. + +`issuedAt` +: A string containing an ISO 8601 [[RFC3339]] timestamp indicating when + the Trust Manifest was created and signed. A Trust Manifest that + carries a `signature` MUST include `issuedAt`. + +`expiresAt` +: A string containing an ISO 8601 [[RFC3339]] timestamp after which the + Trust Manifest MUST be considered stale. Consumers SHOULD reject a + Trust Manifest whose `expiresAt` is in the past. + `signature` -: A string containing a detached JWS [[RFC7515]] signature computed - over the Trust Manifest content. This enables integrity verification - of the trust metadata independent of the artifact. +: A string containing a detached JWS [[RFC7515]] signature computed over + the canonicalized Trust Manifest content, including the `subject` and + `issuedAt` members. Because the signed payload commits to the artifact + digest carried in `subject`, neither the trust claims nor the artifact + reference can be substituted without detection. See + [Trust Manifest Signatures](#trust-manifest-signatures). `metadata` : An open map of string keys to arbitrary values for extending trust - metadata. + metadata. Unlike the Catalog Entry's `metadata` (which is + informational and unsigned), this map is part of the signed payload + when the Trust Manifest carries a `signature`; use it for extensions + that must be cryptographically bound to the manifest, and use the + entry's `metadata` for unsigned, informational extensions. For example, a Trust Manifest with identity, attestations, and provenance: @@ -452,10 +511,57 @@ provenance: ], "privacyPolicyUrl": "https://acme-corp.com/legal/privacy", "termsOfServiceUrl": "https://acme-corp.com/legal/terms", + "subject": { + "url": "https://api.acme-corp.com/agents/finance/v2.1.json", + "mediaType": "application/a2a-agent-card+json", + "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "issuedAt": "2026-03-15T10:00:00Z", "signature": "eyJhbGciOiJFUzI1NiJ9..detached-jws-signature" } ``` +## Subject Binding + +The Subject object binds a Trust Manifest to the specific artifact it +describes, closing the substitution gap in which an attacker who +controls the catalog document leaves a validly-signed Trust Manifest in +place but repoints the entry to a different artifact. Because the +Subject is part of the signed payload, the artifact reference and its +content digest cannot be changed without invalidating the signature. + +A Subject object MUST contain: + +`mediaType` +: A string containing the media type of the bound artifact. This MUST + equal the containing Catalog Entry's `mediaType`. + +`digest` +: A string containing the cryptographic digest of the artifact content, + in the format defined in [Digest Format](#digest-format). For an + artifact referenced by `url`, the digest is computed over the exact + bytes served. For an artifact embedded in `data`, the digest is + computed over the JCS-canonicalized [[RFC8785]] JSON value. + +The following member is OPTIONAL: + +`url` +: A string containing the URL of the bound artifact. When present, it + MUST equal the containing Catalog Entry's `url`. Consumers MUST reject + a Trust Manifest whose `subject.url` does not match the entry's `url`. + +A Trust Manifest that carries a `signature` MUST include a `subject`. +When verifying such a manifest, consumers MUST confirm that the fetched +artifact's media type and digest match the `subject` before relying on +any claim in the Trust Manifest. See +[Verifying Artifact Integrity](#verifying-artifact-integrity). + +The `subject.mediaType` and `subject.url` intentionally restate the +entry's `mediaType` and `url` so that those values fall within the signed +payload. This is a deliberate duplication, not redundant metadata: +without it, an attacker who controls the catalog document could change +the entry's media type or location without invalidating the signature. + ## Trust Schema Object A Trust Schema object describes the trust framework applied to the @@ -569,6 +675,30 @@ This section describes how consumers verify the trust metadata carried by a Trust Manifest. Verification is OPTIONAL — consumers that do not need trust assurance can skip this entirely. +### Safe Fetching + +Verification procedures direct consumers to fetch URLs that originate in +the Trust Manifest itself (`attestation.uri`, `statementUri`, +`registryUri`, and key-resolution endpoints). Because a manifest may be +attacker-controlled before its identity is anchored, these fetches are a +server-side request forgery (SSRF) and denial-of-service surface. +Consumers performing verification MUST: + +- Resolve and reject URLs targeting private, loopback, link-local, or + cloud metadata addresses (e.g., `127.0.0.0/8`, `::1`, + `169.254.0.0/16`, `fc00::/7`, `10.0.0.0/8`, `172.16.0.0/12`, + `192.168.0.0/16`), and re-check the resolved address after any + redirect. +- Restrict fetches to the `https` scheme (or inline `data:` URIs) and + refuse to follow redirects that cross into a disallowed address range. +- Enforce a maximum response size and a request timeout. When + `attestation.size` is present, reject responses that exceed it; in all + cases apply a consumer-defined ceiling. +- Treat every fetched document as untrusted input. + +Consumers SHOULD prefer inline `data:` attestations and Data-URI logos +to avoid leaking verification activity to third-party endpoints. + ### Digest Format Digests in this specification use the format `algorithm:hex-value`, @@ -582,20 +712,54 @@ MUST reject digest values using algorithms shorter than SHA-256. ### Trust Manifest Signatures -The `signature` field carries a detached JWS [[RFC7515]] computed -over the Trust Manifest content. To create or verify a signature: +The `signature` field carries a detached JWS [[RFC7515]] computed over +the Trust Manifest content, including the `subject` and `issuedAt` +members. To create or verify a signature: 1. **Canonicalize** the Trust Manifest JSON using JCS (JSON - Canonicalization Scheme) [[RFC8785]]. Remove the `signature` - field itself before canonicalization. -2. **Sign** (or verify) the canonical bytes as a detached JWS - payload using the publisher's private (or public) key. -3. **Encode** the resulting JWS in compact serialization and store - it in the `signature` field. - -This approach ensures the signature is stable regardless of JSON -key ordering or whitespace, and can be verified independently of the -artifact content. + Canonicalization Scheme) [[RFC8785]]. Remove the `signature` field + itself before canonicalization; all other members — including + `subject` and `issuedAt` — remain in the signed payload. +2. **Select an algorithm** from the allowlist in + [Signature Algorithms](#signature-algorithms). The JWS `alg` header + parameter MUST identify the algorithm used. +3. **Sign** (or verify) the canonical bytes as a detached JWS payload + using the publisher's private (or public) key. +4. **Encode** the resulting JWS in compact serialization and store it + in the `signature` field. + +This approach ensures the signature is stable regardless of JSON key +ordering or whitespace. Because the signed payload includes the +`subject` binding, a verified signature commits the publisher to a +specific artifact digest, not merely to the trust claims. + +Producers SHOULD avoid placing numeric values that do not round-trip +under JCS serialization (e.g., integers outside the range exactly +representable as IEEE 754 doubles) in a signed Trust Manifest, as such +values can cause a verifier's canonicalization to differ from the +producer's. Where large integers are required, encode them as strings. + +### Signature Algorithms + +To prevent signature-forgery attacks, producers and consumers MUST +constrain the JWS algorithms used for Trust Manifest signatures. + +- Consumers MUST reject a signature whose JWS `alg` header is `none`. +- Consumers MUST reject symmetric (MAC-based) algorithms such as + `HS256`; Trust Manifest signatures MUST use an asymmetric algorithm so + that verification cannot be performed with attacker-supplied secret + material (preventing public-key-as-HMAC-secret confusion). +- Producers MUST use, and consumers MUST support, one or more of the + following asymmetric algorithms [[RFC7518]]: `ES256`, `ES384`, + `EdDSA`, `PS256`, `PS384`, or `RS256`. +- Consumers MUST determine the expected algorithm and key from the + resolved trust anchor (see [Trust Anchoring](#trust-anchoring)) and + MUST NOT let the `alg` header alone select a verification algorithm in + a way that downgrades security. Consumers SHOULD pin the expected key + via the JWS `kid` header. + +These constraints follow the JSON Web Token current best practices +[[RFC8725]]. ### Key Resolution @@ -619,6 +783,30 @@ DNS : Resolve the domain's TLS certificate and extract the public key, or look up a DNSKEY/TXT record containing the JWK thumbprint. +### Trust Anchoring + +Verifying a Trust Manifest signature proves that the manifest was signed +by the holder of the key associated with its `identity`. It does NOT, by +itself, prove that the `identity` is the legitimate publisher of the +artifact. An attacker who controls the catalog document can replace both +the `identity` and the key it resolves to, then sign the forged manifest +with their own key — every internal check would still pass. + +Consumers MUST therefore anchor the `identity` (or signing key) to a +trust root established out of band, independent of the catalog document. +Acceptable anchors include: + +- A pinned allowlist of trusted publisher identities or keys. +- A registry or marketplace that vets publisher identities and serves + the catalog over a channel the consumer independently trusts. +- An identity method that proves control of a name the consumer already + trusts (e.g., a `did:web` whose domain matches an expected publisher, + validated against that domain's TLS-authenticated endpoint). + +A verified signature without an anchored identity establishes integrity +and internal consistency only; consumers MUST NOT treat it as proof of +publisher authenticity. + ### Verifying Host Identity To verify the host of a catalog: @@ -642,15 +830,37 @@ To verify the publisher of an artifact: 4. Confirm the JWT claims bind the `publisher.identifier` to the Trust Manifest's `identity`. -### Verifying Artifact Integrity +The `publisher` object resides on the Catalog Entry, outside the Trust +Manifest signature. Consumers MUST treat `publisher` fields as advisory +unless a verified `publisher-identity` attestation cryptographically +binds `publisher.identifier` to the signed manifest's `identity`. -When a Trust Manifest includes `provenance` entries with `sourceDigest`: +### Verifying Artifact Integrity -1. Fetch the artifact content from the entry's `url`. -2. Compute the digest using the algorithm specified in the - `sourceDigest` field. -3. Compare the computed digest to the declared value. Reject the - artifact if they differ. +When a Trust Manifest carries a `signature`, it MUST include a `subject` +that binds it to the artifact (see [Subject Binding](#subject-binding)). +To verify artifact integrity: + +1. Verify the Trust Manifest signature + ([Trust Manifest Signatures](#trust-manifest-signatures)) and anchor + the identity ([Trust Anchoring](#trust-anchoring)). +2. Confirm `subject.mediaType` equals the entry's `mediaType`, and, when + `subject.url` is present, that it equals the entry's `url`. +3. Fetch the artifact content from the entry's `url`, or take it from + the entry's `data`, observing the limits in + [Safe Fetching](#safe-fetching). +4. Compute the digest of the fetched bytes (for `url`) or of the + JCS-canonicalized value (for `data`) using the algorithm named in + `subject.digest`. +5. Compare the computed digest to `subject.digest`. Reject the artifact + if they differ. + +Because the `subject` is part of the signed payload, this check binds +the publisher's signature to the exact artifact, defeating catalog-level +substitution of the artifact URL or content. The OPTIONAL +`provenance[].sourceDigest` records the digest of an upstream *source* +(e.g., a Git commit) and is complementary to — not a substitute for — +the `subject` digest. ### Verifying Attestations @@ -662,6 +872,23 @@ For each attestation in the `attestations` array: 3. Validate the attestation per its `type` (e.g., verify a JWT signature, confirm a PDF certificate is current). +### Provenance Statements + +A Provenance Link MAY reference a signed provenance statement via +`statementUri` and the key that signed it via `signatureRef`. To verify +such a statement: + +1. Fetch the statement document from `statementUri`, observing + [Safe Fetching](#safe-fetching). +2. Resolve the key indicated by `signatureRef` using the procedure in + [Key Resolution](#key-resolution) and anchor it per + [Trust Anchoring](#trust-anchoring). +3. Verify the statement's signature using an algorithm from + [Signature Algorithms](#signature-algorithms). +4. Confirm the statement's subject matches the artifact's `subject` + digest. Treat an unverifiable statement as absent, not as a failure + of the artifact itself. + # Organizing Catalogs As catalogs grow, a flat list of entries becomes unwieldy. Because any @@ -917,7 +1144,8 @@ A conformant Minimal Catalog is a JSON document with media type All other fields (`host`, `publisher`, `trustManifest`, `metadata`) are OPTIONAL. This level is sufficient for use cases that only need a simple list of AI artifacts — for example, a catalog of -MCP servers or A2A agents. +MCP servers or A2A agents. A `trustManifest`, when present at any level, +MUST be substantive (see [Manifest Validity](#manifest-validity)). ## Level 2: Discoverable Catalog @@ -931,8 +1159,19 @@ In addition to Level 1 requirements, a Discoverable Catalog: In addition to Level 2 requirements, a Trusted Catalog: -- Includes `trustManifest` objects on entries and/or the host, as - defined in [Trust Manifest](#trust-manifest) +- Includes a `trustManifest` object on every entry whose trust is to be + relied upon, and MAY include one on the host, as defined in + [Trust Manifest](#trust-manifest) +- Each such `trustManifest` MUST carry a `signature`, a `subject` + binding it to the artifact ([Subject Binding](#subject-binding)), and + an `issuedAt` timestamp +- Consumers MUST verify the signature, anchor the identity + ([Trust Anchoring](#trust-anchoring)), and confirm the `subject` + digest before relying on any claim +- SHOULD provide catalog-level integrity, either by serving the catalog + through a content-addressed channel (see + [Security Considerations](#security-considerations)) or by including a + top-level catalog `signature` - MAY include `publisher` objects on entries with verifiable identifiers - Enables verifiable identity, compliance attestations, and provenance tracking @@ -969,14 +1208,18 @@ appropriate to their threat model. catalog-level substitution.** **Layer 2 — Signed Trust Manifest** -: The Trust Manifest includes a `signature` field (detached JWS). - The consumer verifies the signature against the publisher's public - key before trusting any claims in the Trust Manifest — including - provenance digests, attestations, and identity bindings. This closes - the substitution gap from Layer 1: an attacker cannot forge a - signed Trust Manifest without the publisher's private key. - Consumers SHOULD verify signatures when present and SHOULD reject - Trust Manifests whose signature does not validate. +: The Trust Manifest includes a `signature` field (detached JWS) and a + `subject` that binds the signature to the artifact's content digest + (see [Subject Binding](#subject-binding)). The consumer verifies the + signature, anchors the signer's identity to a trust root + ([Trust Anchoring](#trust-anchoring)), and confirms the `subject` + digest before trusting any claim. This closes the substitution gap + from Layer 1: because the signed payload commits to the artifact + digest, an attacker cannot repoint the entry to a different artifact + or forge claims without the publisher's private key. Consumers that + rely on trust metadata MUST verify signatures and MUST reject Trust + Manifests whose signature does not validate, whose `subject` does not + match the fetched artifact, or whose identity cannot be anchored. **Layer 3 — Content-Addressed Distribution (OCI)** : The catalog is distributed through an OCI registry where all content @@ -1016,11 +1259,37 @@ this threat: unauthorized modification. - **Layer 1** enables post-fetch integrity checks but does not prevent whole-entry substitution. -- **Layer 2** prevents Trust Manifest forgery, ensuring provenance - digests and attestations are authentic. +- **Layer 2** binds the signed Trust Manifest to the artifact digest + via `subject`, preventing both Trust Manifest forgery and artifact + substitution under a valid signature. - **Layer 3** makes modification structurally impossible through content-addressing. +## Trust Manifest Substitution + +Because a Trust Manifest is a peer element of the catalog entry rather +than part of the artifact, an attacker who can write the catalog +document can attempt to substitute the artifact, the Trust Manifest, or +both. This specification defends against substitution with three +compounding mechanisms: + +- **Subject binding.** A signed Trust Manifest MUST include a `subject` + that commits to the artifact's media type and content digest (see + [Subject Binding](#subject-binding)). The artifact reference therefore + cannot be changed without invalidating the signature. +- **Trust anchoring.** A signature is only meaningful once the signer's + identity is anchored to a trust root established out of band (see + [Trust Anchoring](#trust-anchoring)); otherwise an attacker can sign a + forged manifest with their own key. +- **Catalog-level integrity.** Per-entry signatures do not prevent an + attacker from adding, removing, or reordering whole entries. Hosts + SHOULD additionally provide catalog-level integrity, either by serving + the catalog through a content-addressed channel (Layer 3) or by + including a top-level catalog `signature` computed over the + JCS-canonicalized [[RFC8785]] catalog document (excluding the + `signature` member itself) and verified exactly as a Trust Manifest + signature. + ## Identifier Typosquatting Catalog entries are identified by URIs/URNs. An attacker can register @@ -1077,6 +1346,7 @@ classDiagram specVersion string entries CatalogEntry[] host HostInfo + signature string } class HostInfo { displayName string @@ -1098,11 +1368,18 @@ classDiagram } class TrustManifest { identity string + subject Subject trustSchema TrustSchema attestations Attestation[] provenance ProvenanceLink[] + issuedAt string signature string } + class Subject { + url string + mediaType string + digest string + } class TrustSchema { identifier string version string @@ -1124,6 +1401,7 @@ classDiagram CatalogEntry --> "0..1" Publisher : publisher CatalogEntry --> "0..1" TrustManifest : trustManifest HostInfo --> "0..1" TrustManifest : trustManifest + TrustManifest --> "0..1" Subject : subject TrustManifest --> "0..1" TrustSchema : trustSchema TrustManifest --> "*" Attestation : attestations TrustManifest --> "*" ProvenanceLink : provenance @@ -1513,13 +1791,7 @@ artifact with its own trust metadata. The `data` field inlines a catalog with protocol-specific entries, allowing clients to choose MCP or A2A based on their capabilities. -# Mapping to OCI Distribution - -This appendix describes how AI Catalog documents can be distributed -through OCI registries, enabling content-addressed storage, signing, -and replication using existing container infrastructure. - -## Logical Format vs. Physical Distribution +# Mapping to Distribution Substrates The AI Catalog specification defines a **logical format**: a JSON document with `entries`, `displayName`, `type`, and `trustManifest` @@ -1527,36 +1799,89 @@ fields that are immediately meaningful to anyone working with AI artifacts. Authors write simple JSON. APIs serve simple JSON. Clients consume simple JSON. -OCI provides a **physical distribution layer**: content-addressed -storage, cryptographic signing via Cosign/Notation, global replication -through registries, and referrer-based metadata association. These are -valuable infrastructure capabilities, but OCI's data model uses -container-oriented vocabulary (`manifests`, `layers`, `config`, -`digest`) that does not naturally describe a catalog of AI artifacts. - -This specification treats OCI as one distribution option, not as the -canonical data model. The logical AI Catalog format remains the -authoring and consumption interface. Tooling bridges the two: +That logical format can be **physically distributed** over more than one +substrate — an OCI registry, an [[xRegistry]] registry, or a plain HTTP +server. Each substrate has its own data model and its own native +capabilities. To keep the logical format authoritative and to avoid +diverging, substrate-specific dialects, every binding defined by this +specification MUST satisfy the same contract: packing a logical document +into the substrate and unpacking it again MUST reproduce an equivalent +logical document. ``` -Authoring Distribution Consumption -───────── ──────────── ─────────── -ai-catalog.json ──pack──► OCI Registry ──unpack──► ai-catalog.json - entries[] Index/Manifests entries[] - trustManifest Referrers trustManifest +Authoring Distribution Consumption +───────── ──────────── ─────────── +ai-catalog.json ──pack──► OCI / xRegistry / HTTP ──unpack──► ai-catalog.json + entries[] substrate-native form entries[] + trustManifest trustManifest ``` -This separation means: +This separation keeps authoring and consumption simple: publishers and +clients work with domain vocabulary (`entries`, `displayName`, +`mediaType`, `trustManifest`), while infrastructure that wants +content-addressing, signing, replication, or registry APIs uses whichever +binding below matches its substrate. + +## Binding Invariants -- **Simplicity for authors**: Publishers write AI Catalog JSON using - domain vocabulary. No knowledge of OCI manifests, digests, or layer - descriptors is required. -- **Simplicity for consumers**: Clients that fetch from - `/.well-known/ai-catalog.json` or a registry API receive the logical - JSON format. They never need to parse OCI structures. -- **Power for infrastructure**: Registries that want content-addressed - integrity, signing, and replication can store catalogs as OCI - artifacts using the mapping defined below. +A conforming binding MUST preserve each of the following invariants. This +list — not any single substrate's vocabulary — is the conformance bar for +a pack/unpack round-trip. + +| Logical concept | Invariant a binding MUST preserve | +|:---|:---| +| Entry identity (`identifier`) | A stable, addressable identity for each entry | +| Artifact content + `mediaType` | The artifact bytes are retrievable together with their media type | +| Catalog structure / nesting | Nested catalogs remain navigable as a hierarchy | +| Trust Manifest association | An entry's Trust Manifest is discoverable from that entry | +| Content integrity | The served bytes are verifiably bound to `trustManifest.subject.digest` | +| Signing | The Trust Manifest's authenticity is cryptographically verifiable | + +## Delegate, Don't Duplicate + +Substrates differ in what they can express natively. OCI is +content-addressed and has first-class signing (Cosign/Notation); +xRegistry is a hierarchical resource API with versioning and +cross-referencing but no native digest or signature primitive. + +To avoid expressing the same guarantee twice, a binding **delegates** an +invariant to a native substrate primitive when one exists, and otherwise +**carries** it in the logical Trust Manifest. A binding MUST NOT restate, +in substrate vocabulary, a guarantee it has delegated, and MUST NOT drop +a guarantee the substrate cannot express. + +| Invariant | OCI primitive (delegate) | xRegistry primitive (delegate) | Carried fallback | +|:---|:---|:---|:---| +| Identity | Repository path + digest | `resourceid` / `xid` | `entry.identifier` | +| Content + media type | `layers[0]` + `artifactType` | Resource document + `contenttype` | Entry artifact + `mediaType` | +| Nesting | Nested Image Index | Nested Group / `xref` | Nested entry | +| Manifest association | Referrers API (`subject`) | `xref` / extension attribute | Inline `trustManifest` | +| Content integrity | Content-addressed digest | *(none — carried)* | `subject.digest` | +| Signing | Cosign / Notation referrer | *(none — carried)* | Detached JWS in Trust Manifest | + +The two sections that follow are concrete bindings of this contract. The +OCI binding delegates the most (identity, content integrity, signing); +the xRegistry binding delegates structure, identity, and discovery but +carries content integrity and signing because xRegistry has no native +primitive for them. + +# Mapping to OCI Distribution + +This appendix binds the [Mapping to Distribution +Substrates](#mapping-to-distribution-substrates) contract to OCI +registries, enabling content-addressed storage, signing, and replication +using existing container infrastructure. OCI's data model uses +container-oriented vocabulary (`manifests`, `layers`, `config`, `digest`) +that does not naturally describe a catalog of AI artifacts, so tooling +bridges the logical format and the OCI representation. + +Of the binding invariants, the OCI binding **delegates** identity, +content integrity, and signing to OCI's own primitives. Consequently +`trustManifest.subject.digest` is expected to equal the OCI descriptor +digest of the served artifact, and the detached JWS in the Trust Manifest +MAY be omitted from the packed representation because Cosign/Notation +referrers carry signing instead. Unpacking reconstitutes (or re-signs) +the logical Trust Manifest from those referrers. ## Conceptual Mapping @@ -1711,6 +2036,138 @@ Both approaches can coexist. A tooling bridge converts between them losslessly, allowing simple consumers to work with the logical format while infrastructure-oriented deployments leverage OCI distribution. +# Mapping to xRegistry + +This appendix binds the [Mapping to Distribution +Substrates](#mapping-to-distribution-substrates) contract to [[xRegistry]], +a hierarchical registry model organized as Registry → Groups → Resources +→ Versions. xRegistry contributes structure, identity, versioning, +cross-referencing, and a registry API; it has no native content-addressing +or signature primitive. + +Of the binding invariants, the xRegistry binding **delegates** entry +identity, catalog structure, artifact content, and manifest discovery to +xRegistry's own primitives, but **carries** content integrity and signing +in the Trust Manifest. The detached JWS and `subject.digest` remain +authoritative exactly as in plain-HTTP distribution, because xRegistry +cannot express either guarantee natively. + +## Conceptual Mapping + +An AI Catalog document maps to an xRegistry **Group** whose **Resources** +are the catalog entries; each entry's artifact is the Resource document. + +| AI Catalog (Logical) | xRegistry (Physical) | +|:---|:---| +| AI Catalog document | A Group instance (e.g. in a `aicatalogs` Group type), or the Registry root when serving a single catalog | +| Catalog Entry | A Resource within that Group | +| Entry `identifier` | Resource `id` and `xid` | +| Entry `mediaType` | Version `contenttype` (with `format` when a named format applies) | +| Entry artifact content | Resource document — inline (`` / `base64`) or external (`url`) | +| Entry metadata (displayName, description, tags) | `name`, `description`, `labels` | +| Entry version | Version `versionid` | +| Nested Catalog Entry | A nested Group referenced from the entry's Resource via `meta.xref` | +| Trust Manifest | An extension attribute on the Resource (an object), or a related Resource referenced by `xref` | +| Content integrity (`subject.digest`) | Carried in the Trust Manifest (xRegistry has no native digest) | +| Signing | Detached JWS retained in the Trust Manifest (xRegistry has no native signature) | + +## Packing: AI Catalog to xRegistry + +Tooling converts an AI Catalog JSON document into xRegistry resources: + +1. **The catalog** becomes a Group instance. Catalog-level metadata maps + to Group attributes: `displayName` to `name`, host identity and other + metadata to `labels` or extension attributes. + +2. **Each catalog entry** becomes a Resource in that Group. The entry's + artifact content is stored as the Resource document — inline via + `` / `base64`, or by reference via `url`. + `mediaType` maps to `contenttype`; entry metadata maps to `name`, + `description`, and `labels`. Multiple entry versions map to Versions. + +3. **Trust Manifests** are carried as an extension attribute on the + Resource (for example `aicatalog_trustmanifest`), or as a related + Resource referenced from `meta.xref`. The detached JWS and + `subject.digest` are retained unchanged because xRegistry provides no + native signing or content-addressing to delegate to. + +4. **Nested catalog entries** become nested Groups; the parent entry's + Resource references the nested Group through `meta.xref`. + +## Unpacking: xRegistry to AI Catalog + +Tooling converts xRegistry resources back to an AI Catalog JSON document: + +1. Retrieve the Group in document view (the xRegistry `?doc` projection), + which returns a single self-contained JSON document analogous to + `ai-catalog.json`. +2. For each Resource, read its document (inline or via `url`) + and `contenttype` to recover the entry's artifact content and + `mediaType`; map `name`, `description`, and `labels` back to entry + metadata. +3. Read the Trust Manifest from the extension attribute or `xref`'d + Resource, and verify its detached JWS and `subject.digest` against the + served bytes. +4. Resolve `xref`'d nested Groups into nested catalog entries. +5. Assemble the logical `application/ai-catalog+json` document. + +## xRegistry Document Example + +The following shows the xRegistry document-view representation of a Group +holding two entries. This is generated by tooling, not authored by hand: + +```json +{ + "aicatalogid": "acme-services", + "self": "https://registry.acme.com/aicatalogs/acme-services", + "xid": "/aicatalogs/acme-services", + "epoch": 1, + "name": "Acme Services Inc.", + "entriesurl": "https://registry.acme.com/aicatalogs/acme-services/entries", + "entriescount": 2, + "entries": { + "finance-a2a": { + "entryid": "finance-a2a", + "xid": "/aicatalogs/acme-services/entries/finance-a2a", + "name": "Acme Finance A2A Agent", + "contenttype": "application/a2a-agent-card+json", + "labels": { + "ai-catalog.identifier": "urn:acme:agent:finance-a2a" + }, + "entryurl": "https://cards.acme.com/finance/a2a-card.json", + "aicatalog_trustmanifest": { + "issuedAt": "2025-01-01T00:00:00Z", + "subject": { + "mediaType": "application/a2a-agent-card+json", + "digest": "sha256:aaa111..." + }, + "signature": "eyJhbGciOiJFUzI1NiJ9..detached-JWS.." + } + }, + "finance-mcp": { + "entryid": "finance-mcp", + "xid": "/aicatalogs/acme-services/entries/finance-mcp", + "name": "Acme Finance MCP Server", + "contenttype": "application/mcp-server-card+json", + "labels": { + "ai-catalog.identifier": "urn:acme:server:finance-mcp" + }, + "entryurl": "https://cards.acme.com/finance/mcp-server.json" + } + } +} +``` + +## Content Integrity and Signing + +Because xRegistry has no native content-addressing or signature +primitive, this binding does not delegate those invariants: the Trust +Manifest's detached JWS and `subject.digest` remain the source of truth, +verified against the served Resource document during unpacking — the same +model as plain-HTTP distribution. An implementation MAY additionally +expose the digest as an extension attribute for discovery convenience, but +that copy is advisory; the Trust Manifest remains authoritative. + # Mapping to MCP Registry server.json This appendix describes how the MCP Registry `server.json` format diff --git a/specification/examples/ai-catalog.json b/specification/examples/ai-catalog.json index a59b7e1..5d16893 100644 --- a/specification/examples/ai-catalog.json +++ b/specification/examples/ai-catalog.json @@ -16,7 +16,31 @@ "trading" ], "url": "https://api.acme-corp.com/agents/acme-finance-agent.json", - "updatedAt": "2026-02-22T16:30:00Z" + "updatedAt": "2026-02-22T16:30:00Z", + "publisher": { + "identifier": "did:web:acme-corp.com", + "displayName": "Acme Services Inc." + }, + "trustManifest": { + "identity": "urn:example:agent-finance-001", + "identityType": "did", + "attestations": [ + { + "type": "SOC2-Type2", + "uri": "https://trust.acme-corp.com/reports/soc2-2026.pdf", + "mediaType": "application/pdf", + "digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890", + "size": 245760 + } + ], + "subject": { + "url": "https://api.acme-corp.com/agents/acme-finance-agent.json", + "mediaType": "application/a2a-agent-card+json", + "digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "issuedAt": "2026-02-22T16:30:00Z", + "signature": "eyJhbGciOiJFUzI1NiJ9..detached-jws-signature" + } }, { "identifier": "urn:example:data:market-dataset-2026q1", diff --git a/specification/respec-config.json b/specification/respec-config.json index 6e7401b..50c40ea 100644 --- a/specification/respec-config.json +++ b/specification/respec-config.json @@ -2,9 +2,15 @@ "title": "AI Catalog", "shortName": "ai-catalog", "edDraftURI": "https://ai-catalog.io/", - "abstract": "This document defines the AI Catalog, a JSON format for discovering heterogeneous AI artifacts such as MCP servers, A2A agents, Claude Code plugins, datasets, and model cards. Each catalog entry declares the artifact's type via a media type and references or inlines the native artifact metadata, enabling a single discovery mechanism across protocols and platforms. The specification defines three conformance levels — Minimal, Discoverable, and Trusted — allowing implementations to start with a simple list of entries and progressively add host identity, well-known URI discovery, and verifiable trust metadata as needed. An optional Trust Manifest extension provides identity binding, compliance attestations, provenance tracking, and cryptographic signatures without wrapping or modifying the artifact's native format. Informative appendices describe mappings to OCI distribution registries, the MCP Registry server.json format, and the Claude Code Plugins marketplace.", + "abstract": "This document defines the AI Catalog, a JSON format for discovering heterogeneous AI artifacts such as MCP servers, A2A agents, Claude Code plugins, datasets, and model cards. Each catalog entry declares the artifact's type via a media type and references or inlines the native artifact metadata, enabling a single discovery mechanism across protocols and platforms. The specification defines three conformance levels — Minimal, Discoverable, and Trusted — allowing implementations to start with a simple list of entries and progressively add host identity, well-known URI discovery, and verifiable trust metadata as needed. An optional Trust Manifest extension provides identity binding, compliance attestations, provenance tracking, and cryptographic signatures without wrapping or modifying the artifact's native format. Informative appendices define a substrate-neutral distribution-mapping contract and concrete bindings to OCI distribution registries and xRegistry, plus mappings to the MCP Registry server.json format and the Claude Code Plugins marketplace.", "appendixHeaders": ["Data Model", "CDDL", "Example", "Mapping", "Acknowledgment", "Appendix", "IANA"], "localBiblio": { + "xRegistry": { + "title": "xRegistry: A Universal Registry Specification", + "href": "https://github.com/xregistry/spec", + "status": "Draft", + "publisher": "CNCF" + }, "RFC8785": { "title": "JSON Canonicalization Scheme (JCS)", "href": "https://www.rfc-editor.org/rfc/rfc8785", @@ -17,6 +23,18 @@ "status": "RFC", "publisher": "IETF" }, + "RFC7518": { + "title": "JSON Web Algorithms (JWA)", + "href": "https://www.rfc-editor.org/rfc/rfc7518", + "status": "RFC", + "publisher": "IETF" + }, + "RFC8725": { + "title": "JSON Web Token Best Current Practices", + "href": "https://www.rfc-editor.org/rfc/rfc8725", + "status": "BCP 225", + "publisher": "IETF" + }, "RFC6234": { "title": "US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)", "href": "https://www.rfc-editor.org/rfc/rfc6234", From 57be04dd219ab18063090a8ab26ad10f3859c10c Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Wed, 22 Jul 2026 23:52:56 +0200 Subject: [PATCH 2/4] fix(spec): address mindpower review feedback Signed-off-by: Luca Muscariello --- ...g.md => 0017-trust-manifest-artifact-binding.md} | 2 +- ...nifest.md => 0018-substantive-trust-manifest.md} | 13 ++++++------- specification/examples/ai-catalog.json | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) rename adr/{0014-trust-manifest-artifact-binding.md => 0017-trust-manifest-artifact-binding.md} (98%) rename adr/{0015-substantive-trust-manifest.md => 0018-substantive-trust-manifest.md} (83%) diff --git a/adr/0014-trust-manifest-artifact-binding.md b/adr/0017-trust-manifest-artifact-binding.md similarity index 98% rename from adr/0014-trust-manifest-artifact-binding.md rename to adr/0017-trust-manifest-artifact-binding.md index 6fcf03a..4e47f21 100644 --- a/adr/0014-trust-manifest-artifact-binding.md +++ b/adr/0017-trust-manifest-artifact-binding.md @@ -1,4 +1,4 @@ -# ADR-0014: Bind Trust Manifest Signatures to Artifact Content +# ADR-0017: Bind Trust Manifest Signatures to Artifact Content **Status:** Accepted diff --git a/adr/0015-substantive-trust-manifest.md b/adr/0018-substantive-trust-manifest.md similarity index 83% rename from adr/0015-substantive-trust-manifest.md rename to adr/0018-substantive-trust-manifest.md index d917a9f..fe30385 100644 --- a/adr/0015-substantive-trust-manifest.md +++ b/adr/0018-substantive-trust-manifest.md @@ -1,4 +1,4 @@ -# ADR-0015: Require a Substantive Trust Manifest +# ADR-0018: Require a Substantive Trust Manifest **Status:** Accepted @@ -9,13 +9,12 @@ ## Context The Trust Manifest is an OPTIONAL companion to a Catalog Entry. Within -it, however, only `identity` was unconditionally REQUIRED, and `identity` -MUST equal the entry's `identifier`. Every other member — `signature`, +it, however, only `identity` was unconditionally REQUIRED. Every other member — `signature`, `attestations`, `provenance`, `trustSchema`, and the informational fields — was independently OPTIONAL. As a result the smallest valid Trust Manifest was `{ "identity": "..." }`, -which merely restates the entry identifier and carries no trust signal. +which carries no trust signal by itself. Only Level 3 (Trusted Catalog) forced a real payload (`signature` + `subject` + `issuedAt`). Between "no manifest" and Level 3 the format permitted a present-but-empty manifest that looks like trust metadata @@ -36,8 +35,8 @@ member: - a non-empty `provenance` array, or - a `trustSchema`. -`identity` and `identityType` (which restate or describe the entry -identifier) and the informational members `privacyPolicyUrl`, +`identity` and `identityType` (which identify the workload principal) +and the informational members `privacyPolicyUrl`, `termsOfServiceUrl`, and `metadata` do not satisfy this requirement. `subject`, `issuedAt`, and `expiresAt` are not substantive on their own: an unsigned `subject` digest is attacker-settable and unverifiable, so @@ -57,7 +56,7 @@ rule as if no Trust Manifest were present. - The rule is independent of conformance level: even a Level 1 catalog that includes a manifest must make it substantive. Level 3 remains stricter, requiring a `signature` specifically (see - [ADR-0014](0014-trust-manifest-artifact-binding.md)). + [ADR-0017](0017-trust-manifest-artifact-binding.md)). - Unsigned manifests remain valid when they carry attestations, provenance, or a trust schema, preserving low-assurance use cases that do not sign. diff --git a/specification/examples/ai-catalog.json b/specification/examples/ai-catalog.json index e687afc..550219d 100644 --- a/specification/examples/ai-catalog.json +++ b/specification/examples/ai-catalog.json @@ -21,7 +21,7 @@ "displayName": "Acme Services Inc." }, "trustManifest": { - "identity": "urn:example:agent-finance-001", + "identity": "did:web:acme-corp.com", "identityType": "did", "attestations": [ { From a1d25320c1dc8f2a8d174999a46391d42b75b94c Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Thu, 23 Jul 2026 16:05:03 +0200 Subject: [PATCH 3/4] fix(spec): align trust-manifest identity semantics Signed-off-by: Luca Muscariello --- specification/ai-catalog.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specification/ai-catalog.md b/specification/ai-catalog.md index a89d36e..c37b8d4 100644 --- a/specification/ai-catalog.md +++ b/specification/ai-catalog.md @@ -415,14 +415,14 @@ A Trust Manifest MUST contain: identity schemes is open. When a Trust Manifest appears within a Catalog Entry, the `identity` -field MUST match the entry's `identifier` field. This binding ensures trust -claims are unambiguously associated with the catalog artifact. -Consumers MUST reject a Trust Manifest whose `identity` does not -match the containing entry's `identifier`. The `identity` is restated -here — rather than read from the entry's `identifier` — so that it falls -within the signed payload; this is an intentional duplication, not -redundant metadata, and removing it would leave the signature uncommitted -to which identifier the trust claims apply. +field's trust domain MUST align with the publisher domain in the +containing entry's `identifier` field. This binding ensures +trust claims are associated with the authorized publisher namespace even +when `identity` and `identifier` use different URI schemes. +Consumers MUST reject a Trust Manifest whose `identity` domain does not +align with the publisher domain in the containing entry's `identifier`. +The `identity` is carried here so domain binding is part of the signed +payload, rather than inferred only from unsigned entry context. When a Trust Manifest appears on a Host Info object, `identity` SHOULD match the host's `identifier` field when present. @@ -444,8 +444,8 @@ contain at least one *substantive* trust member: - a non-empty `provenance` array, or - a `trustSchema`. -The members `identity` and `identityType` (which restate or describe the -entry identifier) and the informational members `privacyPolicyUrl`, +The members `identity` and `identityType` (which identify the workload +principal) and the informational members `privacyPolicyUrl`, `termsOfServiceUrl`, and `metadata` do NOT satisfy this requirement. `subject`, `issuedAt`, and `expiresAt` are not substantive on their own: an unsigned `subject` digest is attacker-settable and unverifiable, so From 6add452a3f06d6b6216795fd0db78f939881b5f9 Mon Sep 17 00:00:00 2001 From: Luca Muscariello Date: Fri, 31 Jul 2026 11:32:59 +0200 Subject: [PATCH 4/4] docs(adr): bump trust-manifest ADR sequence to 0019 and 0020 Signed-off-by: Luca Muscariello --- ...> 0019-trust-manifest-artifact-binding.md} | 2 +- ....md => 0020-substantive-trust-manifest.md} | 4 +- specification/trust-manifest-threat-model.md | 341 ++++++++++++++++++ 3 files changed, 344 insertions(+), 3 deletions(-) rename adr/{0017-trust-manifest-artifact-binding.md => 0019-trust-manifest-artifact-binding.md} (98%) rename adr/{0018-substantive-trust-manifest.md => 0020-substantive-trust-manifest.md} (95%) create mode 100644 specification/trust-manifest-threat-model.md diff --git a/adr/0017-trust-manifest-artifact-binding.md b/adr/0019-trust-manifest-artifact-binding.md similarity index 98% rename from adr/0017-trust-manifest-artifact-binding.md rename to adr/0019-trust-manifest-artifact-binding.md index 4e47f21..64bc74a 100644 --- a/adr/0017-trust-manifest-artifact-binding.md +++ b/adr/0019-trust-manifest-artifact-binding.md @@ -1,4 +1,4 @@ -# ADR-0017: Bind Trust Manifest Signatures to Artifact Content +# ADR-0019: Bind Trust Manifest Signatures to Artifact Content **Status:** Accepted diff --git a/adr/0018-substantive-trust-manifest.md b/adr/0020-substantive-trust-manifest.md similarity index 95% rename from adr/0018-substantive-trust-manifest.md rename to adr/0020-substantive-trust-manifest.md index fe30385..3ed690e 100644 --- a/adr/0018-substantive-trust-manifest.md +++ b/adr/0020-substantive-trust-manifest.md @@ -1,4 +1,4 @@ -# ADR-0018: Require a Substantive Trust Manifest +# ADR-0020: Require a Substantive Trust Manifest **Status:** Accepted @@ -56,7 +56,7 @@ rule as if no Trust Manifest were present. - The rule is independent of conformance level: even a Level 1 catalog that includes a manifest must make it substantive. Level 3 remains stricter, requiring a `signature` specifically (see - [ADR-0017](0017-trust-manifest-artifact-binding.md)). + [ADR-0019](0019-trust-manifest-artifact-binding.md)). - Unsigned manifests remain valid when they carry attestations, provenance, or a trust schema, preserving low-assurance use cases that do not sign. diff --git a/specification/trust-manifest-threat-model.md b/specification/trust-manifest-threat-model.md new file mode 100644 index 0000000..846e598 --- /dev/null +++ b/specification/trust-manifest-threat-model.md @@ -0,0 +1,341 @@ +# Trust Manifest Threat Model + +**Status:** Informational + +**Date:** 2026-06-20 + +**Methodology:** STRIDE (Spoofing, Tampering, Repudiation, Information +disclosure, Denial of service, Elevation of privilege). + +**Scope:** The optional Trust Manifest extension of the AI Catalog +specification — its data model, signature and verification procedures, +and the conformance levels that depend on it. Catalog mechanisms that +are not trust-bearing (media-type routing, nesting for organization, +version selection) are in scope only where they affect trust decisions. + +This document is the security analysis that motivates the normative +hardening in [ai-catalog.md](ai-catalog.md) and the decisions recorded +in [ADR-0019](../adr/0019-trust-manifest-artifact-binding.md) and +[ADR-0009](../adr/0009-trust-manifest-substitution.md). It exists to +answer the substitution-attack concern raised in ADR-0009: *"The +substitution attack of changing out the trust manifest is very real, +especially if there's no tamper-proofness built in."* + +## 1. System Decomposition + +### 1.1 External entities + +Publisher +: Produces an artifact and (optionally) signs a Trust Manifest that + makes verifiable claims about it. Holds a signing key. + +Catalog Host +: Serves the AI Catalog document. May or may not be the publisher. + Controls the bytes of the catalog at rest and in transit. + +Consumer / Client +: Fetches the catalog, resolves entries, verifies trust metadata, and + makes a trust decision (e.g., install, invoke, or surface an + artifact to a user or agent). The asset being defended is the + *correctness of this decision*. + +Third-party endpoints +: Servers referenced from a Trust Manifest: attestation documents + (`attestation.uri`), key material (DID documents, JWKS URLs, SPIFFE + Workload API), provenance statements (`statementUri`, + `registryUri`). + +OCI registry +: Optional content-addressed distribution channel (Layer 3). + +Attacker +: See threat agents in section 3. + +### 1.2 Data stores + +- Catalog document (`application/ai-catalog+json`) +- Trust Manifest (peer element on an entry or host) +- Artifact bytes (served at `entry.url` or inlined in `entry.data`) +- Attestation documents +- Key material (DID documents, JWK Sets, X.509 SVIDs) +- Provenance statements +- OCI registry content + signatures (Cosign / Notation) + +### 1.3 Processes + +1. Catalog fetch and parse +2. Entry / nested-catalog resolution +3. Trust Manifest signature verification +4. Signer key resolution (from `identity`) +5. Artifact fetch and digest computation +6. Attestation fetch and validation +7. Provenance evaluation + +### 1.4 Data-flow and trust boundaries + +```mermaid +flowchart LR + Pub[Publisher] -->|signs manifest| Cat[(Catalog Document)] + Host[Catalog Host] -->|serves| Cat + Cat -->|B1 TLS| Cons[Consumer] + subgraph Consumer verification + Cons --> Ver[Verify signature] + Ver --> KeyRes[Resolve key] + Cons --> ArtFetch[Fetch + digest artifact] + Cons --> AttFetch[Fetch + validate attestation] + end + KeyRes -->|B4| KeyEP[(Key endpoint: DID/JWKS/SPIFFE)] + ArtFetch -->|B1/B5| Art[(Artifact)] + AttFetch -->|B4/B5| AttEP[(Attestation / provenance endpoints)] + Reg[(OCI registry)] -. B2 alt .-> Cons +``` + +Trust boundaries: + +- **B1 — Transport.** Network path between the consumer and any + server. Crossed by TLS. A network attacker without a valid + certificate cannot read or alter traffic, but TLS says nothing about + the authenticity of the *content* an authorized-but-malicious or + compromised origin returns. +- **B2 — Catalog-document write.** Who can change the bytes of the + catalog document at rest (hosting account, CDN, object store, DNS + control, repository). This is the boundary the substitution attack + crosses. +- **B3 — Publisher signing key.** The boundary between data an + attacker can author and data that requires the publisher's private + key. +- **B4 — Third-party endpoints.** Servers whose URLs appear *inside* + the (possibly attacker-controlled) Trust Manifest. The consumer is + induced to contact them. +- **B5 — Consumer verification egress.** The consumer's own network + egress while fetching manifest-referenced URLs (SSRF surface). + +## 2. Assets + +| # | Asset | Why it matters | +|---|-------|----------------| +| AS1 | Integrity + authenticity of the artifact ↔ trust binding | The whole point of the Trust Manifest: that *these* claims describe *this* artifact. | +| AS2 | Publisher signing keys | Compromise lets an attacker forge authentic-looking trust. | +| AS3 | The consumer's trust decision | The ultimate target — install/invoke a malicious artifact under a trusted label. | +| AS4 | Availability of catalog resolution | Consumers depend on resolving catalogs to find tools. | +| AS5 | Consumer privacy / telemetry | Fetches triggered by verification can leak who is evaluating what, and from where. | + +## 3. Threat Agents and Assumptions + +| ID | Agent | Capability | In scope | +|----|-------|------------|----------| +| A1 | Network attacker | On-path, **cannot** break TLS | Yes (bounded) | +| **A2** | **Catalog-write attacker** | Can modify the catalog document (compromised hosting account, CDN edge, object-store creds, DNS hijack, MITM of a non-TLS origin, or a malicious mirror). **Cannot** obtain the publisher's private key. | **Yes — primary** | +| A3 | Malicious publisher | Authors and signs manifests with a key it legitimately controls | Yes | +| A4 | Compromised third-party endpoint | Controls an attestation/key/provenance URL referenced by a manifest | Yes | +| A5 | Malicious nested/federated author | Authors a sub-catalog that a parent delegates to | Yes | +| A6 | Key-compromise / stale-key attacker | Holds a previously valid (revoked or rotated) publisher key, or replays old signed material | Yes | + +Out of scope: breaking TLS or the underlying hash/signature +primitives; compromise of the consumer's own host; supply-chain +compromise of the artifact *before* the publisher signs it (the +manifest can only attest to what the publisher saw). + +## 4. STRIDE Analysis + +Each threat references the finding IDs (F1–F10) carried in the +companion review and maps to a mitigation in section 6. + +### 4.1 Spoofing + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| S1 | **Self-asserted identity.** Key resolution derives the signer's key *from* the `identity` URI in the manifest. A catalog-write attacker substitutes both `identity` and the key endpoint, signs with their own key, and the signature verifies. Verification proves "signed by whoever owns this identity," not "signed by a publisher I trust." | B2/B3 | A2 | F3 | +| S2 | **Publisher spoofing.** `publisher.identifier`/`displayName` live on the entry, outside any signature; an attacker edits them to impersonate a reputable vendor. | B2 | A2 | F9 | +| S3 | **Host spoofing.** `host.identifier` is attacker-controllable when the catalog is compromised; DID-service-endpoint checks only prove internal consistency of attacker-chosen data. | B2 | A2 | F3 | + +### 4.2 Tampering + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| **T1** | **Artifact substitution under a valid signature (headline).** The `signature` covers only the Trust Manifest JSON. The artifact is referenced by `entry.url`/`data`/`mediaType`, which sit *outside* the signed bytes. The only binding is `identity == entry.identifier`. An attacker keeps the publisher's validly-signed manifest, leaves `identifier` intact, and repoints `url` to a malicious artifact. Signature still verifies. `provenance.sourceDigest` is OPTIONAL and denotes the *source* (`publishedFrom`), not the served artifact, so it does not close the gap. | B2 | A2 | F1 | +| T2 | **Unsigned manifest tampering.** Level 3 ("Trusted Catalog") requires only that a `trustManifest` be *present*, not signed. An attacker freely rewrites every claim in an unsigned manifest. The "Trusted" label implies cryptographic assurance that is not enforced. | B2 | A2 | F2 | +| T3 | **Catalog-level structural tampering.** Nothing signs the `entries` array or `host`. Even with per-entry signed manifests, an attacker injects new (malicious, unsigned) entries, deletes entries, or reorders/selects versions. Only OCI Layer 3 addresses this today. | B2 | A2/A5 | F6 | + +### 4.3 Repudiation + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| R1 | **No issuance time / audit anchor.** A signed manifest carries no `issuedAt`, so a consumer cannot tell *when* a claim was made or which artifact version it covered, weakening dispute resolution and rollback detection. | B3 | A6 | F5 | +| R2 | **Half-specified provenance signatures.** `statementUri` + `signatureRef` exist but have no verification procedure, so provenance statements cannot be relied on as non-repudiable evidence. | B4 | A4 | F8 | + +### 4.4 Information disclosure + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| I1 | **SSRF.** Verification instructs the consumer to fetch attacker-influenced URLs (`attestation.uri`, `statementUri`, `registryUri`, key URLs). A crafted manifest can target internal/loopback/link-local/cloud-metadata addresses from the consumer's network position. | B4/B5 | A2/A4 | F7 | +| I2 | **Verification telemetry / tracking.** Fetches reveal which consumer (IP, identity) is evaluating which artifact, and when. Logo and attestation fetches leak the same. | B1/B5 | A2/A4 | F7 | + +### 4.5 Denial of service + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| D1 | **Oversized / slow verification fetches.** `attestation.size` is OPTIONAL; large attestation documents, large inline `data`, or slow endpoints exhaust consumer memory/connections during verification. | B4/B5 | A2/A4 | F7 | +| D2 | **Nested / circular catalog expansion.** Deep or circular nested catalogs amplify work. (Already addressed by the depth limit and visited-set guidance; retained here for completeness.) | B2 | A2/A5 | — | + +### 4.6 Elevation of privilege + +| ID | Threat | Boundary | Agent | Finding | +|----|--------|----------|-------|---------| +| E1 | **Signature-algorithm confusion.** The spec mandates detached JWS but constrains no algorithms. Acceptance of `alg: none` or symmetric-vs-asymmetric key confusion (using a public key as an HMAC secret) lets an attacker forge a "valid" signature with no private key. | B3 | A2 | F4 | +| E2 | **Rollback / downgrade.** With no freshness binding and multi-version entries, an attacker replays an older, validly-signed manifest plus its older (e.g., since-patched) artifact, or a manifest signed before a key was revoked. The consumer accepts authentic-but-stale trust. | B2/B3 | A6 | F5 | + +## 5. Attack Scenarios + +**SC-1 — URL swap under a valid signature (primary).** Acme publishes +`urn:acme:agent:finance` with a manifest signed by +`did:web:acme-corp.com`. An attacker who compromises Acme's CDN leaves +the manifest byte-for-byte intact (so the signature and +`identity == identifier` checks pass) and changes only `entry.url` to a +look-alike host serving a trojaned agent. A Layer-2 consumer "verifies +the signature," sees green, and installs malware. *Closed by F1 +mitigation: the signed manifest MUST commit to the artifact digest.* + +**SC-2 — False trust at Level 3.** A catalog advertises Level 3 with +unsigned manifests. A consumer treats "Trusted Catalog" as +cryptographic assurance and accepts attacker-rewritten attestations. +*Closed by F2 mitigation: signatures REQUIRED at Level 3.* + +**SC-3 — Self-signed substitution.** Attacker replaces `identity` with +`did:web:attacker.example`, repoints the key endpoint to their own +JWKS, and signs the whole (malicious) manifest. Every internal check +passes. *Closed by F3 mitigation: out-of-band trust anchoring — +verified signature ≠ trusted publisher.* + +**SC-4 — alg:none forgery.** Attacker sets the JWS header `alg` to +`none` (or `HS256` keyed with the publisher's public key) and forges a +manifest with no private key. *Closed by F4 mitigation: algorithm +allowlist.* + +**SC-5 — Downgrade.** A patched v2.1 exists, but the attacker re-serves +the still-validly-signed v2.0 manifest + artifact. *Closed by F5 +mitigation: `issuedAt`/`expiresAt` + anti-rollback guidance.* + +**SC-6 — Verification SSRF.** A manifest sets +`attestation.uri = http://169.254.169.254/latest/meta-data/…`; the +consumer's verifier fetches it and exfiltrates cloud credentials. +*Closed by F7 mitigation: safe-fetching rules.* + +**SC-7 — Entry injection.** Attacker appends an unsigned malicious +entry to a catalog full of legitimately signed entries. *Mitigated by +F6: catalog-level signature and/or OCI content-addressing.* + +## 6. Controls: Existing vs. Proposed + +| Finding | Threats | Existing control | Proposed normative mitigation | Spec section | +|---------|---------|------------------|-------------------------------|--------------| +| F1 | T1 | Detached JWS over manifest; OPTIONAL `sourceDigest` | Signed `subject` `{url?, mediaType, digest}` committing to the served artifact; REQUIRED whenever signed | Trust Manifest → Subject Binding | +| F2 | T2 | Level 3 requires manifest presence | Level 3 MUST carry a signed manifest with subject binding + `issuedAt` | Conformance Level 3 | +| F3 | S1, S3 | Key resolution from `identity` | Trust Anchoring subsection: verified signature ≠ trusted publisher; anchor identity to an out-of-band root | Verification → Trust Anchoring | +| F4 | E1 | "detached JWS" | Algorithm allowlist; reject `alg:none` + symmetric; validate `alg`, pin `kid` | Verification → Signature Algorithms | +| F5 | R1, E2 | none | `issuedAt` (REQUIRED when signed) + `expiresAt`; anti-rollback + revocation guidance | Trust Manifest + Verification | +| F6 | T3 | OCI Layer 3 (informative) | OPTIONAL catalog-level `signature` (RECOMMENDED at L3) + OCI reference | Catalog signature + Security Considerations | +| F7 | I1, I2, D1 | none | Safe-Fetching subsection: size caps, timeouts, no redirects to private ranges, host allowlist | Verification → Safe Fetching | +| F8 | R2 | Fields only | Provenance-statement verification procedure | Verification → Provenance statements | +| F9 | S2 | `identity == identifier` | Bind/flag publisher fields; advisory unless covered by attestation | Verification → Publisher identity | +| F10 | — | JCS | Note JCS numeric round-trip caveat for signed payloads | Verification → Signatures | + +## 7. Comparison with the Sigstore Architecture + +The Trust Manifest derives its model from Sigstore — sign an artifact, +associate it with an identity, and make the evidence verifiable — but it +reproduces only part of Sigstore's guarantees natively. Sigstore secures +software supply chains with three cooperating roles: + +- **Cosign** signs the artifact's *digest* (not just metadata). +- **Fulcio** is a certificate authority that binds an ephemeral signing + key to an OIDC identity and issues a short-lived certificate, so + identity is asserted by a *trusted CA* rather than self-declared. +- **Rekor** is an immutable, append-only **transparency log** that + witnesses each signing event (digest + signature + certificate), + giving public auditability, non-repudiation, freshness, monitoring, + and rollback detection (inclusion and consistency proofs). +- A **TUF**-managed root of trust distributes and rotates the Fulcio and + Rekor public keys. + +Verifying a Sigstore artifact means: verify the signature with the +certificate's public key; confirm the certificate identity matches an +*expected* identity; verify the certificate against Sigstore's root of +trust; and verify proof of inclusion in Rekor. The artifact is thereby +proven to come from its expected source and to be untampered. + +### 7.1 Mapping + +| Sigstore property | Provides | Trust Manifest today | Gap (finding) | +|-------------------|----------|----------------------|---------------| +| Cosign signs the artifact **digest** | Artifact ↔ signature binding | `subject.digest` in the signed payload (this revision) | Closed (F1) | +| **Fulcio** CA binds identity via OIDC; short-lived cert | Identity is CA-attested, not self-asserted | `identity` is self-declared; key resolved *from* it; anchoring pushed to the consumer | Partially open (F3) — anchoring is required but no CA is specified | +| **Rekor** transparency log | Non-repudiation, freshness, monitoring, rollback detection | No transparency-log equivalent; `issuedAt`/`expiresAt` give weak local freshness only | Open (F5, R1, E2) | +| **Keyless / ephemeral keys** | No long-lived key management or revocation problem | Long-lived publisher keys (DID/JWKS); inherits key-management + revocation burden | Open (residual AS2) | +| **TUF** root of trust | Secure distribution + rotation of verification keys | Trust-anchor bootstrapping unspecified | Open (residual) | +| Verify expected identity + cert chain + Rekor inclusion | Full verification chain | Verify signature + `subject` + out-of-band anchor; no inclusion proof | Partially open | + +### 7.2 Implications + +- **What the Trust Manifest now matches.** With `subject` binding, the + Trust Manifest reproduces Cosign's core property: a signature commits + to a specific artifact digest. This is the single most important + Sigstore guarantee and directly closes the substitution attack (T1). +- **What it delegates.** The Trust Manifest deliberately does not + operate a CA or a transparency log. It is a *format that can carry* + Sigstore-style evidence rather than a replacement for Sigstore + services. Consequently it leans on out-of-band **trust anchoring** + (section 4.1 / F3) to substitute for Fulcio's CA-attested identity. +- **What is still weaker than Sigstore.** Without a Rekor-equivalent, + the Trust Manifest cannot offer public auditability, third-party + witnessing, or strong rollback detection; `issuedAt`/`expiresAt` are a + local, unwitnessed approximation. Long-lived publisher keys reintroduce + the key-management and revocation problems Sigstore was designed to + eliminate. + +### 7.3 Recommended convergence + +To close the remaining gaps, the specification SHOULD make Sigstore +evidence first-class rather than reinventing it: + +1. **Carry Fulcio/Cosign evidence.** Define an attestation `type` (e.g., + `sigstore-bundle`) whose document is a Sigstore bundle (certificate + + signature + Rekor inclusion proof). Verifying it gives CA-attested + identity and log inclusion "for free," and the `subject.digest` + already aligns with what Cosign signs. +2. **Anchor via the Sigstore/TUF root.** Permit the trust anchor + (section 4.1) to be Sigstore's TUF-managed root, so identity is + verified against Fulcio rather than a hand-maintained allowlist. +3. **Prefer transparency-log inclusion over local freshness.** Where a + Rekor (or compatible) inclusion proof is available, consumers SHOULD + prefer it to `issuedAt`/`expiresAt` for freshness, non-repudiation, + and rollback detection. +4. **Favor keyless/ephemeral signing.** Treat long-lived publisher keys + as the fallback, not the default, to avoid the revocation burden. + +This keeps the Trust Manifest's artifact-agnostic, peer-element design +while letting trust-sensitive deployments inherit Sigstore's full chain +(CA identity + transparency + keyless) instead of a weaker re-implementation. + +## 8. Residual Risks + +- **Trust-anchor bootstrapping.** Anchoring shifts trust to an + out-of-band root (pinned allowlist, registry vetting, DID method with + domain control). The strength of the whole system reduces to how that + root is established and maintained; the spec can require anchoring but + cannot supply the root. +- **Publisher-key compromise (AS2).** A subject-bound, signed manifest + is only as trustworthy as the publisher's key hygiene. Short-lived + keys, revocation checking, and OCI/Cosign counter-signatures reduce + but do not eliminate this. +- **Pre-signing supply-chain compromise.** If a malicious artifact is + signed by a legitimate publisher, the manifest faithfully attests to + a bad artifact. Out of scope here; addressed by build-provenance + (SLSA) practices upstream. +- **Catalog availability (AS4).** Even with full integrity, an attacker + who can delete the catalog or block resolution causes denial of + service; mitigated operationally (caching, mirrors, OCI), not by the + trust model. +- **Metadata privacy (AS5).** Verification fetches inherently reveal + some consumer activity; Data-URI attestations/logos and host + allowlists reduce, but cannot fully remove, this exposure.