Chio/Docs

Bilateral Receipts

Follow one call into a partner’s tool and verify the receipt both kernels sign, with neither kernel online.

Concept chio@5f63e6e

On this page

Two operators can hold the same signed account of a call that crossed between them. When your partner's agent calls a tool that you host, both kernels sign one receipt body, so either operator, or an auditor, can settle a question about the call from the record alone, with no live kernel asked to repeat anything.

The kernel where the agent lives is the origin: it holds the agent and presents the original capability, or the delegated child from the federation issuance endpoint. The kernel that owns the tool is the tool host: it admits the call, dispatches it, signs first, and drives the exchange. The Quickstart’s merge under a treaty ends in the same kind of record, co-signed by your kernel and the partner’s.

The tool host signs first, then asks the origin to sign the same body

The partner's fixer-2 invokes pricing.quote on your pricing server while fixing issue #417. Five hops separate that call from a record both operators hold.

Bilateral co-signingChoose an outcome.
Org A · originOrg B · tool host1 call2 dispatch3 B signs, requests4 A verifies, signs5 verify, persistaagentK′origin kernelKtool host kerneldbilling.readρdual-signed receipttwo signaturesOrg A · originOrg B · tool host1 call2 dispatch3 request4 response5 persistaagentK′origin kernelKtool hostdbilling.readρdual-signed receipttwo signatures

Both kernels sign the same canonical body

The tool host dispatches the call, signs its receipt and the canonical CoSigningBody, and sends a CoSigningRequest. The origin checks that org_a_kernel_id names it, rebuilds the body, verifies the host’s signature, and returns its own. The host verifies that signature against its pinned peer key and persists the DualSignedReceipt. Independent verification.

One federated call from an agent at Org A to the billing.read tool at Org B, with the co-signing exchange the tool host drives after dispatch. Kernel ids and the tool name are illustrative; the DSSE envelope, the transport, and artifact storage are left out.
  1. Admit. Your kernel receives the ToolCallRequest with federated_origin_kernel_id = Some("kernel.halden.example"), applies the bilateral policy stored at the trust control plane, verifies the inbound capability against its trusted_issuers, and runs the guard pipeline.
  2. Dispatch and sign. On allow, it invokes the tool and signs a ChioReceipt with its own keypair. At this point the receipt is an ordinary one, with the standard signature and kernel_key fields.
  3. Request. The post-sign hook apply_federation_cosign builds the canonical CoSigningBody from the receipt and both kernel ids, signs it as org_b_signature, and sends a CoSigningRequest through the installed co-signer.
  4. Co-sign. The partner’s kernel checks that org_a_kernel_id names it, rebuilds the body, and verifies your signature against the tool-host key it holds. It signs the same bytes and returns org_a_signature in a CoSigningResponse. On any failure it returns a typed error and signs nothing.
  5. Persist. Your kernel verifies the origin’s signature against its pinned peer key, assembles the DualSignedReceipt, checks both signatures again, and persists the record.

The hook runs whenever a request carries federated_origin_kernel_id, and it fails closed. A missing co-signer, an origin peer that is unpinned or past rotation_due, or an error from the co-signer surfaces KernelError::Internal, so the operator sees federation drift instead of a receipt without the remote signature.

The tool host’s call in Rust
Tool host · co_sign_with_originrust
// Embedding the kernel at Larkfield, the tool host.
let dual: DualSignedReceipt = co_sign_with_origin(
    "kernel.halden.example",            // origin kernel id
    &halden_public_key,         // pinned peer key from FederationPeer
    "kernel.larkfield.example",            // local (tool-host) kernel id
    &tool_host_keypair,
    receipt,                   // the locally signed ChioReceipt
    cosigner.as_ref(),         // BilateralCoSigningProtocol impl
)?;

The signed body holds the receipt as the exact bytes each kernel saw

Both signatures cover one CoSigningBody under the schema chio.federation-bilateral-cosigning.v1, the constant BILATERAL_COSIGNING_SCHEMA.

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 rides inside the body as a canonical JSON string. If the receipt schema later adds fields with skip_serializing_if, signing stays stable, because each kernel signs the bytes it saw and nothing re-encodes them between signing and verification.

The persisted record, DualSignedReceipt under chio.federation-dual-signed-receipt.v1 (BILATERAL_DUAL_RECEIPT_SCHEMA), adds the two detached signatures and leaves the embedded receipt’s own signature and kernel_key as they were. A verifier that reads only base receipts still checks the host’s chain in isolation; a federation-aware verifier checks both signatures.

An auditor checks both signatures with neither kernel online

Verification takes three inputs: the persisted DualSignedReceipt, the partner’s Ed25519 kernel key with the kernel id expected on that side, and your key with its kernel id.

