Chio/Docs
LOGIN · JOIN

Bilateral Co-Sign

Two operators sign one invocation with separate keys, and a receiver checks their agreement offline against the records it binds.

Concept chio@5f63e6e

On this page

Two passport keys, held by two operators, sign the same canonical statement about one invocation. The statement binds both kernels to one receipt, so a receiving operator can authenticate the agreement and check the receipt, capability lease, and governance record behind it.

When the Quickstart merges a partner’s fix under a treaty, the receipt is co-signed by both kernels. The statement drawn here is the strict bilateral invocation: tool host B, where the tool runs, and origin A, where the call started, each sign it with a key the other never holds.

Both keys sign the same bytes

The co-signer flow starts at tool host B. B signs the statement’s DSSE PAE bytes, asks A for a signature over the same bytes, checks the response, and assembles the envelope.

Bilateral co-signChoose a step.
scanonical statement · DSSE PAE bytes1 sign, request co-sign2 check B, co-sign the same bytesσBσAorg B · tool hostorg A · origin2 of 2EDSSE envelopeKreceiving kernelsPAE bytesσBσAhost Borigin A2 of 2EDSSE envelopeKreceiving kernel

Two keys sign the same bytes

Tool host B builds the canonical in-toto statement and signs its DSSE PAE bytes. Origin A checks B’s signature against its configured host key and returns its own signature over the same bytes. B verifies the response and assembles an envelope carrying the base64 statement and exactly two signatures. Signing.

The co-signer flow from tool host B to origin A over one statement’s DSSE PAE bytes. The strict context checks of the receipt, lease, and governance records are named, not drawn.

sign_chio_bilateral_dsse_envelope_with_cosigner runs this flow when B holds its own key and knows A’s public key. Its BilateralCoSigningProtocol callback obtains A’s signature, and the helper validates the returned signature and the final envelope. sign_chio_bilateral_dsse_envelope takes both keypairs directly, for local construction and tests.

Across organizations, A’s signature comes from a remote co-signer at A’s door; while it is unreachable, the work stays pending and is never re-dispatched. The in-process reference co-signer checks the request’s origin id and B’s configured key and signature, then signs the supplied bytes. The unit of agreement is one pair of organizations and one statement, so authenticating it needs no federation-wide consensus round.

One statement travels in three encodings

  1. Canonical statement bytes. The in-toto Statement with its receipt subject, strict predicate type, and invocation fields.
  2. DSSE PAE bytes. The payload type and the canonical statement bytes, framed with explicit byte lengths. Both Ed25519 signatures cover this encoding.
  3. Envelope payload. The statement bytes in base64 for transport, beside payloadType and exactly two signatures.

PAE is Pre-Authentication Encoding. Lengths count bytes and are written as decimal ASCII, as this source-test vector on a toy payload shows:

PAE framing vectortext
Type:     application/x
Payload:  hello
PAE:      DSSEv1 13 application/x 5 hello

A Chio envelope sets payloadType to application/vnd.in-toto+json. Its statement uses https://in-toto.io/Statement/v1 and the strict predicate type chio.bilateral-cosign-invocation.v1, and the decoded bytes must equal the statement’s canonical re-encoding.

Each keyid is the SHA-256 of the raw Ed25519 public-key bytes and must match the corresponding passport_key_fingerprint in the predicate. The verifier finds each signature by key id, so either array order passes. A duplicate key id, a missing signer, a third signature, or one key shared by A and B is refused.

The statement binds one receipt, one lease, and both verdicts

The strict context check ties the two signatures to records the receiver resolves. The statement names one subject, chio-receipt: plus the receipt id, whose SHA-256 matches the canonical receipt body. Its tool_name and tool_args_hash match that receipt, and the receipt’s own signature comes from the pinned key of tool host B.

Both policy verdicts must agree. The capability lease must resolve with a matching issuer and exact expiry, and a receipt-backed action class needs a resolvable governance receipt. The verifier reads the two recorded verdicts and reruns neither organization’s policy evaluation. Agreement to refuse is evidence too: a unanimous deny verifies, and admission runs its own allow check.

Read more in Invocation commitments and Verdicts.

Offline verification carries its own context

