# What happens when an instance verifies a delegated agent manifest whose issuer chain crosses an org boundary mid-credential **Date of Record:** 2026 November 10 **Scope:** Applies to cross-org agent trust brokering where agent manifests are signed and presented alongside verifiable credentials (VCs) whose `issuer` and `credentialSubject` are controlled by different organizations, and where the verifying party must decide whether to accept a transitive delegation. Does not apply to single-org trust domains, nor to cases where the manifest and all VCs share a single root of trust. ## Core Distinction The failure is not "untrusted issuer." It is a *split-root* condition: the agent manifest is signed by org A's key, but the VC that attests the agent's delegated authority carries a `credentialSubject` whose controller is org B, and the VC was issued by org B's key. Each signature verifies independently. Each issuer is individually on the verifier's trust list. The composite claim — "agent X, operated by A, may act for B on resource R" — is never actually signed by anyone. The verifier reassembles it from two valid halves and treats the reassembly as a single attestation. The non-obvious part: standard VC verification pipelines check `proof` validity, issuer trust, expiration, and revocation *per credential*. They do not check that the manifest's `controller` field and the VC's `credentialSubject.id` resolve to the same key material. When they differ, the pipeline still returns `verified: true` for both artifacts, and the caller concatenates them into a trust decision that no principal ever authorized. ## What Was Observed During a federated agent handshake, org A's agent presented: 1. A manifest `M` signed by `did:web:org-a.example#agent-key-7`, containing `"controller": "did:web:org-a.example"` and `"delegates": ["did:web:org-b.example/agent/42"]`. 2. A VC `V` with `issuer: did:web:org-b.example`, `credentialSubject.id: did:web:org-b.example/agent/42`, and `credentialSubject.authorizedBy: did:web:org-a.example`. The verifier's trust list contained both `org-a.example` and `org-b.example`. Signature checks passed. Expiry passed. Revocation endpoints returned "not revoked" for both. The verifier concluded the agent could act for org B on the target resource. But the `authorizedBy` field inside `V` is *self-asserted by org B's issuer*. Org B's issuance policy did not require proof that org A had actually delegated anything — it accepted a bearer assertion. The manifest `M` proves only that org A controls its own agent; it says nothing binding about org B's issuance. The two halves are individually sound and jointly meaningless. In the observed incident, the agent exercised a scope (write access to a shared dataset) that org A had never granted and org B had never independently authorized; each org believed the other had approved it. ## Condition of Validity The composite trust decision is valid only if there exists a *binding* between the manifest signer and the VC issuer that is itself signed by at least one of them and references the other's key. Concretely: the VC must contain a `proof` chain whose terminal issuer is reachable from the manifest's `controller`, or the manifest must contain a signed `authorization` object referencing the VC's `id` (not merely its subject). If the only link between the two artifacts is a human-readable or self-asserted field (`authorizedBy`, `delegatedFrom`, free-text `purpose`), the binding is unauthenticated and the composite claim must be treated as unverified. A second validity condition: the verifier's trust list must be evaluated *per edge*, not *per node*. Both orgs being trusted is insufficient; the specific edge A→B must be in the delegation graph the verifier trusts. Most trust-list implementations flatten this into a set membership check and thereby lose the edge. ## Decision Boundary - If `M.controller` key ≠ key that signed the binding to `V.issuer`, **reject** the composite claim even when both signatures verify. - If the only link is a self-asserted field, **downgrade** to "unauthenticated delegation" and require an out-of-band confirmation or a re-issued VC carrying a signed reference to `M.id`. - If the delegation edge A→B is absent from the verifier's per-edge trust graph, **reject** regardless of node trust. - If revocation status is checked per credential but not per edge, treat a revocation of the edge as a revocation of the composite even when both endpoints remain unrevoked. The boundary is sharp: node-level verification passing is necessary and *not sufficient*. The decision must be made on the edge. ## Operational Effect Verifiers that implement only per-credential checks will admit split-root delegations silently; the failure is invisible in logs because every individual check returns success. The observable symptom is scope creep — agents exercising authority neither org consciously granted — with no failed verification to correlate against. Mitigation requires the verifier to construct an explicit signed edge graph, reject composites whose edges are self-asserted, and log the *edge* (issuer→subject→resource) rather than the credential IDs. Retrofitting this after deployment requires re-issuing VCs to carry signed back-references to manifests, because the missing binding cannot be recovered from existing artifacts. ```python from dataclasses import dataclass from typing import Optional class UnauthenticatedDelegationError(RuntimeError): """Raised when a composite agent trust claim lacks a signed edge binding.""" pass @dataclass class Manifest: id: str controller_key: str delegates: list[str] @dataclass class VerifiableCredential: id: str issuer_key: str subject_key: str authorized_by: Optional[str] # self-asserted, unauthenticated signed_edge_ref: Optional[str] # signed reference to a manifest id, if present def verify_composite(manifest: Manifest, vc: VerifiableCredential, trusted_edges: set[tuple[str, str]]) -> bool: # Both artifacts individually verify (assumed upstream). edge = (manifest.controller_key, vc.issuer_key) if vc.signed_edge_ref != manifest.id: # The only link between the two halves is self-asserted. raise UnauthenticatedDelegationError( f"VC {vc.id} claims authorizedBy={vc.authorized_by!r} but carries " f"no signed reference to manifest {manifest.id}; split-root " f"delegation {edge} cannot be authenticated." ) if edge not in trusted_edges: # Node trust passed upstream; edge trust did not. raise UnauthenticatedDelegationError( f"Delegation edge {edge} is not in the verifier's per-edge trust " f"graph; node-level trust is insufficient." ) return True ```