Chio/Docs
LOGIN · JOIN

EconomyOther Companies

Bilateral Receipts

For a cross-organization call, the tool-host and origin kernels sign one receipt body for independent verification.

Origin kernel and tool-host kernel

Cross-org calls have two named roles. The origin kernel is the kernel where the calling agent lives. The tool-host kernel is the kernel that owns the tool and dispatches the invocation. In the federation literature these are commonly tagged Org A and Org B, with Org A holding the agent and Org B holding the tool.

  • Origin (Org A): holds the agent, mints the original capability or presents the delegated child via the federation issuance endpoint, signs the dual receipt as the second co-signer.
  • Tool-host (Org B): receives the call, applies its bilateral policy and guard pipeline, dispatches the tool, builds the receipt, and signs it first.

Each receipt carries the kernel signing public key that produced it through the standard ChioReceipt.signature and kernel_key fields. The bilateral envelope adds a detached signature from the remote kernel without mutating the receipt body, so a verifier that only understands the base receipt can still check the local chain in isolation.


Co-signing body and envelope

Two schemas are involved:

SchemaPurpose
chio.federation-bilateral-cosigning.v1The body both kernels sign. Constant: BILATERAL_COSIGNING_SCHEMA.
chio.federation-dual-signed-receipt.v1Record persisted after both signatures are present. Constant: BILATERAL_DUAL_RECEIPT_SCHEMA.

The signed body is CoSigningBody:

crates/trust/chio-federation/src/bilateral.rs42-57rust
/// Canonical body that the local and remote kernels both sign. The bytes of
/// this structure (in canonical JSON) are the signed message for
/// [`DualSignedReceipt::org_a_signature`] and
/// [`DualSignedReceipt::org_b_signature`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CoSigningBody {
    pub schema: String,
    /// Canonical JSON encoding of the underlying `ChioReceipt`, as a UTF-8
    /// string. The string form (rather than a nested object) keeps signing
    /// stable even if the receipt schema grows new `skip_serializing_if`
    /// fields later: both kernels sign exactly the bytes they saw.
    pub receipt_canonical_json: String,
    pub org_a_kernel_id: String,
    pub org_b_kernel_id: String,
}

The receipt is embedded as canonical JSON in a string, not a nested object. This keeps signing stable even if the receipt schema later adds fields with skip_serializing_if: each kernel signs the exact bytes it saw, no re-encoding step between sign and verify.

The persisted record is DualSignedReceipt:

crates/trust/chio-federation/src/bilateral.rs86-107rust
/// A receipt co-signed by two kernels across a federation boundary.
///
/// * `body` -- the underlying `ChioReceipt` that both kernels agreed on.
/// * `org_a_signature` -- detached signature by the origin (Org A) kernel
///   over the canonical [`CoSigningBody`].
/// * `org_b_signature` -- detached signature by the tool-host (Org B) kernel
///   over the same canonical body.
///
/// The existing receipt's built-in `signature` and `kernel_key` fields are
/// unchanged: a classic verifier can still check the receipt in isolation,
/// while a federation-aware verifier additionally checks both detached
/// signatures via [`DualSignedReceipt::verify`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DualSignedReceipt {
    pub schema: String,
    pub body: ChioReceipt,
    pub org_a_kernel_id: String,
    pub org_b_kernel_id: String,
    pub org_a_signature: Signature,
    pub org_b_signature: Signature,
}

Two verification entry points sit on the type, and the difference between them is which side supplies the kernel ids. verify(org_a_public_key, org_b_public_key) (bilateral.rs:125) takes the expected ids from the artifact it is checking. verify_pinned(ExpectedBilateralPeers) (bilateral.rs:153) takes them from the caller, and the doc comment names it the one to use at a trust boundary for that reason. Both rebuild the canonical CoSigningBody and return Ok only when both detached signatures validate.

Either half alone is not sufficient

A federation-aware verifier MUST refuse a dual-signed receipt when only one detached signature validates. The bilateral contract is that both kernels signed off; either failure invalidates the cross-org assertion. The base ChioReceipt embedded in body can still be verified in isolation by a non-federation verifier, but that is a weaker assertion than the dual signature.

