Selective Disclosure
Receipts are signed canonical JSON, hence cleartext. Cross-trust participation needs selective-disclosure proofs over receipts so two parties can prove “we authorised X” to a third without revealing the body.
Forward-looking concept (draft spec)
The hybrid choice
The default surface is BBS+ (bbs-2023 cryptosuite plus AnonCreds v2 RangeStatement predicates) with a zkVM escape hatch (Risc0 / SP1 + Groth16 wrap) for the cases BBS+ cannot reach. Ed25519 over JCS stays authoritative. BBS+ is a secondary commitment alongside the existing signature: a parallel bbs_messages() projection over ChioReceiptBody, with a separate per-kernel BBS keypair (BLS12-381 and Ed25519 do not compose).
BBS+ projection: receipt body to message vector
Each disclosable top-level field of ChioReceiptBody projects to one BBS message (one BLS12-381 scalar). The example below shows a body alongside the parallel projection it commits to. Field names match the spec exactly; ordering is alphabetical by serde field name and frozen by bbs_projection_version.
{
"schema_id": "chio.bbs-projection.receipt.v1",
"subject": {
"id": "rcpt_a1b2c3d4e5f6",
"capability_id": "cap_7f3a...e91d",
"tool_server": "srv-files",
"tool_name": "read_file",
"decision": "allow",
"timestamp": 1744537862,
"tenant_id": "tenant-ops-1",
"trust_level": "verified",
"kernel_key": "ed25519:pub:9c7b3f...",
"content_hash": "sha256:d7e8f9a0...",
"policy_hash": "sha256:b5c6d7e8...",
"action": { "...": "wholesale-only sub-body" },
"evidence": [ { "...": "wholesale-only" } ],
"metadata": { "...": "wholesale-only sub-body" },
"bbs_projection_version": "chio.bbs-projection.receipt.v1"
},
"bbs_messages": [
{ "index": 0, "field": "action", "encoding": "H", "notes": "wholesale-only" },
{ "index": 1, "field": "capability_id", "encoding": "S", "notes": "disclosable" },
{ "index": 2, "field": "content_hash", "encoding": "Hx", "notes": "disclosable" },
{ "index": 3, "field": "decision", "encoding": "H", "notes": "wholesale-only" },
{ "index": 4, "field": "evidence", "encoding": "H", "notes": "wholesale-only" },
{ "index": 5, "field": "id", "encoding": "S", "notes": "disclosable" },
{ "index": 6, "field": "kernel_key", "encoding": "H", "notes": "disclosable" },
{ "index": 7, "field": "metadata", "encoding": "H", "notes": "wholesale-only (None -> 'null')" },
{ "index": 8, "field": "policy_hash", "encoding": "Hx", "notes": "disclosable" },
{ "index": 9, "field": "tenant_id", "encoding": "Opt<S>","notes": "disclosable" },
{ "index": 10, "field": "timestamp", "encoding": "U64", "notes": "cmp-able" },
{ "index": 11, "field": "tool_name", "encoding": "S", "notes": "disclosable" },
{ "index": 12, "field": "tool_server", "encoding": "S", "notes": "disclosable" },
{ "index": 13, "field": "trust_level", "encoding": "S", "notes": "disclosable" }
]
}Encoding shorthand: S is UTF-8 bytes hashed to scalar; H is SHA-256 over canonical JSON of a structured sub-body, mapped to scalar (wholesale-only); Hx is hex-decoded 32-byte SHA-256 mapped to scalar; U64 is little-endian. Wholesale-only rows carry a single message and may be revealed in full or kept hidden, but v0.1 clauses cannot reach inside; predicates over individual GuardEvidence elements defer to the v0.2 zkVM lane.
Range-proof worked example
A buyer auditor wants to prove that a settlement amount stayed below a contractually agreed cap, without revealing what the amount actually was. The amount is committed under a parallel BBS message (per the 3-vendor spec example, cost.amount_minor as U64 scalar). The predicate uses AnonCreds v2 RangeStatement notation over the BBS commitment.
# Cap-disclosure clause: prove "settlement amount <= $250.00"
# without revealing the amount.
predicate_clauses:
- kind: cmp
field: refund_amount_minor # parallel BBS message bound to cost
op: "<="
const_value: 25000 # $250.00 in minor units
scale: 2 # USD: 2 decimal places
# AnonCreds v2 RangeStatement (Bulletproofs commitment + range proof)
# over the BBS commitment for refund_amount_minor:
#
# RangeStatement {
# commitment: Ck(refund_amount_minor), # BLS12-381 commitment
# bound_op: LessThanOrEqual,
# bound_value: 25000,
# scale_decimal: 2,
# }
#
# What the verifier sees:
# - the disclosed step fields (step_index, server_id, tool_name, outcome)
# - the predicate_id and the RangeStatement bytes
# - PoK verifies; range proof verifies under the BBS commitment
#
# What the verifier does not see:
# - refund_amount_minor itself
# - any other amount in the workflow
# - upstream agent prompts, customer identifier, or step orderingThree driving use cases
The three use cases that motivate the BBS+ choice are structurally identical. Each reduces to: a verifier wants a predicate over a signed receipt body without learning the body. What differs is the predicate shape and what the prover keeps private.
Cybersec confidence threshold
A SOC peer wants to prove that local concentration on a malicious indicator crossed a confidence threshold the peers had agreed on, without revealing the threshold value or which deposits contributed. The confidence value projects as a parallel BBS message; the clause is cmp(confidence, >=, T, scale=2). Disclosed: that the threshold was crossed. Private: the threshold value, the contributing deposit identifiers, and the indicator itself.
Finance amount cap
A buyer auditor wants to prove that the cross-vendor invocation cost across a workflow stayed below the buyer’s authorised cap. The clause is cmp(refund_amount_minor, <=, 25000, scale=2). Disclosed: that the cap held. Private: the actual refund amount, the customer identifier, and the upstream agent prompts. This is the worked example from the 3-vendor fixture (G6 closure).
Compliance tier floor
A compliance verifier wants to prove that an action was authorised at or above a KYC tier floor, without revealing the actual tier or the underlying evidence. The clause is cmp(kyc_tier, >=, 2, scale=0) over a child receipt’s projection that contributes a kyc_tier message. Disclosed: that the tier floor held. Private: the actual tier, the evidence chain, and the customer identifier.
When to use the zkVM escape hatch
BBS+ has a frozen predicate language: three primitives (eq, cmp, member),AND-only composition, and a hard ceiling of eight clauses. Anything past that is the zkVM lane (Risc0 or SP1 + Groth16 wrap). The four motivating cases:
- Chained-receipt proofs. Properties over N receipts: “every receipt in this child chain shares the same parent capability lease and falls within a 5-minute window.” BBS+ commits to one body; chained proofs require a chain-walking circuit.
- Predicates over the Ed25519 signature itself. “Signed by a key in this set” without revealing which.
- Non-arithmetic boolean logic.
OR, negation, nested quantifiers, anything past v0.1’s AND-of-eight. - Predicates over wholesale-only fields. v0.2 zkVM circuits crack nested bodies (
evidence,metadata,outcome) and prove sub-field predicates without disclosing them.
v0.1 reserves the optional verifier_image_hash envelope field so a v0.2 zkVM proof can ride the same envelope, with the image binding as a public input. Proof-bytes wire format is not specified in v0.1.
Open: projection ordering
v0.1 picks alphabetical by serde field name, frozen by bbs_projection_version. The alternative was schema-declared ordering (a sidecar manifest listing the fields in some chosen order). Alphabetical wins for three reasons. First, inserting a field forces a new projection version rather than silently shifting indices. Second, it is mechanically reproducible from the struct with no sidecar manifest. Third, RFC 8785 already mandates alphabetical key ordering for canonical JSON, so projection and JCS walk the same fields in the same sequence. The cost is schema-evolution friction: any rename rebuilds the projection. Schema-declared ordering would have been friendlier to evolution but harder to verify reproducibly without the schema in hand. The CHIODOS_PHEROMONE.md projection (v0.2) has not committed; review should confirm alphabetical for both or flag the divergence as a known cross-spec inconsistency.
Pheromone disclosure
Selective disclosure is also the answer for sensitive pheromone deposits. A peer can prove that local concentration on a subject class crossed a threshold without revealing which deposits contributed or what the subject class was, by projecting the relevant fields and producing a range proof. This is the v0.2 deferred lane in the disclosure spec; it exists in draft but is not the default path.