Skip to content

Support independently signed Trust Manifest contributions - #117

Open
jonathanhefner wants to merge 3 commits into
Agent-Card:mainfrom
jonathanhefner:agent/identity-keyed-trust-manifests
Open

jonathanhefner wants to merge 3 commits into
Agent-Card:mainfrom
jonathanhefner:agent/identity-keyed-trust-manifests

Conversation

@jonathanhefner

@jonathanhefner jonathanhefner commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Allow publishers, assessors, and catalog operators to contribute independently signed trust metadata about the same artifact.

Area Proposed change
Contributions Replace entry.trustManifest with a trustManifests array, each identifying its contributor
Signatures Use a Signature object containing the signer, verification profile, timestamps, and detached JWS
Artifact integrity Add entry.digest, mirrored by each signed manifest’s subject.digest
Policy links Move privacy and terms URLs into the official policy-URL extension

The schema, guides, examples, mappings, and threat model are updated together.

Motivation

A publisher should be able to provide provenance while an assessor supplies independent audit evidence. Either should be able to update its contribution without requiring the other to sign again.

Shared artifact metadata, such as policy URLs, also needs one predictable location rather than competing values inside individual contributors’ manifests.

Rationale

Each manifest carries its own artifact subject and signature, making it independently verifiable and portable between catalogs. Repeating release coordinates introduces matching rules, but gives each signature a fixed scope.

Whole-catalog signatures separately authenticate the complete snapshot, including Host Info and nested manifests. Multiple signers per manifest and inter-manifest references remain deferred.

See Independent Trust Manifests and Whole-Object Signatures and Define the Policy URLs Extension for details.

Follow-up work

The following draft PRs are stacked in order, targeting jonathanhefner/independent-trust-manifests with cumulative follow-up diffs:

  1. Require JCS-compatible catalog data
  2. Define the did:web Catalog Profile
  3. Allow HTTPS URL binding in signed Trust Manifests
  4. Authenticate selected entry extensions through digests

They will be rebased as the preceding PRs merge.

| Existing location | Proposed location |
| --- | --- |
| `trustManifest.identity` | Key in `entry.trustManifests` |
| `trustManifest.identity` | Contributor key in `entry.trustManifests`; endorsement identity in `entry.signatures[].signer` |

@muscariello muscariello Sep 17, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One gap: a future signer profile could accept the same kind of signer value as did:web.

A did:web:acme.com:... identity and the plain URL https://acme.com/... refer to the same underlying resource. So imagine a second profile gets added later — say, a plain JWKS-over-HTTPS profile that just fetches a key list from a URL, no extra authorization check. If a verifier can't tell the two profiles apart from the signer value alone, it might apply the JWKS profile's rules to a signer that was actually meant to go through did:web. That matters because the JWKS profile would accept any key found at that URL, while did:web only accepts a key the DID document specifically lists under assertionMethod. Applying the JWKS profile by mistake means skipping that check entirely, not just using a different lookup mechanism.

To fix that, add an explicit profile field to the Signature object, signed the same way signer is today. Same paths/issuedAt/jws shape either way — only signer and profile differ:

ex:

{
"signer": "did:web:acme-corp.com:agent:finance",
"profile": "did:web-v1",
"paths": [["identifier"], ["type"], ["digest"], ["trustManifests", "did:web:acme-corp.com:agent:finance"]],
"issuedAt": "2026-09-13T00:00:00Z",
"jws": "..."
}

{
"signer": "https://acme.com/certs/jwks.json",
"profile": "https-jwks-v1",
"paths": [["identifier"], ["type"], ["digest"], ["trustManifests", "https://acme.com/certs/jwks.json"]],
"issuedAt": "2026-09-13T00:00:00Z",
"jws": "..."
}

A verifier picks the profile by reading profile directly, not by guessing from what signer looks like — and since it's part of the signed payload, it can't be changed after the fact. Small addition today, much harder to bolt on safely once a second profile actually exists.