Protocol flow

The tool-host kernel drives the protocol. After the local receipt has been built and signed, a apply_federation_cosign post-sign hook runs whenever the request carries a federated_origin_kernel_id.

  1. Tool-host kernel builds the canonical CoSigningBody from the receipt and the two kernel ids.
  2. Tool-host kernel signs the canonical bytes with its own keypair, producing org_b_signature.
  3. Tool-host kernel calls the installed BilateralCoSigningProtocol implementation with a CoSigningRequest.
  4. Origin kernel checks that org_a_kernel_id names it, rebuilds the canonical body from the receipt and both ids, and verifies Org B's signature against the tool-host key it holds. On any failure it returns a typed error and refuses to co-sign (InProcessCoSigner, bilateral.rs:437).
  5. Origin kernel signs the same canonical bytes with its own keypair and returns CoSigningResponse carrying org_a_signature.
  6. Tool-host kernel verifies the origin signature against its pinned peer key, assembles the DualSignedReceipt, checks both signatures again and persists the record.
rust
// chio_federation::bilateral::co_sign_with_origin drives the full protocol.
// Tool-host (Org B) side:
let dual: DualSignedReceipt = co_sign_with_origin(
    "org-a-kernel",            // origin kernel id
    &org_a_public_key,         // pinned peer key from FederationPeer
    "org-b-kernel",            // local (tool-host) kernel id
    &tool_host_keypair,
    receipt,                   // the locally-signed ChioReceipt
    cosigner.as_ref(),         // BilateralCoSigningProtocol impl
)?;

The kernel post-sign hook fails closed: when the request carries a federated origin id but the cosigner is missing, or the peer is unpinned, or the cosigner returns an error, the kernel surfaces a KernelError::Internal so operators see federation drift instead of receiving a receipt without the remote signature.

rendering
The co-signing round trip co_sign_with_origin drives: the tool-host signs the canonical CoSigningBody, the origin verifies that signature before adding its own, and the tool-host checks the returned signature before assembling the DualSignedReceipt. The DSSE envelope the same hook emits alongside the record is not drawn.
sourcecrates/trust/chio-federation/src/bilateral.rs:505-573at fe56570

Receipt routing on cross-org calls

The kernel state for the federation hook is set up via three builder methods on ChioKernel:

MethodPurpose
with_federation_peers(peers)Pin the trusted peer set after handshake.
set_federation_cosigner(cosigner)Install the implementation that contacts the origin kernel for a co-signature.
set_federation_local_kernel_id(id)Advertise this kernel's stable name. Defaults to the hex of the signing public key.

Production deployments wire the cosigner to an mTLS-attested RPC client that talks to the origin kernel; in-process tests can use InProcessCoSigner which holds the origin kernel's signing keypair directly.

After the hook completes, the dual-signed receipt is reachable from the kernel by receipt id:

rust
if let Some(dual) = kernel.dual_signed_receipt(&receipt.id) {
    dual.verify(&org_a_public_key, &org_b_public_key)?;
    // both kernels signed off
}

Federated lineage bridges

Cross-org delegation produces capability lineage that crosses a federation boundary. The receipt store captures this with three related tables, created as inline CREATE TABLE statements in chio-store-sqlite/src/receipt_store/bootstrap/open.rs (there is no migrations/ directory):

federated_lineage_bridges

Maps a local capability id to its parent in another org and the share id that imported the parent. The table joins local capability_lineage rows to imported evidence, so an auditor walking child-to-parent traversal lands on a recognizable record even when the parent was issued at a different kernel.

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS federated_lineage_bridges (
    local_capability_id TEXT PRIMARY KEY
        REFERENCES capability_lineage(capability_id) ON DELETE CASCADE,
    parent_capability_id TEXT NOT NULL,
    share_id TEXT REFERENCES federated_evidence_shares(share_id)
);

federated_evidence_shares