To verify offline, supply locally what the strict verifier reads: the receipt store, the lease and governance records, the pinned keys and ladder references, the action-class map, the evaluation time, and the revocation view. Two signatures and an isolated receipt satisfy only the envelope check.

Time and revocation are inputs. A later check can refuse an expired lease or ladder reference, and retained evidence of an agreement says nothing about whether the same action is authorized now. Both peers need a ladder manifest reference that is fresh at the chosen time.

Worked Rust example: Sign the same invocation at two operator boundaries

Sign the same invocation at two operator boundaries

Create one strict bilateral invocation statement, compute its DSSE PAE bytes, and collect two signatures over those exact bytes. Verify with the buyer and vendor public keys, then alter the payload.

Use the Rust example setup, or download the complete source project. The files below belong to that project; shared constructors and configuration are included in the download.

Run it

bash
cargo run --locked --features federation --bin bilateral

Implementation

bilateral.rsrust
#[path = "../receipt.rs"]
mod receipt;
use base64::{engine::general_purpose::STANDARD, Engine};
use chio_core_types::{crypto::Keypair, receipt::body::ChioReceipt};
use chio_federation::bilateral_dsse::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let buyer = Keypair::generate();
    let vendor = Keypair::generate();
    let receipt = ChioReceipt::sign(receipt::receipt_body(&vendor), &vendor)?;
    // Construct one strict invocation statement, then send the same PAE bytes
    // to each operator's signing boundary. Keys stay with their owners.
    let buyer_id = Keyid::from_public_key(&buyer.public_key());
    let vendor_id = Keyid::from_public_key(&vendor.public_key());
    let identity = |kernel: &str, fingerprint: Keyid| KernelIdentity {
        kernel_id: kernel.into(),
        passport_key_fingerprint: fingerprint,
        alg: "ed25519".into(),
    };
    let allow = |policy: &str| PolicyVerdict {
        verdict: "allow".into(),
        policy_id: policy.into(),
        policy_version: "1".into(),
        rationale_code: None,
    };
    let extensions = BilateralPredicateExtensions {
        capability_lease_ref: Some(CapabilityLeaseRef {
            lease_id: "analysis-read-lease".into(),
            issuer: "buyer".into(),
            expires_at_unix_ms: swarm_authority_examples::now_ms() + 60000,
            scope_digest: None,
        }),
        policy_evaluation_summary: Some(PolicyEvaluationSummary {
            server_a_verdict: allow("buyer-read-policy"),
            server_b_verdict: allow("vendor-read-policy"),
            joint_disposition: Some("allow".into()),
        }),
        consistency_model: Some("single-kernel".into()),
        ..Default::default()
    };
    let predicate = build_chio_bilateral_invocation_predicate(
        &receipt,
        identity("buyer", buyer_id.clone()),
        identity("vendor", vendor_id.clone()),
        "read_refund_case",
        swarm_authority_examples::now_ms(),
        extensions,
    )?;
    let statement = build_chio_bilateral_invocation_statement(&receipt, predicate)?;
    let payload = statement.canonical_bytes()?;
    let message = pae(PAYLOAD_TYPE_IN_TOTO, &payload);
    let mut envelope = DsseEnvelope {
        payload_type: PAYLOAD_TYPE_IN_TOTO.into(),
        payload: STANDARD.encode(&payload),
        signatures: vec![
            DsseSignature {
                keyid: buyer_id.0,
                sig: STANDARD.encode(buyer.sign(&message).to_bytes()),
            },
            DsseSignature {
                keyid: vendor_id.0,
                sig: STANDARD.encode(vendor.sign(&message).to_bytes()),
            },
        ],
    };
    verify_chio_bilateral_dsse_envelope(&envelope, &buyer.public_key(), &vendor.public_key())?;
    println!("strict invocation: both operator signatures verified");
    let (mut changed, _) = envelope.decode_statement()?;
    changed.predicate.tool_name = "write_refund_case".into();
    envelope.payload = STANDARD.encode(changed.canonical_bytes()?);
    println!(
        "changed payload: {}",
        verify_chio_bilateral_dsse_envelope(&envelope, &buyer.public_key(), &vendor.public_key())
            .expect_err("signatures must bind the payload")
    );
    Ok(())
}

Open src/bin/bilateral.rs