Auditor · offline verifierrust
// Stand-alone verifier with no live kernel access.
let dual: DualSignedReceipt = read_persisted_artifact()?;
dual.verify_pinned(ExpectedBilateralPeers {
    org_a_kernel_id: "kernel.halden.example",
    org_a_public_key: &halden_public_key,
    org_b_kernel_id: "kernel.larkfield.example",
    org_b_public_key: &larkfield_public_key,
})?;

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

verify_pinned takes the expected ids from the caller and refuses when the artifact declares different ones, the one check an artifact cannot make about itself. It also refuses two equal kernel ids or two equal keys, rebuilds the canonical body, checks both detached signatures, verifies the embedded receipt’s own signature, and requires the receipt’s kernel_key to be the tool-host key it was handed. verify reads the ids from the artifact instead, so a trust boundary uses verify_pinned.

A federation-aware verifier MUST refuse a dual-signed receipt when only one detached signature validates, because the bilateral claim is that both kernels signed. co_sign_with_origin runs the same verification on the assembled artifact before it returns, so the record the tool host keeps has already passed the check a third party will run.

Read more in Verify Receipts Offline.

A DSSE envelope carries the joint signature to any verifier

For portable verification, Chio wraps the co-signing operation in an in-toto DSSE attestation predicate, chio.bilateral-cosign-invocation.v1, which chio-federation emits and verifies.

The 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 valid only when both declared keys appear and both verify. That is stricter than DSSE’s (t,n) threshold: it asserts a joint commit by two named organizational identities. The Statement subject digest is the SHA-256 of the RFC 8785 canonical JSON of the ChioReceipt body, the same bytes the co-signing operation covers.

Terminalbash
chio receipt explain rcpt-01ab…f2d0 --input-file ./bilateral.json --inspect-bilateral

The command explains a stored artifact; it verifies no signature. The input file must contain both dualSignedReceipt and dsseEnvelope, and on this path the positional receipt id is informational. Check the signatures with verify_dsse_envelope against pinned keys.

Trust inputs stay outside the proof package. A verifier resolves peer pins, ladder references, and action-class policy from chio.federation.verifier-trust-bundle.v1 and chio.federation.verification-context.v1, and MUST reject a package whose embedded hints disagree with the trust bundle. The chio.bilateral-signature-slice.v1 profile is a compatibility artifact for local receipt binding, and strict Chio verification rejects it as conformance evidence.

Admission, outcome, and attestation fail separately

A dual signature attests one fact: both kernels signed the record of the call’s outcome. Admission happened earlier, at the tool host, and business correctness and payment settlement sit outside the record. Co-signing starts after the tool has run, so a failed exchange leaves an effect backed by the host’s signature alone.

SignalRaised byNext action
KernelError::InternalTool host, before the requestInstall the co-signer, or re-handshake a stale peer
OrgBSignatureInvalidOriginCheck the tool-host key for drift
ReceiptMismatchOriginCompare the body each side built
OrgASignatureInvalidTool hostCompare the expected peer ids and keys
Origin unreachableTool hostKeep the signed body; request the signature again
Artifact write failsTool hostCheck the durable store before reporting completion

A pending co-signature stays pending, and the tool is never dispatched again to obtain it. co_sign_with_origin consumes the retained, locally signed receipt and asks the origin to sign those exact bytes; it invokes nothing. The host’s signature alone remains a one-operator record, and the second signature can only come from the origin.

The caller may see an error for a call whose effect completed. Recovery resolves that call by its original identity, with no second dispatch; a second successful call would be evidence of a second invocation. An admission refusal is a separate case, repaired at the peer, capability, or policy input it names before the work is requested again.

Read more in Unknown Outcomes.

Kernel wiring, stored records, and shared evidence

Kernel wiring

On your side, install a BilateralCoSigningProtocol implementation that reaches the partner over the cross-organization transport: HTTPS with mutual TLS or signed bodies by default, or the iroh co-sign lane where both partners run iroh on self-hosted relays. On the partner's side, the signing key stays local, and the co-signer validates each request against the expected host identity and key.

Tool host · kernel constructionrust
// Embedding the kernel at Larkfield. larkfield_kernel is mutable during construction.
larkfield_kernel.set_federation_local_kernel_id("kernel.larkfield.example");
larkfield_kernel.set_federation_cosigner(origin_cosigner);
larkfield_kernel.set_federation_artifact_store(bilateral_artifact_store);

origin_cosigner has type Arc<dyn BilateralCoSigningProtocol>; bilateral_artifact_store has type Arc<dyn FederationArtifactStore>. InProcessCoSigner holds the origin’s signing keypair directly and serves in-process tests and local examples.

Bind the origin before dispatch. The federated request names the partner in federated_origin_kernel_id; connect that identity to the authenticated peer and the admitted authority, because a caller-supplied string is not peer authentication. Preserve the request and receipt identifiers the application uses; the exchange consumes the locally signed receipt.