One row per signed evidence package imported from a partner. The manifest hash is the content identifier; issuer and partner name the two operators; signer public key identifies the partner's signing key at import time.

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS federated_evidence_shares (
    share_id TEXT PRIMARY KEY,
    manifest_hash TEXT NOT NULL,
    imported_at INTEGER NOT NULL,
    exported_at INTEGER NOT NULL,
    issuer TEXT NOT NULL,
    partner TEXT NOT NULL,
    signer_public_key TEXT NOT NULL,
    require_proofs INTEGER NOT NULL DEFAULT 0,
    query_json TEXT NOT NULL
);

federated_share_tool_receipts

Receipt rows attached to an imported share. Each row carries the receipt id, capability id, subject and issuer keys, and the raw JSON of the receipt for verification. The composite primary key (share_id, seq) orders the receipts inside one share; a unique constraint on (share_id, receipt_id) prevents duplicate import.

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS federated_share_tool_receipts (
    share_id TEXT NOT NULL
        REFERENCES federated_evidence_shares(share_id) ON DELETE CASCADE,
    seq INTEGER NOT NULL,
    receipt_id TEXT NOT NULL,
    timestamp INTEGER NOT NULL,
    capability_id TEXT NOT NULL,
    subject_key TEXT,
    issuer_key TEXT,
    raw_json TEXT NOT NULL,
    PRIMARY KEY (share_id, seq),
    UNIQUE (share_id, receipt_id)
);

A companion table federated_share_capability_lineage records imported capability lineage, including delegation depth and parent capability id, so a partner's chain can be walked offline against the share manifest.


Federated evidence shares

Beyond co-signed receipts on the request path, the federation API supports asynchronous evidence sharing. A partner can package a bundle of receipts and lineage rows into a signed share, send it across the boundary, and let the receiving org import it under its bilateral FederationImportControl:

  • Explicit local activation required by default. Imported shares stay visibility-only until an operator activates them.
  • Manual review required by default before activation.
  • Stale inputs rejected: reject_stale_inputs is a boolean and defaults true. The control carries no age threshold of its own; freshness is decided by whatever the receiving side treats as stale for the artifact kind.
  • Ambient runtime admission prohibited: imported evidence influences visibility and reputation displays; it does not silently widen runtime authority.

Visibility, not admission

Imported shares feed comparison views and reputation projections. They never substitute for a locally issued capability. Even after activation, runtime authority requires a capability that satisfies the local guard pipeline.

Revocation root gossip

When Org A revokes a capability, Org B has to stop honoring it at the next enforcement decision. What crosses the boundary is not a list of revoked ids but a signed sparse-Merkle-tree root per epoch. The oracle in chio-revocation-oracle owns the tree and the signing; chio-federation::revocation_gossip owns the wire envelope, the batched push queue, and the gap-fill protocol.

The envelope is RevocationRootGossip, schema chio.federation-revocation-root-gossip.v1 (chio-federation/src/revocation_gossip.rs:40). It carries the signed epoch root, the signer identity, the epoch duplicated outside the signature for cheap routing, and ts_unix_ms, the sender's emit time, which the receiver uses for freshness gating and to age out a stalled feed. Four things fail closed on the receiving side:

  • Structure before crypto. validate_envelope rejects a wrong schema tag, an epoch that disagrees with signed_root.root.epoch, and a signer_id that disagrees with the one inside the signature. It verifies no signature, and the doc comment says so: the caller still runs SignedEpochRoot::verify against a pinned signer before trusting the root.
  • A pinned signer, not a URL. The wire types carry no endpoint and no public key. A receiver verifies each root against a key it already holds, so the transport contributes origin, ordering, and reliability and contributes nothing to authenticity.
  • Bounded push. RevocationGossipPushQueue keeps a per-peer FIFO ring. Inside one tick a newer root for the same epoch replaces an older one and a strictly higher epoch evicts every queued lower epoch, so a high revoke rate coalesces instead of storming. flush_batches_at drains each peer into a RevocationGossipBatch, and an empty queue produces no batch at all.
  • Bounded catch-up. A peer that missed epochs sends a RevocationCatchupRequest naming an inclusive range. An inverted range and a range wider than REVOCATION_CATCHUP_MAX_EPOCHS are both refused, so one stalled peer cannot make a responder materialize an unbounded history slice. validate_response then requires strictly increasing epochs with no internal gap, and surfaces a short answer as truncation rather than as coverage.