receipt.rsrust
use chio_core_types::{
    crypto::{sha256_hex, Keypair},
    receipt::{
        body::ChioReceiptBody,
        decision::{Decision, ToolCallAction},
        kinds::TrustLevel,
    },
};
pub fn receipt_body(kp: &Keypair) -> ChioReceiptBody {
    ChioReceiptBody {
        id: "rcpt-chio-bbs".to_string(),
        timestamp: swarm_authority_examples::now_ms() / 1000,
        capability_id: "cap-chio-receipt".to_string(),
        tool_server: "vendor-a.files".to_string(),
        tool_name: "read_refund_case".to_string(),
        action: ToolCallAction::from_parameters(serde_json::json!({
            "path": "/cases/refund-250.json"
        }))
        .expect("fixture action is valid"),
        decision: Some(Decision::Allow),
        receipt_kind: Default::default(),
        boundary_class: Default::default(),
        observation_outcome: None,
        tool_origin: Default::default(),
        redaction_mode: Default::default(),
        actor_chain: Vec::new(),
        content_hash: sha256_hex(b"{\"refund_minor\":25000}"),
        policy_hash: sha256_hex(b"chio-policy"),
        evidence: Vec::new(),
        metadata: Some(serde_json::json!({"workflow_id": "wf-chio-refund-001"})),
        trust_level: TrustLevel::Mediated,
        tenant_id: Some("buyer-tenant".to_string()),
        kernel_key: kp.public_key(),
        bbs_projection_version: None,
    }
}

Open src/receipt.rs

Captured result and refusal

Captured executable outputtext
strict invocation: both operator signatures verified
changed payload: origin (Org A) signature failed verification

The two signing boundaries run locally here. Lease and policy records are supplied declarations; independent policy evaluation and remote signer transport belong to the host. Direct DualSignedReceipt uses a separate signature profile.

Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.

The downloadable project creates one strict invocation statement, collects two signatures over its PAE bytes, and verifies them with the buyer and vendor public keys. After it alters the payload, the origin’s signature fails verification.

Read more in Verifier entry points.

Threshold authority travels beside the two signatures

An action whose ladder class requires co_sign: n_of_m adds a FROST authorization. The envelope keeps its two passport signatures, and the aggregate is a separate artifact, verified against its roster and epoch, then passed to verify_chio_bilateral_invocation_with_frost as VerifiedFrostAuthorization.

  • The predicate names a treaty_binding_ref, its consistency_model is quorum-required, and its consistency_anchor is frost-quorum.
  • The authorization’s action class matches the treaty action class, and its scope id matches the treaty id.
  • Its resource id matches invocation_id, and it is current at the pinned time.
  • A FROST authorization supplied with an ordinary bilateral mode is refused.

The binding compares those identifiers and the time. Recomputing the action digest from the tool arguments, claiming the current resource version and fence, and keeping the effect single-use with an idempotency key, restored state included, belong to the authorization and effect path.

Read more in FROST Quorum, which covers the roster, signing ceremony, slot lifecycle, and rotation.

Co-sign commitments, profiles, and diagnostics

Invocation commitments

  • Receipt subject. One subject named chio-receipt: plus the receipt id. Its SHA-256 matches the canonical receipt body.
  • Tool and arguments. tool_name matches the resolved receipt. tool_args_hash matches its parameter hash, which is recomputed from the stored parameters.
  • Receipt signer. The resolved receipt’s own signature verifies, and its kernel key equals the pinned tool host B key.
  • Policy summary. Both verdicts are allow or both are deny. Policy ids and versions are nonempty; joint_disposition agrees when present.
  • Capability lease. The lease resolves; issuer and exact expiry match, and expiry is strictly after the pinned time. Scope-digest presence and value match in both directions.
  • Governance evidence. A locally registered receipt-backed action class requires a resolvable governance receipt. Unknown tool names are refused.
  • Treaty binding. When supplied, the request, outcome, receipt, lease, governance, signer, and consistency references must satisfy the treaty checks.

tool_args_hash, capability_lease_ref, and policy_evaluation_summary are required even though their shared Rust representation uses Option. The compatibility-only schema and receipt_canonical_json fields must be absent from the strict predicate. Each kernel identity declares alg: ed25519 and a 64-character lowercase hexadecimal fingerprint.

Verdicts