@muscariello muscariello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See inline

@muscariello

Copy link
Copy Markdown
Member

I would like to share how I would use this to also include SPIRE/SPIFFE not as workload identity manager but as a signing identity:

spiffe-x509-v1 profile could work like this:

signer = the SPIFFE ID (spiffe://acme.com/ns/finance/sa/finance-agent).

Verification fetches the trust domain's bundle endpoint (live, same as did:web resolves a DID document live) to get the current root/intermediate CAs — this part rotates rarely, same cadence as a DID document.

The actual short-lived leaf SVID that did the signing doesn't need re-resolving at all — it rides along in the JWS's own protected header via the standard x5c parameter (RFC 7515's mechanism for embedding a certificate chain directly in a JWS), so no new Signature-object field is needed.

Verification checks:
the x5c chain is cryptographically valid and terminates at one of the currently fetched trust-domain roots, the leaf cert's SAN contains the claimed SPIFFE ID, and issuedAt falls inside the leaf cert's own validity window —
"was this valid then," not "is it valid now." That's the piece that actually accounts for SVIDs being short-lived: you don't need the leaf to still be valid at verification time, you need proof it was valid at signing time, exactly like a timestamped code-signing certificate that outlives its own cert's expiry.
It's equivalent to sigstore.

@muscariello

Copy link
Copy Markdown
Member

did:web-v1
{
"signer": "did:web:acme-corp.com:agent:finance",
"profile": "did:web-v1",
"paths": [["identifier"], ["type"], ["digest"], ["trustManifests", "did:web:acme-corp.com:agent:finance"]],
"issuedAt": "2026-09-13T00:00:00Z",
"jws": "..."
}

https-jwks-v1
{
"signer": "https://acme.com/certs/jwks.json",
"profile": "https-jwks-v1",
"paths": [["identifier"], ["type"], ["digest"], ["trustManifests", "https://acme.com/certs/jwks.json"]],
"issuedAt": "2026-09-13T00:00:00Z",
"jws": "..."
}

spiffe-x509-v1
{
"signer": "spiffe://acme.com/ns/finance/sa/finance-agent",
"profile": "spiffe-x509-v1",
"paths": [["identifier"], ["type"], ["digest"], ["trustManifests", "spiffe://acme.com/ns/finance/sa/finance-agent"]],
"issuedAt": "2026-09-13T00:00:00Z",
"jws": "..."
}

@jonathanhefner

Copy link
Copy Markdown
Collaborator Author

@muscariello Thank you for the review!

I've pushed 51df07b, which adds a profile field to signature objects.

Comment thread specification/examples/ai-catalog.json Outdated
]
}
},
"signatures": [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The idea of grouping claims by contributor (trustManifests) is great for downstream evaluations. However, moving signatures to a top-level entry.signatures: [...] array with 2D JSON path selectors introduces substantial verification fragility, custom path-walking parsers across all SDKs, and breaks the 'self-contained envelope' principle.

Keeping signature and subject inside each trustManifest keeps manifests transport-portable, and avoids custom JSON path-walking engines."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Path selectors allow targeted signing. Without that we would need to:

custom JSON path-walking engines

I feel like that sounds a lot more complex than it actually is. 😅 Here is an example implementation:

function buildPayload(paths: string[][], entry: JsonObject) {
  return paths
    .toSorted(comparePaths)
    .map(path => [path, extractValue(path, entry)] as const);
}

function comparePaths(a: string[], b: string[]): number {
  for (let i = 0; i < Math.min(a.length, b.length); i++) {
    if (a[i] < b[i]) return -1;
    if (a[i] > b[i]) return 1;
  }
  return a.length - b.length;
}

function extractValue(path: string[], entry: JsonObject): JsonValue {
  if (path[0] === "signatures") {
    throw new Error("Selecting top-level signatures is forbidden");
  }

  let value: JsonValue = entry;

  for (const key of path) {
    if (!isJsonObject(value) || !Object.hasOwn(value, key)) {
      throw new Error(`Cannot resolve path: ${JSON.stringify(path)}`);
    }
    value = value[key];
  }

  return value;
}

function isJsonObject(value: JsonValue): value is JsonObject {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

If we want to simplify even further, we can drop the sorting requirement and allow signatures to be included in a path (though no one should want to).

If you're strongly opposed to selector paths, though, I can explore less generic approaches.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

An example of a less general approach would be to replace paths with:

  • implied entry.identifier, entry.type, entry.version if present, entry.digest if present
  • optional signatures[].additionalSubjectFields array of entry fields to include in an ephemeral (generated) subject
  • optional signatures[].subjectExtensions array of extension keys to include in an ephemeral subject
  • optional signatures[].subjectTrustManifests array of Trust Manifest contributor identities to include in an ephemeral subject

Of course, names can be tweaked -- e.g., signatures[].additionalEntryFields or signatures[].subjectDefinition.entryFields or whatever.

(By the way, even if we decide to keep paths, we can make the inclusion of entry.identifier et al implied.)

I still favor keeping signatures as a peer of trustManifests though. Putting signatures inside a Trust Manifest makes it a bit more awkward to generate the payload to sign (because signatures would have to be removed from each Trust Manifest before signing).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @jonathanhefner that getting signatures in the payload is ugly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

If we really want all trust-related data contained in a single field, I would recommend to:

  • rename trustManifests to trustContributions or just contributions
  • make a newly defined Trust Manifest object (entry.trustManifest) that would contain contributions and signatures
  • possibly move privacyPolicyUrl and termsOfServiceUrl from directly under entry back into individual contributions objects
    • this would partly be based on who we expect to contribute these values, but also partly based on how those fields are referenced to build an ephemeral subject
  • keep digest directly under entry

Altogether, an entry could look like:

{
  "identifier": "urn:air:acme.com:agent:finance-a2a",
  "type": "application/a2a-agent-card+json",
  "version": "2.1.0",
  "digest": "sha256:...",
  "url": "..."
  "publisher": {
    "identifier": "did:web:acme.com",
    "displayName": "Acme Financial Corp"
  },
  "extensions": {
    "https://ai-catalog.org/extensions/metadata": {
      "foo": "bar",
    }
  },
  "trustManifest": {
    "contributions": {
      "did:web:acme.com": {
        "privacyPolicyUrl": "https://acme.com/legal/privacy",
        "termsOfServiceUrl": "https://acme.com/legal/terms",
        "attestations": [
          {
            "type": "SOC2-Type2",
            "uri": "https://trust.acme.com/reports/soc2.pdf",
            "digest": "sha256:..."
          }
        ]
      }
    },
    "signatures": [
      {
        "signer": "did:web:acme.com",
        "profile": "did-web-v1",
        "subjectAdditionalFields": ["url"],
        "subjectExtensions": ["https://ai-catalog.org/extensions/metadata"],
        "subjectTrustContributions": ["did:web:acme.com"],
        "issuedAt": "2026-03-20T14:00:00Z",
        "jws": "..."
      }
    ]
  }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Regarding the latest splitting trustManfiest into trustContribution and signatures:

The good:

  1. Moving away from arbitrary 2D JSON paths (paths: string[][]).
  2. Establishing identifier, type, version, and digest as non-negotiable core fields.
  3. Consolidating everything under entry.trustManifest with contributions and signatures (Comment Welcome to ai-card Discussions! #2). This brings much-needed structural cohesion to the catalog entry.

However, the idea of an ephemeral (dynamically synthesized) subject via subjectAdditionalFields, subjectExtensions, and subjectTrustContributions still introduces serious security and interoperability issues: (similar to the 2D json path)

  1. The Selective Signing / Truncation Vulnerability (Confused Deputy)
    If a signer only binds to an ephemeral subset of fields:
  • What prevents an untrusted intermediary or rogue registry from modifying unselected fields (e.g. altering publisher.displayName, changing url, or injecting an unauthorized extension)?
  • The signature over the ephemeral subject would still verify as 100% valid! A consumer checking the signature would see a valid signature from the publisher and falsely assume the entire entry is authentic.
  1. Verifier Policy and Cross-Language Drift
    In this model, every signature in signatures can have a different recipe for what it chose to include or omit. Verifiers in Python, Go, Rust, and TypeScript would not only have to implement complex plucking-and-reassembly logic, but consuming applications would have to inspect each signature's recipe to determine if critical fields were left unsigned.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Basically, with path selectors and ephemeral subjects, we are trying to re-invent an ad-hoc JSON path-walking engine.

As discussed earlier, while the TypeScript prototype snippet looks deceptively simple, from a security and standards engineering standpoint, it introduces critical vulnerabilities.

The latest code example is largely moving in the right direction, except that instead of manual field selection, we should sign the whole entry and the whole contribution respectively.

It would look like this:

{
  "identifier": "urn:air:acme.com:agent:finance-a2a",
  "version": "2.1.0",
  "digest": "sha256:7f83b1657ff...",
  "url": "https://agents.acme.com/finance",
  "publisher": { "identifier": "did:web:acme.com" },
  "extensions": { "foo": "bar" },

  "signature": {
    "signer": "did:web:acme.com",
    "jws": "<publisher-signature-over-entry>"
  },

  "trustManifests": [
    {
      "contributor": "did:web:acme.com",
      "subject": { "digest": "sha256:7f83b1657ff..." },
      "privacyPolicyUrl": "https://acme.com/legal/privacy",
      "attestations": [ ... ],
      "signature": {
        "signer": "did:web:acme.com",
        "jws": "<signature-over-acme-manifest>"
      }
    },
    {
      "contributor": "did:web:cisco.com",
      "subject": { "digest": "sha256:7f83b1657ff..." },
      "attestations": [
        {
          "type": "SecurityPostureAudit",
          "score": "A+"
        }
      ],
      "signature": {
        "signer": "did:web:cisco.com",
        "jws": "<signature-over-cisco-manifest>"
      }
    }
  ]
}

@jonathanhefner jonathanhefner Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

1. Selective Signing / Truncation Vulnerability:
When a signature only commits to specific selected paths (e.g. [["identifier"], ["version"]]), any untrusted registry or MITM can inject arbitrary unselected fields (e.g. malicious endpoints or permissions) into the entry, and the signature will still validate cleanly. The verifier has no cryptographic guarantee over what was deliberately omitted versus what was maliciously injected.

1. The Selective Signing / Truncation Vulnerability (Confused Deputy)
If a signer only binds to an ephemeral subset of fields:

  • What prevents an untrusted intermediary or rogue registry from modifying unselected fields (e.g. altering publisher.displayName, changing url, or injecting an unauthorized extension)?
  • The signature over the ephemeral subject would still verify as 100% valid! A consumer checking the signature would see a valid signature from the publisher and falsely assume the entire entry is authentic.

That also applies to what we have now, no? Isn't that just the nature of (intentionally) signing a subset of values?


2. Cross-Language Canonicalization & Interop (RFC 8785):
Converting an in-memory tuple list [[path, value], ...] into deterministic, byte-for-byte signing bytes requires strict canonicalization across all SDKs (Python, Go, Rust, TypeScript). Array indexing, missing keys, and path overlap handling will create spec divergence and parsing vulnerabilities.

I'm not sure I understand the point this is trying to make.

The same applies to JSON objects (perhaps even moreso). We also already have arrays that we are signing, such as attestations.


3. The Role of subject (Cryptographic Digest vs. Data Duplication):
Jonathan raised valid concerns about needing to duplicate entry fields in subject or constantly updating the subject schema when new fields (like version or extensions) are added.
In established supply-chain standards (such as in-toto Statements and SLSA), the subject is never a duplicate copy of entry fields. It is simply an immutable cryptographic digest

Producing a an immutable cryptographic digest requires an ephemeral structure to hash...

If we're going define and materialize an ephemeral structure such that it could be hashed, why not just include the structure directly in the signing payload?

In any case, what is the definition of that structure?


2. Verifier Policy and Cross-Language Drift
In this model, every signature in signatures can have a different recipe for what it chose to include or omit. Verifiers in Python, Go, Rust, and TypeScript would not only have to implement complex plucking-and-reassembly logic, but consuming applications would have to inspect each signature's recipe to determine if critical fields were left unsigned.

Yes, that is by design. Different signers may sign different things -- e.g., signing the URL vs not, signing different trust manifests, signing different extensions.


The latest code example is largely moving in the right direction, except that instead of manual field selection, we should sign the whole entry and the whole contribution respectively.

It would look like this:

@mindpower Signing the whole entry means extensions cannot be added and url cannot be unsigned / changed for mirrors.

Therefore, I think you're saying "sign a slice of the entry". In which case, what is the slice and do we allow variations from it (such as additional fields)?

@mindpower mindpower Sep 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That also applies to what we have now, no? Isn't that just the nature of (intentionally) signing a subset of values?

That's different. The publisher simply signs the entry data (the entry object excluding signature and trustManifests). When a publisher publishes an entry, they stand behind that complete definition. Allowing arbitrary slicing is what creates confused-deputy risks.

Different signers may sign different things

Agreed, but they should sign different self-contained objects (Acme signs the entry; Cisco signs Cisco’s contribution), not arbitrary slices. Also url should not be mutable. If an intermediary or mirror can rewrite url without invalidating the publisher's signature, an attacker can redirect users to a malicious endpoint while keeping Acme’s valid signature.

@jonathanhefner jonathanhefner Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's different. The publisher simply signs the entry data (the entry object excluding signature and trustManifests). When a publisher publishes an entry, they stand behind that complete definition. Allowing arbitrary slicing is what creates confused-deputy risks.

To reiterate my clarification from the meeting: my follow-up proposal in #117 (comment) did not allow completely arbitrary slicing. All signatures would include the same core entry fields that the publisher signs.

Agreed, but they should sign different self-contained objects (Acme signs the entry; Cisco signs Cisco’s contribution), not arbitrary slices. Also url should not be mutable. If an intermediary or mirror can rewrite url without invalidating the publisher's signature, an attacker can redirect users to a malicious endpoint while keeping Acme’s valid signature.

Also reiterating from the meeting: entry.digest protects the artifact while allowing mirror URLs. The spec currently allows that even without this PR. If we want to disallow mirror URLs, it would be an additional behavioral change on top of what this PR is proposing.

However, I understand your preference for self-contained objects. During the meeting, @muscariello also made the point that when there are multiple signers, they often sign the exact same (immutable) object.

Taking all of the above into account, I think the following structure is closer to what you're asking for:

{
  "identifier": "urn:air:acme.com:agent:finance-a2a",
  "type": "application/a2a-agent-card+json",
  "version": "2.1.0",
  "digest": "sha256:...",
  "url": "...",
  "publisher": {
    "identifier": "did:web:acme.com",
    "displayName": "Acme Financial Corp"
  },
  "extensions": {
    "https://ai-catalog.org/extensions/metadata": {
      "foo": "bar",
    }
  },

  "trustManifests": [
    {
      "contributor": "did:web:acme.com",
      "privacyPolicyUrl": "https://acme.com/legal/privacy",
      "termsOfServiceUrl": "https://acme.com/legal/terms",
      "attestations": [
        {
          "type": "SOC2-Type2",
          "uri": "https://trust.acme.com/reports/soc2.pdf",
          "digest": "sha256:..."
        }
      ],

      // `canonicalUrl` would be semantically similar to `entry.url` but not
      // required to exactly match. It would merely communicate the artifact
      // URL that the Trust Manifest contributor endorses.
      //
      // **THIS FIELD COULD BE DEFINED IN V1.1 OF THE SPEC**
      "canonicalUrl": "...",

      // `supportedExtensions` lists the extensions (by key) that the Trust
      // Manifest contributor supports. If those extensions are present in the
      // entry, then their values MUST be included in the signature payload -- i.e.,
      // anyone signing this Trust Manifest must also sign exactly those extensions.
      //
      // **THIS FIELD COULD BE DEFINED IN V1.1 OF THE SPEC**
      "supportedExtensions": ["https://ai-catalog.org/extensions/metadata"],

      "signatures": [
        {
          "signer": "did:web:acme.com",
          "profile": "did-web-v1",
          "issuedAt": "2026-03-20T14:00:00Z",

          // The actual JWS signature covers:
          // - `entry.identifier`, `entry.type`, `entry.digest`, and `entry.version` if present
          // - the present `entry.extensions` values listed in `supportedExtensions`
          // - the containing Trust Manifest object minus the `signatures` field
          // - this Signature object minus the `jws` field
          "jws": "..."
        }
      ]
    }
  ]
}

It does put signatures back inside the Trust Manifest, which I think is a bit awkward. If we don't like that, then we can make signatures a peer of trustManifests (or trustManifest.contributions), and each signature object could contain a single Trust Manifest key.

Also note that if we want to leave the canonicalUrl and supportedExtensions fields out of v1 of the spec, then we can do so without painting ourselves into a corner (assuming we think they are at least directionally correct).

Comment thread docs/mappings/xregistry.md Outdated

@muscariello muscariello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @jonathanhefner — the design looks good to me. Four small inline comments below. Proposed path to merge quickly:

  1. Merge #108 and #109 (both approved).
  2. Mark #110 ready for review, approve, and merge.
  3. Rebase this PR onto main and fold in the inline comments:
    • Before merge: duplicate paths, and signer must exactly equal the manifest key (one sentence each).
    • Before merge if you agree, otherwise a follow-up issue: root ["entries"] should exclude contributions, so aggregation never requires re-signing.
    • Follow-up issue is fine: I-JSON requirement for inline data digests.
  4. Approve and merge this PR.

Comment thread specification/ai-catalog.md Outdated
Resolution of a missing member MUST fail; a present JSON `null` is a value,
not absence. A path whose first segment is `"signatures"` MUST be rejected,
preventing selection of the containing signature array. Nested signatures
inside a selected value MUST be included unchanged; implementations MUST NOT

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Selecting ["entries"] at the root covers every entry's signatures and trustManifests, so any new contribution breaks the catalog signature. Suggest leaving those two members out of each entry when resolving ["entries"] for a root signature, so aggregation never needs re-signing.

Comment thread specification/ai-catalog.md Outdated
recursively remove signatures. For example, a root selection of `entries`
includes entry signatures and any signatures in embedded catalogs.

For each path, form the pair `[path, resolvedValue]`. Sort the pairs by path,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please forbid duplicate paths. Otherwise a verifier that dedupes builds a different payload than one that doesn't.

Comment thread specification/ai-catalog.md
Comment thread specification/ai-catalog.md Outdated
@darrelmiller darrelmiller added this to the 0.9 RC release milestone Oct 1, 2026
@kenneives

Copy link
Copy Markdown

Reviewing as a third-party assessor: AgentAvow issues signed safety verdicts about other people's MCP servers, so the contributor-keyed trust manifest in this PR is the slot we would publish into. Three things from implementing it against the current text, then six test cases I can contribute as fixtures.

  1. State that the comparison between a signature's signer and the trustManifests key is byte-exact, case-sensitive, no URI normalization. The text says keys are preserved exactly but never says the match rule.
  2. Duplicate entries in paths. A verifier that dedupes builds a different payload than one that does not. Suggest MUST reject duplicates.
  3. For live-protocol entries the digest binds the server card bytes, not what the server serves. An assessor endorsement of such an entry says nothing about runtime unless the manifest carries its own binding to the served tool definitions. Worth a sentence so readers do not infer more than the signature covers.

Fixtures I can contribute, each one line: trustManifests key with a case or trailing-dot variant of the signer must fail; kid listed under authentication only must be rejected; signer A with a kid under DID B that verifies cryptographically must be rejected; a valid signature past expiresAt is not an endorsement, and expiresAt before issuedAt is structurally invalid; an operator adding version after an assessor signed an unversioned entry breaks release coverage; a relabelled profile on the same jws fails and must not fall back to did-web-v1. Plus a positive control: reordered paths and reordered manifest members must still verify.

One question: only did-web-v1 is reserved and it is ES256 only, while the listed Python reference signer is EdDSA. Is a reserved EdDSA profile in scope, or should EdDSA signers define a URI profile?

@frmoretto

Copy link
Copy Markdown

On ADR-0033 (policy URLs): an agent acting for a buyer needs to know which terms applied when it relied on an entry.
As the ADR notes, signing the URL doesn't establish that.
Could the extension carry an optional digest of each document (termsOfServiceDigest, privacyPolicyDigest), taken when the entry is signed?
Same pattern as the policy digest in #55.

Francesco Marinoni Moretto

@kenneives

Copy link
Copy Markdown

The six fixtures from my review are now written against the 10-05 shape and opened against this PR's branch so they can fold in: jonathanhefner#19. Fifteen cases plus a reordered positive control, each carrying the catalog, the DID documents a verifier would resolve, the verification instant and the expected rejection code, with a premise showing the JWS itself verifies wherever the rejection is a rule rather than a bad signature. Standard library only, nothing added to pyproject or the lockfile. Two of my earlier points are resolved by the revision: duplicate paths no longer exist, and signer-to-contributor equality is now stated byte-exact. The EdDSA profile question stands.

AI usage disclosure: the fixtures were prepared with AI assistance under my direct supervision; this comment is mine.

Give publishers, assessors, and registries separate contributor manifests
with one signature and a signed artifact subject. Authenticate signer,
profile, and timestamps through whole-manifest JWS signatures, and sign
complete catalog snapshots separately.

Add the entry artifact digest, require matching release coordinates,
and reserve additional signatures without assigning them semantics.
Update the schema, guides, mappings, examples, and threat model together.
Consolidate the superseded selected-field implementation history into
this self-contained redesign.

Signed-off-by: Jonathan Hefner <jonathan@hefner.pro>
Give privacy and terms links one official entry extension that publishers
or catalog operators can populate. Preserve their existing meanings and
URL requirements while removing the former core entry fields.

Keep contributor authentication of entry extensions as separate work.
Whole-catalog signatures cover these values as snapshot contents, with
signer authorization evaluated separately.

Signed-off-by: Jonathan Hefner <jonathan@hefner.pro>
@jonathanhefner
jonathanhefner force-pushed the agent/identity-keyed-trust-manifests branch from 00f1a05 to 99d8bfd Compare October 6, 2026 19:46
@jonathanhefner jonathanhefner changed the title Group trust metadata by contributor and generalize signatures Support independently signed Trust Manifest contributions Oct 6, 2026
@jonathanhefner
jonathanhefner marked this pull request as ready for review October 6, 2026 20:13
@jonathanhefner
jonathanhefner requested a review from a team as a code owner October 6, 2026 20:13
Name implementation defaults and local policy as permitted sources of
the existing small clock-skew tolerance. This resolves ambiguity about
who configures the allowance while preserving the temporal acceptance
bounds.

This branch has not been deployed

No deployments
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.

7 participants