3-Vendor Walkthrough
Run a three-vendor refund workflow, then read how each signed record lets an auditor check the handoffs offline.
On this page
You can split one refund case across three vendors and give an auditor one package showing which organization ran each step, under whose authority. Each vendor signs its own contribution, a second signer approves the destructive step, and the buyer’s workflow receipt joins the records.
The three-vendor example builds that package for workflow wf-chio-refund-001: a refund case read, a customer check, and a staged refund, the same three steps behind pricing.refund when a refund needs outside help. The generator signs every record locally and checks the result against a trust bundle the auditor supplies; it contacts no live vendor and moves no money. You are the buyer in this story; the package names you by role, buyer.
The verifier accepts the package on eleven checks
Pull the example, then run the generator with --report to print the verifier’s report instead of the package itself. Add --out-dir ./three-vendor-output to write the generated package to a directory.
chio new three-vendor --example three-vendor
cd three-vendor$ cargo run --locked --bin generate-chio-proof-package -- --report \
| jq -r '"accepted: \(.accepted) revocationEpochHeight: \(.revocationEpochHeight)",
$ (.checks[] | "\(if .passed then "pass" else "FAIL" end) \(.code)")'accepted: true revocationEpochHeight: 64 pass workflow.kernel_signature pass trust.package_hints pass workflow.intersection pass workflow.vendor_cosignatures pass workflow.step_links pass governance.lease_scope_bindings pass governance.capability_leases pass governance.receipts pass federation.strict_bilateral_invocations pass trust.bbs_issuer pass bbs.selective_disclosure
You should see accepted: true at revocation epoch height 64, then eleven pass rows after the package and context prechecks. In order, they cover the buyer’s signature, the trusted package bindings and workflow intersection, the vendor signatures, the step links, the lease-scope bindings, the capability leases, the governance receipts, the strict bilateral invocations, the BBS issuer, and the disclosure proof.
The report also records the package, trust-bundle, and context digests and the pinned revocation epoch, so each acceptance is tied to its exact inputs. An acceptance means the evidence satisfies the trust bundle and context you supplied, checked without relying on the buyer’s summary. Whether a customer was checked or a refund settled lies outside the receipts.
One buyer and three vendors each sign their own part
The buyer runs the refund-underwriting skill under its cap-chio-workflow grant and issues one lease per vendor. Each vendor kernel runs one step and signs what it did.
ρ0read_refund_caseρ1verify_customerρ2stage_refund
One receipt chains the three vendor steps
Workflow wf-chio-refund-001 runs read_refund_case at vendor A, verify_customer at vendor B, and stage_refund at vendor C under the buyer’s cap-chio-workflow grant. Each step record hashes the one before it, and the buyer and all three vendors sign the aggregate receipt. 3-Vendor Walkthrough.
| Participant | Contribution | Signs |
|---|---|---|
| Buyer | Owns the workflow and issues each vendor’s lease | The aggregate workflow receipt |
| Vendor A, step 0 | read_refund_case on vendor-a.files | Tool receipt, bilateral envelope, aggregate |
| Vendor B, step 1 | verify_customer on vendor-b.kyc | Tool receipt, bilateral envelope, aggregate |
| Vendor C, step 2 | stage_refund on vendor-c.payments | The same three, plus a separate destructive authorization |
| Buyer governance | Authorizes the destructive step inside the lease window | The governance receipt |
| Verifier | Evaluates the package under its own trust bundle and context | The acceptance or failure report |
Identities such as did:chio:vendor-c are readable scenario identifiers that the fixture’s trust data binds to explicit public keys. They stand in for generated workload credentials.
The verifier judges the package by its own trust bundle
The package carries four peer bindings, one each for the buyer and the three vendor kernels, and each names a public key and a governance-ladder manifest. The verifier treats them as hints and checks them against the trust bundle it was given. The package’s workflow-intersection artifact binds each step to its action class and peer kernel:
"stepClassBindings": [
{
"stepIndex": 0,
"toolName": "read_refund_case",
"actionClassId": "read_refund_case",
"peerKernelId": "did:chio:vendor-a"
},
{
"stepIndex": 1,
"toolName": "verify_customer",
"actionClassId": "verify_customer",
"peerKernelId": "did:chio:vendor-b"
},
{
"stepIndex": 2,
"toolName": "stage_refund",
"actionClassId": "stage_refund",
"peerKernelId": "did:chio:vendor-c"
}
],The trust bundle supplies the accepted peers, vendor signers, action classes, lease and governance authorities, disclosure policy, and revocation context. Including a key in the package gives that key no standing. The intersection also names the workflow grant, the pairwise relationships, the required vendor signers, and the aggregate body digest; the verifier recomputes that digest and checks the intersection’s identifier and digest against the bundle. The fixture builds the four bindings locally, with no network handshake among four running kernels.
In the run, the trust.package_hints and workflow.intersection rows check these inputs.
Read more in Governance Ladder, which shows a peer’s ladder binding, and Capability Leases.
Each vendor step carries a lease, its own receipt, and a jointly signed statement
Four records cross every vendor boundary, and the destructive step adds a fifth:
- Capability lease. Buyer-issued authority for one subject, scope digest, action class, and validity window.
- Lease-scope binding. Ties the lease to the workflow grant, step index, peer, tool, action class, and canonical tool-argument hash.
- Vendor tool receipt. The vendor’s recorded decision with its request, policy, and content commitments. Step 0’s receipt names
vendor-a.files,read_refund_case, andlease-vendor-a-read, records{"verdict":"allow"}, and keeps the arguments and their hash inaction. - Bilateral envelope. The buyer’s and the vendor’s signatures over one invocation statement.
- Governance receipt, step 2 only. A separate authority’s authorization naming the lease, the workflow, and the tool-receipt body hash.
Each envelope is DSSE around an in-toto statement that names the vendor receipt. Its bilateral predicate binds both kernel identities, the tool and argument hash, the lease reference, and the policy and governance terms. The producer builds that predicate and has both kernels sign the same DSSE PAE message:
let extensions = BilateralPredicateExtensions {
capability_lease_ref: Some(CapabilityLeaseRef {
lease_id: lease.body.lease_id.clone(),
issuer: lease.body.issuer.clone(),
expires_at_unix_ms: lease.body.expires_at_unix_ms,
scope_digest: Some(HashRecord {
alg: "sha256".to_string(),
value: lease.body.scope_digest.clone(),
}),
}),
policy_evaluation_summary: Some(policy_summary(vendor)),
governance_receipt_ref: governance_ref,
consistency_anchor: Some(format!("chio:consistency:{WORKFLOW_ID}:{index}")),
consistency_model: None,
cross_org_visibility: None,
treaty_binding_ref: None,
};
let envelope = sign_chio_bilateral_dsse_envelope(
&receipt,
&buyer_key,
&vendor_key,
BUYER_KERNEL_ID,
vendor.kernel_id,
vendor.tool_name,
GENERATED_AT_UNIX_MS,
extensions,
)
.map_err(|error| ChioPackageError::Federation(error.to_string()))?;The vendor’s own receipt signature stays a separate layer. The verifier checks each envelope offline under the strict bilateral profile, with the trust bundle’s peer pins, action classes, and pinned revocation epoch, and requires a joint verdict of allow. Chio also has a direct co-signing form, DualSignedReceipt; this package uses DSSE envelopes. Three report rows cover these records: governance.lease_scope_bindings, governance.capability_leases, and federation.strict_bilateral_invocations.
The capability lease record
pub struct CapabilityLeaseArtifact {
pub schema: String,
pub lease_id: String,
pub issuer: String,
pub subject: String,
pub scope_digest: String,
pub action_class: CapabilityLeaseActionClass,
pub issued_at_unix_ms: u64,
pub expires_at_unix_ms: u64,
}Read more in Bilateral Co-Sign, Bilateral Receipts for DualSignedReceipt, and Receipt Format for the signed receipt body.
The destructive step needs a second signer inside the lease window
Steps 0 and 1 run under delegated_action leases. Step 2 runs under narrow_destructive: the buyer kernel signs its lease, and did:chio:buyer-governance signs a governance receipt that authorizes it.
"governanceReceipts": [
{
"body": {
"schema": "chio.governance-receipt.v1",
"receiptId": "gov-refund-stage-authorization",
"authorizingKernel": "did:chio:buyer-governance",
"caseKind": "destructive_authorization",
"authorizedLeaseId": "lease-vendor-c-refund",
"workflowId": "wf-chio-refund-001",
"stepSha256": "31118bb62ec8d467670bfc3b8fc6ad88e6946380bdfb51486a0f2daa4781b3f9",
"issuedAtUnixMs": 1765999980000,
"expiresAtUnixMs": 1766000020000
},
"signerKey": "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
"signature": "0e871e80b21e7dd57c46698168bc2ea993ce25130dff5cd0a90c7bb38cdc3cfa2e8ffcede775cdaf7f71db33544d54e225b5f0bd99a88634dcd6745927ea6809"
}
],The authorization lasts 40 seconds inside the lease’s 60-second window. The verifier checks the governance receipt against the expected lease and workflow. Here stepSha256 is the canonical hash of the vendor’s tool-receipt body, a different commitment from the previous-StepRecord hash the aggregate chain uses. The governance.receipts row is that check.
Read more in Capability Leases, which defines the governance receipt record.
One workflow receipt chains the steps under four signatures
The buyer signs a workflow receipt containing all three StepRecords, and each vendor adds a detached signature over the same canonical body. Joint authorship exists at two levels: the bilateral invocation and the complete aggregate.
"steps": [
{
"step_index": 0,
"server_id": "vendor-a.files",
"tool_name": "read_refund_case",
"allowed": true,
"tool_receipt_id": "ea1f74990eaef54beaac9d7af04893a2320390b773b556fbd310fa9246ed3b3d",
"outcome": "success",
"duration_ms": 12,
"cost": {
"units": 100,
"currency": "USD"
},
"output_hash": "6c7596557f061fb92c19195dd7184598e026bb5212185a1231f6bcefc6fee009",
"bilateral_dsse_sha256": "3e7ac510ae43d4668966534d81c7592d2c7bb62d10e7938e1bb7c9e477c49077",
"consistency_anchor": "chio:consistency:wf-chio-refund-001:0"
},
{
"step_index": 1,
"server_id": "vendor-b.kyc",
"tool_name": "verify_customer",
"allowed": true,
"tool_receipt_id": "cb31b1ffad667916f951842002f66c0357556ebe2dc58bb5664e920611516cfc",
"outcome": "success",
"duration_ms": 18,
"cost": {
"units": 200,
"currency": "USD"
},
"output_hash": "20d1a73014db4806b985f39c23c7391e269b66a7bfcbf13f014636c13146dd0f",
"bilateral_dsse_sha256": "bd3b5f81c03b8ba749a2a4b4af630787cf0a9716e464ad6bf136a3ac6ca8bee5",
"parent_receipt_sha256": "31b6b050e93f0bf36f3232d4f34b72222a879a3196f2c10d7d1607cbc3a0e5bb",
"consistency_anchor": "chio:consistency:wf-chio-refund-001:1"
},
{
"step_index": 2,
"server_id": "vendor-c.payments",
"tool_name": "stage_refund",
"allowed": true,
"tool_receipt_id": "d342a59dd9d25aa977b098b3f847b89d01ea11501f9ba3c99a4348156fba2e3f",
"outcome": "success",
"duration_ms": 12,
"cost": {
"units": 250,
"currency": "USD"
},
"output_hash": "c9abf68ae26e738826dabd29da7f6c623608221b253a70e6be1ec05bd9803987",
"bilateral_dsse_sha256": "c03843a008c00646de37e4006857b35fb0347827d1b8f4186ee87e0c0ecb97b6",
"governance_receipt_id": "gov-refund-stage-authorization",
"parent_receipt_sha256": "9cbe4438bc3128da868d1f207a74f7f112fb45c5f3565a635bc98886ad017121",
"consistency_anchor": "chio:consistency:wf-chio-refund-001:2",
"destructive": true
}
],Each field in a step names one relationship:
parent_receipt_sha256hashes the canonical previous StepRecord; step 0 has no predecessor.bilateral_dsse_sha256hashes this step’s canonical bilateral envelope.tool_receipt_idnames the vendor tool receipt, and the envelope’s invocation identifier must agree.governance_receipt_idnames the destructive authorization that step 2 and its envelope both reference.
Hashing the previous StepRecord covers the recorded cost, outcome, duration, and governance pointer along with the evidence references. The aggregate signature covers the complete body, including the last step, which has no successor to commit to it.
The costs are 100, 200, and 250 USD minor units, 550 in total: USD 1.00, USD 2.00, and USD 2.50, or USD 5.50. They are the declared workflow costs, separate from the refund amount. The report checks both signature layers and the chain as workflow.kernel_signature, workflow.vendor_cosignatures, and workflow.step_links.
Signing code and the captured predecessor hashes
let mut workflow_receipt = WorkflowReceipt::sign(workflow_body, &buyer_key)
.map_err(|error| ChioPackageError::Workflow(error.to_string()))?;
for vendor in &VENDORS {
let key = Keypair::from_seed(&vendor.seed);
workflow_receipt
.add_vendor_signature(vendor.vendor_id, &key)
.map_err(|error| ChioPackageError::Workflow(error.to_string()))?;
}This command reads the package in the example’s fixtures/ directory and reproduces its two predecessor hashes. The jq key-sort shortcut works for these values; general signing and verification use Chio’s canonical JSON. StepRecord and its MonetaryAmount cost are defined in Workflow.
$ F=./fixtures/buyer-auditor-proof-package.json
$ for i in 0 1; do
$ printf 'sha256(canonical steps[%s]) %s\n' "$i" \
"$(jq -cSj ".workflowReceipt.steps[$i]" "$F" | sha256sum | cut -d' ' -f1)"
$ done
$ jq -r '.workflowReceipt.steps[1,2]
$ | "steps[\(.step_index)].parent_receipt_sha256 \(.parent_receipt_sha256)"' "$F"sha256(canonical steps[0]) 31b6b050e93f0bf36f3232d4f34b72222a879a3196f2c10d7d1607cbc3a0e5bb sha256(canonical steps[1]) 9cbe4438bc3128da868d1f207a74f7f112fb45c5f3565a635bc98886ad017121 steps[1].parent_receipt_sha256 31b6b050e93f0bf36f3232d4f34b72222a879a3196f2c10d7d1607cbc3a0e5bb steps[2].parent_receipt_sha256 9cbe4438bc3128da868d1f207a74f7f112fb45c5f3565a635bc98886ad017121
The disclosure proof opens four fields of the same aggregate
The BBS proof takes the aggregate body digest as its subject, which ties it to the workflow being audited. It opens four of fourteen messages: id, session_id, skill_id, and skill_version, at indices 4, 8, 9, and 10.
pub struct ChioProofClaims {
pub bbs_reveal_set: bool,
pub hidden_range_predicates: bool,
pub vc_data_integrity_bbs: bool,
pub zkvm: bool,
}The proof establishes that reveal set under the trusted BBS issuer and verification context. The package refuses claims of hidden range predicates, VC Data Integrity BBS interoperability, or zkVM verification. A matching digest and a package-provided intersection both leave the verifier’s own trust configuration in charge.
The complete audit package also carries the full workflow receipt and tool receipts, so its recipient can read the steps, costs, and destructive flag there. Leaving a message closed in the proof hides nothing the package carries elsewhere. The last two report rows, trust.bbs_issuer and bbs.selective_disclosure, check the issuer and the proof.
Read more in Selective Disclosure, which covers the projection, the full proof, and the reveal-policy contract.
Changing one vendor’s output breaks the buyer’s signature
The downloadable swarm-authority project calls the same producer, verifies the package under an explicit trust bundle and verification context, then changes step 0’s declared output hash and verifies again. Set it up with the Rust example setup.
cargo run --locked --features workflow --bin three-vendorthree-vendor package: {"schema":"chio.attest.verifier-report.v1","packageSha256":"642e30c16d45e86a2d4a4aa4b490cb14784b00931edefa322c52568713134cc7","trustBundleSha256":"1237cb2ecc390fb996ff59d36566689711ab270ce457ff2abf6eee8dc56fe42c","contextSha256":"4758a93f2dbc16256105ce6d47a4efdd39d477733dcb2a5e80075a3e1aaddcce","revocationEpochHeight":64,"accepted":true,"checks":[{"code":"workflow.kernel_signature","name":"workflow-kernel-signature","passed":true},{"code":"trust.package_hints","name":"package-trust-hints","passed":true},{"code":"workflow.intersection","name":"workflow-intersection","passed":true},{"code":"workflow.vendor_cosignatures","name":"workflow-vendor-cosignatures","passed":true},{"code":"workflow.step_links","name":"workflow-step-links","passed":true},{"code":"governance.lease_scope_bindings","name":"lease-scope-bindings","passed":true},{"code":"governance.capability_leases","name":"capability-leases","passed":true},{"code":"governance.receipts","name":"governance-receipts","passed":true},{"code":"federation.strict_bilateral_invocations","name":"strict-bilateral-invocations","passed":true},{"code":"trust.bbs_issuer","name":"trusted-bbs-issuer","passed":true},{"code":"bbs.selective_disclosure","name":"bbs-selective-disclosure","passed":true}]}
changed vendor result: workflow verification failed: workflow signature is invalidYou should see the accepted report, then workflow signature is invalid for the changed package. Verification stops at the first failure; the report-returning library API keeps the completed checks and adds a failure code, phase, and detail, so a refused package can show fewer than eleven rows. The example’s negative cases pin four more mutations to their failure codes:
governance: the destructive step’s governance receipt is removed.workflow.intersection: the trusted workflow-intersection digest changes.bbs.context_nonce: the verifier supplies a different challenge.package.claim: the package claims an unsupported zkVM proof.
The example program
use chio_attest_loopback::{
fresh_proof_package, verification_context, verifier_trust_bundle, verify_package,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// This producer constructs signed artifacts locally. No vendor service or
// payment rail is contacted. Its fixed evaluation context belongs to this
// artifact walkthrough, not to a current-time production admission decision.
let mut package = fresh_proof_package()?;
let trust = verifier_trust_bundle()?;
let context = verification_context();
let report = verify_package(&package, &trust, &context)?;
println!("three-vendor package: {}", serde_json::to_string(&report)?);
std::fs::write(
"three-vendor-package.json",
serde_json::to_vec_pretty(&package)?,
)?;
package.workflow_receipt.steps[0].output_hash = Some("0".repeat(64));
println!(
"changed vendor result: {}",
verify_package(&package, &trust, &context)
.expect_err("aggregate signature must bind steps")
);
Ok(())
}Captured on darwin arm64, exit 0. The program signs locally under the walkthrough’s fixed context and checks the expected refusal itself; invalid command inputs exit nonzero. Shared constructors and configuration come with the download. Keys, digests, and timestamps vary between runs.
Extend it: tamper with the last step
The chain hashes predecessors, so step 2 has no later record committing to it. Point the example’s mutation at step 2 and rerun the command above:
// Replace the steps[0] mutation with the last step, which no later record hashes.
package.workflow_receipt.steps[2].output_hash = Some("0".repeat(64));You should see the same workflow signature is invalid failure, because the buyer’s signature covers the complete body. A deployment produces the same records from its own execution path and chooses which issuers and statements it trusts.
Read the example and fixtures, the package producer that separates fixture input from runtime receipts, and the buyer-side verifier.