Bilateral Receipts
Follow one call into a partner’s tool and verify the receipt both kernels sign, with neither kernel online.
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.
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.
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.- Admit. Your kernel receives the
ToolCallRequestwithfederated_origin_kernel_id = Some("kernel.halden.example"), applies the bilateral policy stored at the trust control plane, verifies the inbound capability against itstrusted_issuers, and runs the guard pipeline. - Dispatch and sign. On allow, it invokes the tool and signs a
ChioReceiptwith its own keypair. At this point the receipt is an ordinary one, with the standardsignatureandkernel_keyfields. - Request. The post-sign hook
apply_federation_cosignbuilds the canonicalCoSigningBodyfrom the receipt and both kernel ids, signs it asorg_b_signature, and sends aCoSigningRequestthrough the installed co-signer. - Co-sign. The partner’s kernel checks that
org_a_kernel_idnames it, rebuilds the body, and verifies your signature against the tool-host key it holds. It signs the same bytes and returnsorg_a_signaturein aCoSigningResponse. On any failure it returns a typed error and signs nothing. - 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
// 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.
/// 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.
// 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.
chio receipt explain rcpt-01ab…f2d0 --input-file ./bilateral.json --inspect-bilateralThe 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.
| Signal | Raised by | Next action |
|---|---|---|
KernelError::Internal | Tool host, before the request | Install the co-signer, or re-handshake a stale peer |
OrgBSignatureInvalid | Origin | Check the tool-host key for drift |
ReceiptMismatch | Origin | Compare the body each side built |
OrgASignatureInvalid | Tool host | Compare the expected peer ids and keys |
| Origin unreachable | Tool host | Keep the signed body; request the signature again |
| Artifact write fails | Tool host | Check 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
Try verify_pinned, federated_lineage_bridges, or RevocationRootGossip.
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.
// 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.
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
/// 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.
schemaandshape: the report names itselfchio.cli.receipt-explain.bilateral.v1over aBilateralCoSignArtifactsdocument. - DualSignedReceipt (NON-SECTION-6-CONFORMANT).
receipt_id,schema,org_a_kernel_id,org_b_kernel_id, and adisclaimerline giving the reason for the heading: this half signs the canonical JSON ofCoSigningBody, not the DSSE PAE preimage. - DSSE envelope (signature-slice API artifact).
schema,payload_type,payload_hextruncated to 96 characters with the decoded byte count, one entry per signature with itskeyidand asigtruncated to 32 characters, and a closingconformancenote.
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.
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.
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.
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_inputsis 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_enveloperejects a wrong schema tag, anepochthat disagrees withsigned_root.root.epoch, and asigner_idthat disagrees with the one inside the signature. It verifies no signature: the caller still runsSignedEpochRoot::verifyagainst 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.
RevocationGossipPushQueuekeeps 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_atdrains each peer into aRevocationGossipBatch, and an empty queue produces no batch. - Bounded catch-up. A peer that missed epochs sends a
RevocationCatchupRequestnaming an inclusive range. An inverted range and a range wider thanREVOCATION_CATCHUP_MAX_EPOCHSare refused, so one stalled peer cannot make a responder materialize an unbounded history slice.validate_responserequires 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.