The verifier accepts unanimous deny for audit and dispute review, as evidence that both operators agreed to refuse the action. The separate require_policy_evaluation_allow_admission helper requires unanimous allow before admission. A successful signature verification is never an instruction to execute.

Verifier entry points

Entry pointWhat it adds
verify_chio_bilateral_dsse_envelope
Strict envelope and predicate validation plus both signatures, using the two supplied public keys.
verify_chio_bilateral_invocation
Pinned identities, ladder-reference freshness, epoch revocation, resolved receipt and arguments, lease, governance, and treaty context.
verify_chio_bilateral_invocation_with_frost
The same context checks plus binding a separately verified FROST authorization to an n_of_m invocation.

Ladder references

The strict context check tests each supplied ladder reference for freshness at the chosen time. Fetching the manifest, checking its signature, and reconstructing its contents happen when the pins are established, through the trust process in Governance Ladder.

Artifact profiles

  • Strict invocation. chio.bilateral-cosign-invocation.v1 carries tool-argument and operational commitments and uses the strict entry points above.
  • Compatibility signature slice. chio.bilateral-signature-slice.v1 includes the receipt JSON inline and uses verify_dsse_envelope and verify_bilateral_cosign_invocation. The strict entry points refuse it.
  • Detached dual-signed receipt. DualSignedReceipt signs a separate CoSigningBody containing the receipt JSON and two organization ids, distinct from the DSSE statement and its PAE message.

Consistency models

The ordinary strict context path supports crdt-commutative. Any other consistency_model needs a treaty_binding_ref whose model matches. The vocabulary is crdt-commutative, totally-ordered, single-kernel, and quorum-required.

totally-ordered and quorum-required also need a nonempty consistency_anchor. The strict check validates these declared relationships; the action’s integration enforces the ordering contract the anchor names.

Workflows of pairwise records

A workflow can retain a separate bilateral envelope for each cross-organization step. StepRecord provides an optional bilateral_dsse_sha256 field, which the runtime buyer-closure path fills with the canonical JSON hash of the whole envelope, signatures included.

The workflow record therefore binds the exact envelope reviewed for a step. Verifying the workflow’s signature authenticates that record, and each bilateral claim is checked with its own envelope and context. Keep the envelope’s signature array in order when carrying the hash forward: reordering the two signatures leaves them valid but changes the canonical bytes being hashed.

Diagnostic codes

CodeInspect
dsse.malformed
Envelope type/count/encoding and the attached low-level context. This is also the fallback mapping for some structural errors.
statement.malformed
Payload JSON and whether its bytes are already canonical.
statement.schema_invalid
The Statement type and subject structure.
predicate.type_unrecognised
The artifact profile and chosen verifier entry point.
predicate.schema_invalid
Required fields, forbidden compatibility fields, and treaty/consistency/FROST relationships.
subject.digest_mismatch
Receipt resolution, body/name digest, receipt signature, tool name, and parameter hash.
peer.unpinned_or_keyid_mismatch
Kernel identity, configured key, predicate fingerprint, and receipt signer.
peer.revoked_at_epoch
The supplied revocation oracle’s result at the pinned epoch.
signature.server_a_invalid
A’s algorithm, expected fingerprint, and signature over the PAE bytes.
signature.server_b_invalid
B’s algorithm, expected fingerprint, and signature over the PAE bytes.
policy.verdict_disagreement
Both verdicts, policy provenance, and any joint disposition.
capability.lease_expired_or_unknown
Registry resolution, issuer, exact expiry, evaluation time, and scope-digest presence/value.
governance.receipt_required_missing
The action’s classification and its referenced governance record.
ladder.manifest_missing
Both peers’ pinned ladder references.
ladder.manifest_stale
The reference’s validity interval at the selected time.
governance.unknown_action_class
The tool name’s explicit registration in the verifier’s action-class map.

A code names a boundary and its context narrows the failure, since several checks share one code. Keep both when exporting evidence or diagnosing a refusal.

Publication, agreement, and execution

Publishing a signed artifact to a transparency log and validating its bilateral authorization are separate operations. The co-sign contract authenticates two named signers and checks the records around their agreement; whether an external action completed exactly once is a question for the effect path and its receipts.

Sources

Next: put a quorum behind the action