Bilateral Federation
Two chio kernels in different organizations agree, by explicit handshake, to recognize each other's artifacts under stated boundaries. Trust is established per pair, not as a transitive property of any directory. The federation surface lives in chio-federation, with the kernel binding installed via builder methods on ChioKernel.
Bilateral, Not Multilateral
Every cross-kernel relationship is exactly one pair. Org A pins Org B's kernel key; Org B pins Org A's kernel key. Neither side gains authority over a third operator by transitivity. If Org B has a relationship with Org C, calls from A reaching tools at C require their own A-to-C handshake. There is no chio-operated root that vouches for either side.
- No global identity provider: identity is the Ed25519 kernel keypair on each side, exchanged out of band and pinned locally.
- No silent transitivity: a verifier checking a receipt from another org refuses unless the issuing kernel id matches a peer it has pinned.
- Symmetric per-pair terms: rotation windows, handshake skew, and import controls live in each kernel's local configuration.
Federation widens visibility, not authority
KernelTrustExchange Handshake
Two kernels bootstrap mutual trust by exchanging signed challenges and pinning each other's kernel signing public keys. The primitive is KernelTrustExchange, defined in chio-federation::trust_establishment. It is mTLS-style: both sides authenticate, both sides confirm key material, neither side accepts the other on weight of name alone.
The handshake body is HandshakeChallenge at chio-federation/src/trust_establishment.rs:76-85:
pub const FEDERATION_HANDSHAKE_SCHEMA: &str =
"chio.federation-kernel-handshake.v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HandshakeChallenge {
pub schema: String,
pub local_kernel_id: String,
pub remote_kernel_id: String,
pub nonce: String,
pub timestamp: u64,
}Each kernel signs the canonical-JSON encoding of its challenge and wraps the result in a PeerHandshakeEnvelope (trust_establishment.rs:108-114):
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PeerHandshakeEnvelope {
pub challenge: HandshakeChallenge,
pub declared_public_key: PublicKey,
pub signature: Signature,
}The remote side calls accept_envelope at trust_establishment.rs:367-424 with the envelope, the expected remote kernel id, and the local clock. The method enforces six steps in order:
envelope.verify_signature()checks that the canonical-JSON challenge bytes verify under the declared public key (trust_establishment.rs:144-155).- The envelope's
challenge.remote_kernel_idMUST equal the local kernel id, otherwiseAddressMismatch. - The envelope's
challenge.local_kernel_idMUST equal the expected peer id, otherwiseKernelIdMismatch. envelope_ts.abs_diff(now)MUST be withinmax_handshake_skew_secs(defaultDEFAULT_HANDSHAKE_MAX_SKEW_SECS = 5 * 60attrust_establishment.rs:50), otherwiseClockSkewExceeded.- The declared key MUST match either an anchor installed via
with_trusted_peer(trust_establishment.rs:325-332) or an already-pinned peer's key. Missing anchor returnsMissingTrustAnchor; mismatched anchor returnsUnexpectedPeerKeycarrying both expected and actual hex. - On success, the remote key is pinned as a fresh
FederationPeerwithrotation_due = now + rotation_window_secs(defaultDEFAULT_ROTATION_WINDOW_SECS = 12 * 60 * 60attrust_establishment.rs:45).
Out-of-band key pinning is required
with_trusted_peer(kernel_id, public_key) before the handshake will be accepted. Without an anchor, accept_envelope fails with MissingTrustAnchor.use chio_federation::{
KernelTrustExchange, KernelTrustExchangeConfig,
PeerHandshakeEnvelope, DEFAULT_ROTATION_WINDOW_SECS,
};
// Org A side: install a trust anchor for Org B and accept their envelope.
let exchange = KernelTrustExchange::new("org-a-kernel", org_a_keypair)
.with_trusted_peer("org-b-kernel", org_b_public_key.clone());
let envelope_from_b: PeerHandshakeEnvelope = receive_handshake_envelope();
let now = unix_seconds_now();
let peer = exchange.accept_envelope(&envelope_from_b, "org-b-kernel", now)?;
// 'peer' is now pinned. peer.is_fresh(now) returns true until rotation_due.
assert!(peer.is_fresh(now));Handshake Over HTTP
Production deployments wrap the envelope exchange in two HTTP POST calls over an mTLS-attested transport. Org A POSTs its signed envelope to Org B's federation handshake endpoint; Org B replies with its own envelope. Each side runs accept_envelope on the envelope it received.
POST /v1/federation/handshake HTTP/1.1
Host: chio.org-b.example
Content-Type: application/json
Authorization: Bearer <op-token>
{
"challenge": {
"schema": "chio.federation-kernel-handshake.v1",
"localKernelId": "org-a-kernel",
"remoteKernelId": "org-b-kernel",
"nonce": "8f3b9e0c-2a18-4f1a-9bd3-3a31c6e5d3f5",
"timestamp": 1714291200
},
"declaredPublicKey": "ed25519:80f2b53c9a4f1c3b2e7d8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d",
"signature": "ed25519:7e1b...c4a2"
}HTTP/1.1 200 OK
Content-Type: application/json
{
"challenge": {
"schema": "chio.federation-kernel-handshake.v1",
"localKernelId": "org-b-kernel",
"remoteKernelId": "org-a-kernel",
"nonce": "5d27f0a1-3b4c-4d5e-9f8a-7b6c5d4e3f2a",
"timestamp": 1714291203
},
"declaredPublicKey": "ed25519:9a4f1c3b2e7d8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",
"signature": "ed25519:c2d8...4f3a"
}After both responses verify, both sides hold a fresh FederationPeer with rotation_due = timestamp + 43_200 (12 hours). The transport is HTTP, but the trust comes from the Ed25519 signature on the canonical HandshakeChallenge bytes; mTLS adds confidentiality and channel auth, not handshake authenticity.
Federation Peer Set
Successful handshakes leave the local kernel with a set of pinned peers. ChioKernel exposes four methods around that set, defined in chio-kernel/src/kernel/mod.rs (Phase 20.3):
| Method | Purpose | Source |
|---|---|---|
with_federation_peers(self, peers: Vec<FederationPeer>) -> Self | Builder-style; install the trusted peer set during kernel construction. Replaces any prior set. Marked #[must_use]. | mod.rs:1533-1541 |
set_federation_cosigner(&mut self, cosigner) | Install the bilateral cosigner that contacts a peer kernel for a co-signature. Tests use InProcessCoSigner; production uses an mTLS RPC client. | mod.rs:1547-1552 |
set_federation_local_kernel_id(&self, id) | Advertise this kernel's stable id (e.g. a DNS name) to remote peers. Defaults to the hex encoding of the signing public key. | mod.rs:1558-1561 |
federation_peer(&self, remote_kernel_id, now) -> Option<FederationPeer> | Resolve a peer; returns None when unknown OR when !peer.is_fresh(now). Stale pins fail closed at the lookup, not at the call site. | mod.rs:1565-1577 |
federation_peers_snapshot(&self) -> Vec<FederationPeer> | Cloned snapshot of all currently-pinned peers. | mod.rs:1580-1582 |
Method names verified against source
with_federation_peers (builder, not set_federation_peers), the resolver is federation_peer(remote_kernel_id, now) (not resolve_federation_peer), and the local-id setter is set_federation_local_kernel_id (singular). Earlier docs drift was flagged in review; the names above were re-verified against mod.rs:1525-1582.The pinned peer record is FederationPeer:
pub struct FederationPeer {
pub kernel_id: String,
pub public_key: PublicKey,
pub established_at: u64,
pub rotation_due: u64,
}
impl FederationPeer {
pub fn is_fresh(&self, now: u64) -> bool {
now < self.rotation_due
}
}Stale peers fail closed
rotation_due, the kernel returns None from federation_peer and any federation operation that depends on the peer must refuse. The two kernels MUST re-run the handshake before further bilateral traffic is accepted; rotation never silently renews.Bilateral Federation Policy
The handshake establishes who you are talking to. A separate federation policy describes what each side will accept across the boundary. The policy lives on the trust control plane and is managed via chio trust federation-policy. Per-partner records carry these fields:
| Field | Purpose |
|---|---|
partner_id | Canonical name of the counterparty operator. Audit and evidence tagging. |
trusted_issuers | Ed25519 public keys the partner is allowed to sign passports and capabilities under. |
max_scope | Upper bound on tools, parameters, and budgets a foreign capability may reach. |
max_autonomy_tier | Highest GovernedAutonomyTier accepted from the partner. |
max_evidence_age_secs | Freshness ceiling for imported reputation, receipts, and revocation feeds. |
revocation_feed | Signed transparency URL the partner uses to publish revocations. |
sharing_posture | Whether imported evidence may be re-exported or stays pair-scoped. |
Federation activation artifacts in chio-federation attach the policy intent to a signed exchange. The struct definition at chio-federation/src/lib.rs:160-179:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FederationActivationExchangeArtifact {
pub schema: String,
pub exchange_id: String,
pub issued_at: u64,
pub expires_at: u64,
pub source_operator_id: String,
pub target_operator_id: String,
pub listing_id: String,
pub activation_ref: FederationArtifactReference,
pub listing_ref: FederationArtifactReference,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governing_charter_ref: Option<FederationArtifactReference>,
pub scope: FederationTrustScope,
pub delegation_control: FederationDelegationControl,
pub import_control: FederationImportControl,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}Canonical-JSON form of one such artifact wrapped in a SignedFederationActivationExchange envelope:
{
"body": {
"schema": "chio.federation.activation-exchange.v1",
"exchangeId": "fxa-org-a-org-b-2026-04-28-001",
"issuedAt": 1714291200,
"expiresAt": 1716883200,
"sourceOperatorId": "org-a",
"targetOperatorId": "org-b",
"listingId": "lst-billing-readonly",
"activationRef": {
"artifactId": "act-org-b-2026-04-21-77",
"operatorId": "org-b",
"sha256": "ad81...c4"
},
"listingRef": {
"artifactId": "lst-billing-readonly@v3",
"operatorId": "org-b",
"sha256": "92ef...fa"
},
"scope": {
"namespaces": ["billing.org-b.internal"],
"tools": [{ "tool": "billing.read" }]
},
"delegationControl": {
"maxAutonomyTier": "TIER_2_DELEGATED",
"maxRedelegationDepth": 1
},
"importControl": {
"explicitLocalActivationRequired": true,
"manualReviewRequired": true,
"rejectStaleInputs": true,
"allowVisibilityWithoutRuntimeTrust": true,
"prohibitAmbientRuntimeAdmission": true
}
},
"signerOperatorId": "org-a",
"signerPublicKey": "ed25519:80f2b53c...",
"signature": "ed25519:7e1b...c4a2"
}The default FederationImportControl (lib.rs:148-157) is conservative across all five booleans, so even after the artifact lands in the local store no runtime traffic is admitted until the operator explicitly opts in.
Cross-Org Capability Issuance
With the peer set pinned and a partner policy stored, an authority in Org A can mint a capability that is honored at Org B. The endpoint is POST /v1/federation/capabilities/issue on the trust control plane.
Request fields, in addition to the standard issuance body:
| Field | Meaning |
|---|---|
presentation | Passport presentation proving federated subject identity. |
expectedChallenge | Challenge value the presentation must satisfy. |
capability | Requested issued capability body. |
delegationPolicy | Optional signed ceiling for delegated issuance. Trust-control rejects untrusted signers. |
upstreamCapabilityId | Optional imported parent capability anchor for multi-hop lineage. |
Normative rules from the shipped service:
- When
upstreamCapabilityIdis supplied, a delegation policy bound to that exact parent capability id MUST also be present. - When a delegation policy is supplied, the requested child capability MUST NOT exceed the policy ceiling.
- The trust-control service MUST reject untrusted delegation-policy signers.
Success response:
{
"capability": { "...": "..." },
"delegationAnchorCapabilityId": "cap-anchor-org-a-2026-04-28-001",
"subjectPublicKey": "ed25519:80f2b5..."
}Delegation Anchor and Child Snapshot
When trust-control issues a delegated cross-org capability, it persists two records into capability lineage: the new local delegation anchor, and a child snapshot rooted at that anchor. The anchor is identified by the returned delegationAnchorCapabilityId and is the parent of the issued child. When upstreamCapabilityId is present, the anchor also bridges to the imported upstream capability so multi-hop lineage reconstructs truthfully through both organizations.
Lineage is the audit primitive
Why Bilateral Instead of Global
A global identity network would require every operator to accept a shared root, accept changes to that root by majority or committee, and accept that revocation propagates through someone else's schedule. Bilateral federation avoids all three questions.
- No shared root: every operator pins keys it has out-of-band reason to trust. There is no key whose compromise breaks the whole network.
- Per-pair negotiation: rotation windows, autonomy ceilings, evidence freshness, and re-export posture are negotiated by the two operators who care and recorded in their own configurations.
- Bounded blast radius: an Org A key compromise affects every counterparty pinned to that key, not the entire ecosystem. Each counterparty rotates on its own schedule.
- Audit clarity: every cross-org artifact names the two kernels involved. An auditor checking a receipt knows exactly which two operators' policies applied.
Worked Example: Org A and Org B
Org A runs a research kernel; Org B runs a partner kernel hosting domain-specific tools. They want one of A's agents to call B's billing.read tool with a budget of USD 50.00.
1. Exchange Kernel Keys Out of Band
Each operator publishes its kernel signing public key through a channel both sides already trust (signed announcement, secure email, ticket attached to a procurement record). The other side installs the key as a trust anchor:
// Org A configuring Org B as a trust anchor.
let exchange_a = KernelTrustExchange::new("org-a-kernel", org_a_keypair.clone())
.with_trusted_peer("org-b-kernel", org_b_public_key.clone());
// Org B configuring Org A as a trust anchor.
let exchange_b = KernelTrustExchange::new("org-b-kernel", org_b_keypair.clone())
.with_trusted_peer("org-a-kernel", org_a_public_key.clone());2. Run the Handshake
Each side builds a signed envelope addressed to the other and sends it over an attested transport (mTLS RPC in production). Each side verifies and pins:
let now = unix_seconds_now();
let envelope_a_to_b = exchange_a.local_envelope("org-b-kernel", "nonce-1", now)?;
let envelope_b_to_a = exchange_b.local_envelope("org-a-kernel", "nonce-2", now)?;
// Org B receives A's envelope and pins.
let peer_a_at_b = exchange_b.accept_envelope(&envelope_a_to_b, "org-a-kernel", now)?;
// Org A receives B's envelope and pins.
let peer_b_at_a = exchange_a.accept_envelope(&envelope_b_to_a, "org-b-kernel", now)?;3. Install Pinned Peers on the Kernel
let kernel_a = ChioKernel::new(config_a)
.with_federation_peers(vec![peer_b_at_a]);
kernel_a.set_federation_local_kernel_id("org-a-kernel");
let kernel_b = ChioKernel::new(config_b)
.with_federation_peers(vec![peer_a_at_b]);
kernel_b.set_federation_local_kernel_id("org-b-kernel");4. Store the Federation Policy
Both sides record their bilateral policy on the local trust control plane.
$ chio trust federation-policy create \
--config ./org-a-to-org-b.yaml \
--control-url https://ctl.org-a.example:8940 \
--control-token-file ./secrets/chio_admin.txtapiVersion: chio.dev/v1
kind: FederationPolicy
metadata:
name: org-a-to-org-b
spec:
partner_id: org-b
trusted_issuers:
- ed25519:9a4f1c3b2e7d8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b
max_scope:
tool_servers: [billing.org-b.internal]
tools:
- tool: billing.read
max_autonomy_tier: TIER_2_DELEGATED
max_evidence_age_secs: 3600
revocation_feed: https://trust.org-b.example/v1/revocations/feed
sharing_posture: pair_scoped5. Issue the Cross-Org Capability
An authority at Org A calls POST /v1/federation/capabilities/issue with a delegation policy that caps scope to billing.read on billing.org-b.internal with a USD 50.00 ceiling. Trust-control mints the child capability, persists the delegation anchor and child snapshot, and returns both ids.
6. Dispatch the Tool Call
The agent at Org A presents the child capability to Org B's tool surface. Org B's kernel verifies the issuer key against its bilateral trusted_issuers set, applies its max_scope and max_autonomy_tier, invokes the tool, and produces a receipt. The receipt is co-signed by both kernels (see Bilateral Receipts).
Error Cases
The handshake variants are enumerated by PeerHandshakeError at chio-federation/src/trust_establishment.rs:160-208. Every variant is fail-closed; callers MUST refuse to pin a peer when any step fails.
| Variant | When it fires | Operator action |
|---|---|---|
UnsupportedSchema | Envelope's challenge.schema does not equal FEDERATION_HANDSHAKE_SCHEMA. | Update the producer to emit chio.federation-kernel-handshake.v1. |
InvalidSignature | Signature does not verify against declared_public_key. | Investigate. Likely tampering, key mismatch, or canonical-JSON drift on the producer. |
AddressMismatch | Envelope addressed to a different remote_kernel_id than the local kernel. | Confirm the remote producer is using the right peer id. |
KernelIdMismatch | Envelope's declared local_kernel_id disagrees with the expected peer id. | Confirm the receiving operator is calling accept_envelope with the right peer name. |
ClockSkewExceeded | envelope_ts.abs_diff(now) > max_handshake_skew_secs (default 300s). | Check NTP on both kernels. Re-issue the envelope with a current timestamp. |
MissingTrustAnchor | First-contact handshake without a prior with_trusted_peer install. | Install the anchor out-of-band, then retry. |
UnexpectedPeerKey | Declared key differs from the anchor or the already-pinned key. The error carries both expected and actual hex. | Manual investigation. Re-pinning requires explicit operator action. |
PeerStale | resolve(kernel_id, now) on a peer past rotation_due. | Re-run the handshake. |
Example error response for a missing-anchor first contact:
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
{
"type": "https://chio.dev/errors/federation/missing-trust-anchor",
"title": "Missing trust anchor",
"status": 412,
"detail": "peer org-b-kernel is not trusted for first contact; configure a trust anchor before accepting handshakes",
"kernelId": "org-b-kernel"
}Example response for an out-of-skew envelope (the variant carries all three integers; the { envelope, local, skew } struct from trust_establishment.rs:183-188 is mirrored verbatim into the error body):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://chio.dev/errors/federation/clock-skew-exceeded",
"title": "Clock skew exceeded",
"status": 422,
"detail": "remote envelope timestamp 1714290000 drifts from local clock 1714291205 beyond 300s",
"envelope": 1714290000,
"local": 1714291205,
"skew": 300
}Trust-control issuance has its own failure case beyond the handshake:
- Untrusted delegation-policy signer: a federated issuance carrying a delegation policy whose signer is not in the local trusted issuer set is rejected at
POST /v1/federation/capabilities/issue(seespec/WIRE_PROTOCOL.md §4.2).
Federated Receipt Tables
Imported evidence and cross-org lineage land in three SQLite tables created by chio-store-sqlite/src/receipt_store/bootstrap.rs. The schemas (lines 847-884):
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)
);
CREATE INDEX IF NOT EXISTS idx_federated_lineage_bridges_parent
ON federated_lineage_bridges(parent_capability_id);
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
);
CREATE INDEX IF NOT EXISTS idx_federated_evidence_shares_imported_at
ON federated_evidence_shares(imported_at);
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)
);
CREATE INDEX IF NOT EXISTS idx_federated_share_receipts_capability
ON federated_share_tool_receipts(capability_id);
CREATE INDEX IF NOT EXISTS idx_federated_share_receipts_subject
ON federated_share_tool_receipts(subject_key);federated_evidence_sharesrecords each accepted import with issuer, partner, signer key, and the canonical query that produced it.federated_share_tool_receiptsstores raw imported receipt JSON keyed by(share_id, seq);(share_id, receipt_id)is uniquely indexed so duplicate imports are idempotent.federated_lineage_bridgeslinks a locally-minted delegation anchor (incapability_lineage) to its imported parent and the share that brought the parent across.ON DELETE CASCADEon the local capability id means deleting the local anchor drops the bridge row, never the imported share.
In the Procurement Tour
This is station 4 of the Procurement Tour: the buyer kernel resolves Vanguard as a pinned federation peer before sending the call. The handshake itself completed earlier; this station is the runtime check.
GovernedTransactionIntent targeting vanguard-security with a quoted, hold-capture settlement.Lattice and Vanguard ran a bilateral handshake at onboarding; each kernel pinned the other's key as a FederationPeer with a 12-hour rotation window. At dispatch time the buyer kernel calls KernelTrustExchange::resolve(remote_kernel_id, now); the call returns the pinned record and asserts freshness via FederationPeer::is_fresh(now):
{
"kernelId": "did:chio:c87a...",
"publicKey": "ed25519:c87a3f9b...",
"establishedAt": 1745784000,
"rotationDue": 1745827200
}The pin is fresh (now < rotationDue), so the buyer kernel proceeds. The resolved public key is the verification anchor that station 5 will use to check Vanguard's detached signature on the dual-signed receipt; if the pin had been stale, the kernel would return PeerStale and refuse the call until a re-handshake. Per-pair policy (autonomy tier ceiling, evidence-reshare flags, tenant scope) is carried by the bilateral policy contract pinned alongside the peer.
Related
- Bilateral Receipts covers how cross-org tool calls produce dual-signed receipts.
- Portable Reputation shows how passport history travels with an agent across pinned peer boundaries.
- Guide: Bootstrap Federated Trust walks through configuring two operators end to end.
- Federation Overview places the bilateral surface inside the broader
did:chioidentity and revocation story.