The transport for this is lane b of chio-federation-transport-iroh, a direct per-peer QUIC stream that carries both the pushed batch and the catch-up exchange (chio-federation-transport-iroh/src/lanes/revocation.rs:1-41). Its accept handler runs three independent mandatory checks: the admission gate refuses an endpoint not bound to an admitted kernel, the frame's signer_id must be pinned to that same authenticated endpoint, and the SignedEpochRoot must verify against the bound key.


Independent verification

An auditor can verify a dual-signed receipt without contacting either kernel at runtime. They need:

  1. The persisted DualSignedReceipt.
  2. The Org A kernel signing public key (Ed25519) and the kernel id it expects to see on that side.
  3. The Org B kernel signing public key (Ed25519) and its kernel id.

The ids belong on that list because verify_pinned compares them against the ids the artifact declares and refuses on a mismatch, which is the check the artifact cannot make about itself. It also refuses two equal kernel ids and two equal keys, rebuilds the canonical CoSigningBody, checks both detached signatures, verifies the embedded receipt's own signature, and requires that the receipt's kernel_key be the tool-host key it was handed. co_sign_with_origin runs the same verification on the assembled artifact before it returns (bilateral.rs:571), so a record the tool-host kept is one that already passed the check a third party will run.

Where the kernel keeps it is a bounded in-memory map, federation_dual_receipts, which drops oldest at capacity. set_federation_artifact_store(store) (construction.rs:1288) closes that gap: with a store installed, each artifact is written through before it enters the cache and dual_signed_receipt falls back to the store on a cache miss. Without one, an evicted co-sign artifact is gone while the durable receipt store still holds the base receipt.

rust
// Stand-alone verifier with no live kernel access.
let dual: DualSignedReceipt = read_persisted_artifact()?;
dual.verify_pinned(ExpectedBilateralPeers {
    org_a_kernel_id: "org-a-kernel",
    org_a_public_key: &org_a_public_key,
    org_b_kernel_id: "org-b-kernel",
    org_b_public_key: &org_b_public_key,
})?;

// verify_pinned already ran this. The embedded ChioReceipt is also
// verifiable on its own, and it takes no key argument: verify_signature
// checks the kernel_key the receipt carries, after re-deriving the
// receipt id from the body.
let body_signature_ok = dual.body.verify_signature()?;
assert!(body_signature_ok);

DSSE attestation layer

DualSignedReceipt::verify checks two raw kernel keys the auditor already holds. For portable, third-party verification, Chio wraps that verification operation in a normative in-toto-style DSSE attestation predicate, chio.bilateral-cosign-invocation.v1 (spec CHIO_BILATERAL_COSIGN_INVOCATION.md, v1, 2026-05-04), implemented in chio-federation's bilateral_dsse.rs and bilateral_verifier.rs. The spec names bilateral.rs, the CoSigningBody and DualSignedReceipt pair above, as the existing Chio verification operation it wraps.

The DSSE envelope carries a named, ordered pair of signatures. Each keyid is the SHA-256 of the signing kernel's passport public key, and the envelope is invalid unless both declared keys appear and both verify. That is stricter than DSSE's permissive (t,n) threshold: it asserts a joint commit by two named organizational identities, not two signers who happen to sign the same statement. The Statement subject digest is the SHA-256 of the canonical-JSON (RFC 8785) ChioReceipt body, the same bytes the co-signing operation covers.

Trust inputs live outside the proof package. A verifier resolves peer pins, ladder references, and action-class policy from separate signed records ( chio.federation.verifier-trust-bundle.v1 plus chio.federation.verification-context.v1 ) and MUST reject a package whose embedded hints disagree with the trust bundle. The proof package itself embeds no peer pins, ladder refs, or action-class policy.

signature-slice is not conformance evidence