Federation builder methods

  • with_federation_peers(peers) pins the trusted peer set after the handshake.
  • set_federation_cosigner(cosigner) installs the implementation that contacts the origin kernel for a co-signature.
  • set_federation_local_kernel_id(id) advertises this kernel’s stable name. It defaults to the hex of the signing public key.
  • set_federation_artifact_store(store) (construction.rs:1288) writes each co-sign artifact through before it enters the cache.

The kernel caches co-sign artifacts in a bounded in-memory map, federation_dual_receipts, that drops the oldest at capacity. With a store installed, 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. Use a durable store for retained evidence.

Tool host · read a dual receiptrust
if let Some(dual) = kernel.dual_signed_receipt(&receipt.id) {
    dual.verify_pinned(ExpectedBilateralPeers {
        org_a_kernel_id: "kernel.halden.example",
        org_a_public_key: &halden_public_key,
        org_b_kernel_id: "kernel.larkfield.example",
        org_b_public_key: &larkfield_public_key,
    })?;
    // both expected kernels signed off
}

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,
}

verify(org_a_public_key, org_b_public_key) (bilateral.rs:125) takes the expected ids from the artifact. verify_pinned(ExpectedBilateralPeers) (bilateral.rs:153) takes them from the caller, and its doc comment names it the one to use at a trust boundary. Both rebuild the canonical CoSigningBody and return Ok only when both detached signatures validate. co_sign_with_origin runs the check before it returns (bilateral.rs:571).

The kernel construction code defines the setters and artifact write-through. The bilateral module defines the request, the response, the signature checks, and InProcessCoSigner (bilateral.rs:437).

Audit walk output

chio receipt explain detects the bilateral shape from the input file (chio-cli/src/cli/types/receipt.rs:80-101). The renderer, print_bilateral_human (chio-cli/src/cli/trust/receipt/explain.rs:558), prints three labeled sections:

  • Header. schema and shape: the report names itself 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 giving 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, one entry per signature with its keyid and a sig truncated to 32 characters, and a closing conformance note.

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

The renderer holds neither operator’s passport key, so it makes no verification claim. chio_federation::bilateral_dsse::verify_dsse_envelope (bilateral_dsse/verify.rs:17) verifies the DSSE half, the one the disclaimer points to for section 6 conformance. Its 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. The auditor then walks lineage through federated_lineage_bridges to the parent capability at the partner, and from there into the partner’s own capability lineage where available, each step a signed record verified offline.

Bilateral co-sign predicate

The predicate is normative in CHIO_BILATERAL_COSIGN_INVOCATION.md (v1, 2026-05-04) and implemented in chio-federation’s bilateral_dsse.rs and bilateral_verifier.rs. The specification names bilateral.rs, the CoSigningBody and DualSignedReceipt pair, as the verification operation it wraps. The proof package embeds no peer pins, ladder references, or action-class policy.

The specification 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.

Federated lineage tables

Cross-org delegation produces capability lineage that crosses a federation boundary. The receipt store records it in three related tables, created as inline CREATE TABLE statements in chio-store-sqlite/src/receipt_store/bootstrap/open.rs.

federated_lineage_bridges

Maps a local capability id to its parent in another organization and the share id that imported the parent. The table joins local capability_lineage rows to imported evidence, so a child-to-parent walk 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; the 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 receipt JSON for verification. The composite primary key (share_id, seq) orders the receipts inside one share, and 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

Beside co-signed receipts on the request path, the federation API shares evidence asynchronously. A partner packages receipts and lineage rows into a signed share and sends it across the boundary, and the receiving organization imports 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 that defaults to true. The control carries no age threshold of its own; the receiving side decides freshness for each artifact kind.
  • Ambient runtime admission prohibited. Imported evidence feeds visibility and reputation displays and leaves runtime authority unchanged.

Imported shares feed comparison views and reputation projections. Runtime authority, even after activation, still requires a locally issued capability that satisfies the local guard pipeline.

Revocation root gossip

When your partner revokes a capability, your kernel stops honoring it at its next enforcement decision. What crosses the boundary is a signed sparse Merkle tree root per epoch. 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 cryptography. 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: the caller still runs SignedEpochRoot::verify against a pinned signer before trusting the root.
  • A pinned signer. 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 authenticity comes from the pin.
  • Bounded push. RevocationGossipPushQueue keeps a per-peer FIFO ring. Inside one tick a newer root for the same epoch replaces the one queued before it, and a strictly higher epoch evicts every queued lower epoch, so a high revoke rate coalesces. flush_batches_at drains each peer into a RevocationGossipBatch, and an empty queue produces no batch.
  • 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 refused, so one stalled peer cannot make a responder materialize an unbounded history slice. validate_response requires strictly increasing epochs with no internal gap and surfaces a short answer as truncation.

HTTPS with mutual TLS or signed bodies carries the feed by default. Where the partners run the optional iroh transport on self-hosted relays, the revocation lane of chio-federation-transport-iroh carries both the pushed batch and the catch-up exchange (lanes/revocation.rs:1-41). Its accept handler runs three 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.