The chio.bilateral-signature-slice.v1 profile ships as a compatibility artifact for local receipt binding, and strict Chio verification rejects it as conformance evidence. Chio proof packages carry chio.bilateral-cosign-invocation.v1, which is what chio-federation emits and verifies. The spec reserves a second URI for the same predicate, https://in-toto.io/attestation/bilateral-cosign-invocation/v1. A verifier treats the two as semantically equivalent inside one deployment and MUST NOT rewrite either into the other, because the predicate type sits inside the signed Statement and rewriting breaks signature verification.

Worked example: tracing a dual receipt

Suppose an agent at Org A invokes billing.read on Org B's billing tool server. The trace looks like this:

1. Tool-host receives the call

Org B's kernel receives the ToolCallRequest with federated_origin_kernel_id = Some("org-a-kernel"). The kernel applies the bilateral policy stored at trust control, verifies the inbound capability against its trusted_issuers set, and runs the guard pipeline.

2. Local receipt

On allow, Org B's kernel invokes the tool, builds a ChioReceipt, and signs it with the local kernel keypair. The receipt looks identical to a non-federation receipt at this point: it carries the standard signature and kernel_key fields.

3. Co-signing hook

The post-sign hook fires. It builds the canonical CoSigningBody, signs it as Org B, sends a CoSigningRequest to the origin via the installed cosigner, verifies the returned org_a_signature, and persists the DualSignedReceipt.

4. Audit walk

Six months later an auditor opens the stored artifact with chio receipt explain <receipt-id> --input-file ./bilateral.json --inspect-bilateral. The positional receipt id is a sentinel on this path, informational only; the bilateral shape is detected from the file, which has to carry both a dualSignedReceipt and a dsseEnvelope section (chio-cli/src/cli/types/receipt.rs:80-101). The renderer is print_bilateral_human (chio-cli/src/cli/trust/receipt/explain.rs:558), and it prints three labelled sections:

SectionLines it prints
Headerschema and shape, the report naming itself as chio.cli.receipt-explain.bilateral.v1 over a BilateralCoSignArtifacts document.
DualSignedReceipt (NON-SECTION-6-CONFORMANT)receipt_id, schema, org_a_kernel_id, org_b_kernel_id, and a disclaimer line carrying the reason for the heading: this half signs the canonical JSON of CoSigningBody, not the DSSE PAE preimage.
DSSE envelope (signature-slice API artifact)schema, payload_type, payload_hex truncated to 96 characters with the decoded byte count, then one entry per signature with its keyid and a sig truncated to 32 characters, and a closing conformance note.

A keyid here is the SHA-256 of the signing kernel's passport public key, so it reads as 64 hex characters and not as a kernel name. --inspect-bilateral adds a fourth section, a numbered structural trace that opens by printing that it is an inspection trace and that Ed25519 signatures are not verified in it; without the flag no trace renders at all (explain.rs:108, :128).

The renderer reads structure, not cryptography: it does not hold the Org A and Org B passport keys, so it makes no verification claim. The auditor verifies both signatures against pinned keys with chio_federation::bilateral_dsse::verify_dsse_envelope (bilateral_dsse/verify.rs:17), which covers the DSSE half, the one the disclaimer points at for section-6 conformance. Its own conformance note keeps the boundary narrow: the signatures cover the PAE bytes, and the predicate is the signature-slice profile rather than the strict bilateral invocation schema. They walk lineage through federated_lineage_bridges to the parent capability id at Org A, and from there into Org A's own capability lineage if available. Each step uses a signed record verified offline.


Failure modes

  • Cosigner missing: a request with a federated origin lands on a kernel that has no cosigner installed. The post-sign hook returns KernelError::Internal and the receipt is rejected.
  • Peer unpinned or stale: the federation peer for the origin kernel id is not present or has aged past rotation_due. The hook fails closed and the operator must re-handshake.
  • Origin signature invalid: the response from the cosigner does not verify against the pinned peer key. co_sign_with_origin returns OrgASignatureInvalid.
  • Tool-host signature invalid: the origin kernel rejects the request with OrgBSignatureInvalid. Operators check tool-host key drift first.
  • Receipt body mismatch: the body in the request differs from the body the cosigner observed. Surfaced as ReceiptMismatch.