Disclosure Lineage
Check another operator’s disclosed evidence against its policy, provenance graph, leakage ledger, and signed crypto report before accepting it.
On this page
An operator can accept a partner’s disclosed evidence knowing which fields were released, which policy allowed them, which records they came from, and which inferences the producer declared.
The disclosure-lineage verifier checks those relationships offline, from the bundle and the trust choices you supply, without querying the producing kernel. The BBS proof inside the bundle is the subject of Selective Disclosure; this mechanism checks the policy and provenance contract around it.
A redacted node keeps its place in the signed lineage
The positive fixture’s signed subgraph has two nodes: receipt-root at depth 0, observed and disclosed, and receipt-child at depth 1, derived and redacted for privacy_profile.
A redacted node keeps its place in the lineage
receipt-root sits at depth 0 with no parents, and receipt-child at depth 1 declares it as parent through a continued_by edge. The child is redacted for privacy_profile, yet its receipt reference and every other node field stay in the signed subgraph, and the subgraph signature covers the redaction state. Disclosure Lineage.
A continued_by edge links the root to the child, and the child declares the root as its parent. Redaction keeps the child’s receipt reference and every other node field, and the subgraph signature covers the redaction state. The frontier is every node with no outgoing edge: the verifier sorts their id:digest:depth strings, joins them with |, and hashes the result, redacted leaves included.
With the fixture family generated, as the fixture run below does, chio lineage roots lists the root and pinned frontier of each subgraph in the directory:
chio lineage roots --dir ./lineageroot frontier frontier_sha256 nodes edges receipt-root receipt-child (depth 1, redacted) 5be1…0c7d 2 1 1 pinned frontier in ./lineage
You should see the redacted child listed as the frontier. Its digest is the value the verifier recomputes from the graph and compares with the signed declaration.
Because an extra edge or a missing node changes the frontier, the digest commits to the graph’s shape. The lineage-frontier-mismatch negative fixture re-signs a subgraph whose declared frontier_sha256 disagrees with the frontier computed from its graph, and verification refuses it with proof-room.negative.lineage-frontier-digest-mismatch.
Read more in Graph checks and Digest meanings.
Five artifacts are accepted only as one connected record
The library accepts a bundle after it checks each artifact’s shape and then the references that tie them together. Each artifact contributes one part of the claim:
- Disclosure capsule. The disclosed-field list, the hidden-predicate declarations, and the references that connect the other four artifacts.
- Privacy profile. Field and predicate allow and forbid lists, sensitivity classes, count limits, and cryptographic-policy requirements.
- Signed lineage subgraph. Nodes, edges, redaction states, roots, depths, and digest declarations, signed by a lineage signer.
- Leakage ledger. One classified entry per disclosed field, declared predicate, and required derived fact, with scores and explanatory notes.
- Crypto-context report. A report signer’s statement that the proof and its presentation-context policy verified.
References are equality checks over the supplied objects: the capsule names the subgraph, ledger, profile, and report it belongs with, and the capsule, subgraph, ledger, and profile carry the same transaction-passport reference. You choose the acceptable privacy profile and both signer sets. A profile included in a bundle is an input you compare, and it carries no authority of its own.
Read more in Bundle artifacts and Reference bindings.
The leakage ledger accounts for every declared disclosure
The producer declares each released field and inference in the ledger; the verifier checks the declarations against the profile and requires coverage for the capsule’s disclosures.
Every entry has a classification, a policy rule, a score, and a disclosure kind: disclosed_field, hidden_predicate, or derived_fact. A capsule with any disclosed field or declared predicate must also cover three derived facts: derived.crypto.issuer_status, derived.crypto.revocation_freshness, and derived.crypto.presentation_timing. A hidden predicate, a score above 1, or declared cross-tenant risk requires a non-empty residual-inference note.
The positive fixture discloses capability_id and tool_name, declares amount_lte_100, and covers the three derived facts. Its six scores total 7 against a ledger maximum of 7, and its profile permits two disclosed fields and one hidden predicate.
Read more in Count limits and scores and Predicate declarations.
You choose the signers, and the CLI adds proof and context checks
The lineage signer set and the crypto-report signer set are independent. The CLI reads them as two roles in your trust file, disclosure.lineage-signer and disclosure.crypto-report-signer, and a key is trusted in both roles only when you pin it to both. An empty set trusts nobody, so the convenience verifier with default trust refuses signed bundles, and the CLI stops with trust:role-unpinned when either role has no pin.
Both signatures use the carrier sig-ed25519:<public-key>:<signature>. The subgraph signs its hexadecimal digest as text; the crypto report signs its canonical form with the signature field cleared. A self-declared digest-only signature is refused.
The pure library verifies the signed crypto report. The CLI disclosure path also loads the proof, context, and projection manifest from the evidence graph, checks their bindings, re-verifies the BBS proof under the supplied context policy, requires a Verified result, and compares the resulting claim and disclosed-field sets with the report. When the context or profile requires anchored transparency, it also checks an inclusion proof. Its context evaluation reads the key, freshness, replay, holder, and transparency observations described in Selective Disclosure, as supplied by your integration.
The fixture run accepts one bundle and refuses the rest at their first failure
The library checks capsule shape, profile shape, lineage and its signature, the ledger, cross-artifact bindings, disclosure policy, ledger coverage, and finally the report signature. It returns the first error and builds a verifier report only after every check passes. The doctor replays the fixtures built into chio against their own trust:
$ chio proof doctor --scenario disclosure-lineagechio proof doctor disclosure-lineage [passed] disclosure_lineage_valid_ledger: transaction passport verified [passed] disclosure_lineage_missing_ledger_entry: transaction passport rejected [passed] disclosure_lineage_unknown_lineage_root: transaction passport rejected verdict: passed
You should see one valid bundle verified and two expected refusals. A negative case reports passed when the verifier rejected it for the expected reason. To run the negatives yourself, generate the family with its trust file:
$ chio proof fixture generate disclosure-lineage-ledger --with-negatives --out ./lineagegenerated disclosure-lineage-ledger ./lineage trust ./lineage/trust.toml (fixture profile, 3 roles) negatives 15 under ./lineage/negatives
Then verify four of them in one command and require each to be refused:
$ chio proof verify \
./lineage/negatives/missing-ledger-entry \
./lineage/negatives/unknown-lineage-root \
./lineage/negatives/forbidden-disclosed-field \
./lineage/negatives/lineage-depth-regression \
--trust ./lineage/trust.toml --expect refuseREFUSED missing-ledger-entry disclosed field absent from leakage ledger: tool_name REFUSED unknown-lineage-root unknown lineage root receipt: receipt-missing REFUSED forbidden-disclosed-field disclosed field forbidden by privacy profile: tool_name REFUSED lineage-depth-regression lineage node depth not greater than parent: receipt-child 4 of 4 refused, as expected trust ./lineage/trust.toml (fixture keys)
You should see four refusals, each naming the first contract its fixture breaks. The two tool_name failures come from different fixtures: one lacks its ledger entry, and the other violates the profile. The summary line and exit status 0 confirm every input was refused as expected, so the same command works as a CI check. Repair the first reported contract, then verify the complete bundle again.
Worked Rust example: Verify a connected disclosure lineage
Verify a connected disclosure lineage
Construct a privacy profile, capsule, signed two-node lineage, leakage ledger and signed crypto-context report. Verify their shared references, then change the child's digest and re-sign without changing its frontier commitment.
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
cargo run --locked --features lineage --bin lineageImplementation
use chio_core_types::{sha256_hex, Keypair};
use chio_disclosure_lineage::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut bundle = build_lineage_bundle()?;
let report = verify_bundle(&bundle)?;
println!("lineage: {}", serde_json::to_string(&report)?);
bundle.lineage.nodes[1].artifact_sha256 = digest("changed-child");
// Re-sign to ensure this exercises the connected digest contract, not merely
// a stale signature. The frontier commitment deliberately remains unchanged.
bundle.lineage.subgraph_sha256 = compute_signed_lineage_subgraph_digest(&bundle.lineage)?;
bundle.lineage.signature = sign_lineage_subgraph(&bundle.lineage, &lineage_signer())?;
println!(
"changed child digest: {}",
verify_bundle(&bundle).expect_err("frontier must bind the changed child")
);
Ok(())
}
fn build_lineage_bundle() -> Result<DisclosureLineageBundle, Box<dyn std::error::Error>> {
let capsule = DisclosureCapsule {
schema: DISCLOSURE_CAPSULE_SCHEMA_V1.to_string(),
id: "disclosure-capsule-valid".to_string(),
transaction_passport_ref: "passport-disclosure-valid".to_string(),
crypto_context_report_ref: "crypto-context-report-valid".to_string(),
projection_manifest_ref: "bbs-projection-manifest-valid".to_string(),
privacy_profile_ref: "privacy-profile-valid".to_string(),
lineage_subgraph_ref: "lineage-subgraph-valid".to_string(),
leakage_ledger_ref: "leakage-ledger-valid".to_string(),
disclosed_fields: vec!["capability_id".to_string(), "tool_name".to_string()],
hidden_predicates: vec![amount_cap_hidden_predicate()],
};
let privacy_profile = DisclosureVerifierPrivacyProfile {
schema: DISCLOSURE_VERIFIER_PRIVACY_PROFILE_SCHEMA_V1.to_string(),
profile_id: "privacy-profile-valid".to_string(),
allowed_proof_mechanisms: vec!["bbs".to_string()],
required_holder_binding: Some("holder:buyer-agent".to_string()),
transaction_passport_ref: "passport-disclosure-valid".to_string(),
leakage_budget: DisclosureProfileLeakageBudget {
max_disclosed_fields: 2,
max_hidden_predicates: 1,
},
sensitivity_classes: vec![
DisclosureSensitivityClass {
class_id: "capability_identifier".to_string(),
fields: vec!["capability_id".to_string()],
},
DisclosureSensitivityClass {
class_id: "tool_identity".to_string(),
fields: vec!["tool_name".to_string()],
},
DisclosureSensitivityClass {
class_id: "amount_or_budget".to_string(),
fields: vec!["amount_lte_100".to_string()],
},
DisclosureSensitivityClass {
class_id: "runtime_assurance".to_string(),
fields: vec![
"derived.crypto.issuer_status".to_string(),
"derived.crypto.revocation_freshness".to_string(),
],
},
DisclosureSensitivityClass {
class_id: "timing".to_string(),
fields: vec!["derived.crypto.presentation_timing".to_string()],
},
],
allowed_issuer_keys: vec![
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
],
required_key_epoch_min: 7,
forbidden_key_epochs: vec![9],
required_status_freshness_seconds: 300,
required_audience: "https://auditor.example/chio".to_string(),
nonce_policy: "no_replay".to_string(),
allowed_algorithms: vec!["bbs-bls12381-sha256".to_string()],
forbidden_algorithms: vec!["rsa-pkcs1v15-sha1".to_string()],
required_transparency_state: TransparencyState::Anchored,
max_presentation_age_seconds: 600,
allowed_disclosed_fields: vec!["capability_id".to_string(), "tool_name".to_string()],
forbidden_disclosed_fields: vec!["customer_email".to_string()],
allowed_hidden_predicates: vec!["amount_lte_100".to_string()],
forbidden_hidden_predicates: vec!["raw_amount".to_string()],
};
let frontier_sha256 = lineage_frontier_digest("receipt-child", &digest("receipt-child"), 1);
let checkpoint_ref = "checkpoint-disclosure-valid".to_string();
let mut lineage = SignedLineageSubgraph {
schema: LINEAGE_SIGNED_SUBGRAPH_SCHEMA_V1.to_string(),
id: "lineage-subgraph-valid".to_string(),
transaction_passport_ref: "passport-disclosure-valid".to_string(),
policy_profile_id: "privacy-profile-valid".to_string(),
generated_at: "2026-06-10T00:00:00Z".to_string(),
audience: "https://auditor.example/chio".to_string(),
challenge_nonce: "disclosure-lineage-fixture-nonce".to_string(),
frontier_sha256: frontier_sha256.clone(),
checkpoint_ref: checkpoint_ref.clone(),
checkpoint_inclusion_sha256: digest(&format!("{checkpoint_ref}|{frontier_sha256}")),
max_depth: 1,
required_evidence_class: "observed".to_string(),
lineage_anchor_ref: "lineage-anchor-local-fixture".to_string(),
redaction_map_sha256: digest("receipt-child|privacy_profile"),
leakage_ledger_sha256: digest("leakage-ledger-valid"),
projection_manifest_sha256: digest("bbs-projection-manifest-valid"),
root_receipt_ids: vec!["receipt-root".to_string()],
nodes: vec![
DisclosureSignedLineageNode {
id: "receipt-root".to_string(),
kind: "receipt".to_string(),
receipt_ref: "receipt-root".to_string(),
artifact_sha256: digest("receipt-root"),
artifact_schema: "chio.receipt.v1".to_string(),
evidence_class: "observed".to_string(),
tenant_hash: digest("tenant-fixture"),
source_table: "receipts".to_string(),
source_id_hash: digest("receipt-root"),
depth: 0,
parent_ids: Vec::new(),
disclosure_state: "disclosed".to_string(),
},
DisclosureSignedLineageNode {
id: "receipt-child".to_string(),
kind: "receipt_lineage_statement".to_string(),
receipt_ref: "receipt-child".to_string(),
artifact_sha256: digest("receipt-child"),
artifact_schema: "chio.receipt-lineage-statement.v1".to_string(),
evidence_class: "derived".to_string(),
tenant_hash: digest("tenant-fixture"),
source_table: "receipt_lineage_statements".to_string(),
source_id_hash: digest("receipt-child"),
depth: 1,
parent_ids: vec!["receipt-root".to_string()],
disclosure_state: "redacted".to_string(),
},
],
edges: vec![DisclosureSignedLineageEdge {
edge_id: "edge-receipt-root-receipt-child".to_string(),
from: "receipt-root".to_string(),
to: "receipt-child".to_string(),
relation: "continued".to_string(),
kind: "continued_by".to_string(),
evidence_class: "observed".to_string(),
source_artifact_sha256: digest("edge-receipt-root-receipt-child"),
statement_sha256: digest("receipt-root|receipt-child|continued_by"),
disclosure_state: "disclosed".to_string(),
}],
redactions: vec![DisclosureSignedLineageRedaction {
node_id: "receipt-child".to_string(),
reason: "privacy_profile".to_string(),
}],
subgraph_sha256: String::new(),
signature: String::new(),
};
lineage.subgraph_sha256 = compute_signed_lineage_subgraph_digest(&lineage)?;
lineage.signature = sign_lineage_subgraph(&lineage, &lineage_signer())?;
let leakage_ledger = DisclosureLeakageLedger {
schema: DISCLOSURE_LEAKAGE_LEDGER_SCHEMA_V1.to_string(),
id: "leakage-ledger-valid".to_string(),
transaction_passport_ref: "passport-disclosure-valid".to_string(),
privacy_profile_ref: "privacy-profile-valid".to_string(),
policy_profile_id: "privacy-profile-valid".to_string(),
subject_artifact_sha256: digest("disclosure-capsule-valid"),
generated_at: "2026-06-10T00:00:00Z".to_string(),
audience: "https://auditor.example/chio".to_string(),
total_leakage_score: 7,
max_allowed_leakage_score: 7,
tenant_leakage_notice_ref: "tenant-leakage-notice-none".to_string(),
accepted: true,
entries: vec![
leakage_entry(
"leakage-capability-id",
"capability_id",
"disclosed_field",
"capability_identifier",
1,
None,
),
leakage_entry(
"leakage-tool-name",
"tool_name",
"disclosed_field",
"tool_identity",
1,
None,
),
leakage_entry(
"leakage-amount-cap",
"amount_lte_100",
"hidden_predicate",
"amount_or_budget",
2,
Some("predicate reveals capped amount band"),
),
leakage_entry(
"leakage-derived-issuer-status",
"derived.crypto.issuer_status",
"derived_fact",
"runtime_assurance",
1,
None,
),
leakage_entry(
"leakage-derived-revocation-freshness",
"derived.crypto.revocation_freshness",
"derived_fact",
"runtime_assurance",
1,
None,
),
leakage_entry(
"leakage-derived-presentation-timing",
"derived.crypto.presentation_timing",
"derived_fact",
"timing",
1,
None,
),
],
};
let mut crypto_context_report = DisclosureCryptoContextReport {
schema: DISCLOSURE_CRYPTO_CONTEXT_REPORT_SCHEMA_V1.to_string(),
id: "crypto-context-report-valid".to_string(),
context_id: "crypto-context-valid".to_string(),
artifact_ref: "disclosure-capsule-valid".to_string(),
projection_manifest_ref: "bbs-projection-manifest-valid".to_string(),
verdict: DisclosureContextVerdict::Verified,
evidence_class: "verifier_context".to_string(),
cryptographic_proof_verified: true,
verified_claims: vec![
"claim.disclosure.crypto_context_bound".to_string(),
"claim.disclosure.profile_context_policy_enforced".to_string(),
],
rejected_checks: Vec::new(),
disclosed_fields: vec!["capability_id".to_string(), "tool_name".to_string()],
signature: None,
};
crypto_context_report.signature = Some(sign_crypto_context_report(
&crypto_context_report,
&lineage_signer(),
)?);
Ok(DisclosureLineageBundle {
capsule,
privacy_profile,
lineage,
leakage_ledger,
crypto_context_report: Some(crypto_context_report),
})
}
fn lineage_signer() -> Keypair {
swarm_authority_examples::witness_keypair()
}
fn disclosure_lineage_trust() -> DisclosureLineageVerifierTrust {
let signer = lineage_signer().public_key();
DisclosureLineageVerifierTrust::new()
.with_trusted_lineage_signer_keys(vec![signer.clone()])
.with_trusted_crypto_context_report_signer_keys(vec![signer])
}
fn verify_bundle(
bundle: &DisclosureLineageBundle,
) -> Result<DisclosureLineageVerifierReport, DisclosureLineageError> {
verify_disclosure_lineage_bundle_with_trust(bundle, &disclosure_lineage_trust())
}
fn digest(value: &str) -> String {
sha256_hex(value.as_bytes())
}
fn lineage_frontier_digest(node_id: &str, artifact_sha256: &str, depth: u32) -> String {
digest(&format!("{node_id}:{artifact_sha256}:{depth}"))
}
fn leakage_entry(
entry_id: &str,
field: &str,
leakage_kind: &str,
sensitivity_class: &str,
score: u32,
residual_inference_note: Option<&str>,
) -> DisclosureLeakageLedgerEntry {
DisclosureLeakageLedgerEntry {
entry_id: entry_id.to_string(),
source: "disclosure-capsule".to_string(),
field: field.to_string(),
leakage_kind: leakage_kind.to_string(),
disclosure_kind: leakage_kind.to_string(),
sensitivity_class: sensitivity_class.to_string(),
value_class: "identifier_or_predicate".to_string(),
reason: "required by disclosure profile".to_string(),
policy_rule: "profile.allowed_disclosure".to_string(),
derived_inferences: Vec::new(),
cross_tenant_risk: false,
mitigation: None,
score,
allowed_by_profile: true,
residual_inference_note: residual_inference_note.map(str::to_string),
}
}
fn amount_cap_hidden_predicate() -> DisclosureHiddenPredicate {
DisclosureHiddenPredicate {
predicate_id: "amount_lte_100".to_string(),
kind: "amount_cap".to_string(),
field: "amount".to_string(),
operator: "<=".to_string(),
operand: "100".to_string(),
unit: "USD".to_string(),
result: true,
proof_ref: "selective-disclosure-proof".to_string(),
projection_slot: 2,
}
}Captured result and refusal
lineage: {"schema":"chio.disclosure.lineage-verifier-report.v1","id":"disclosure-lineage-verifier-report-disclosure-capsule-valid","verdict":"verified","capsule_id":"disclosure-capsule-valid","transaction_passport_ref":"passport-disclosure-valid","lineage_subgraph_ref":"lineage-subgraph-valid","leakage_ledger_ref":"leakage-ledger-valid","crypto_verified":true,"privacy_profile_verified":true,"disclosed_field_count":2,"hidden_predicate_count":1,"verified_claims":["claim.disclosure.lineage_subgraph_bound","claim.disclosure.leakage_ledger_complete","claim.disclosure.crypto_context_bound","claim.disclosure.profile_context_policy_enforced"]}
changed child digest: invalid disclosure lineage artifact: lineage node artifact digest mismatch: receipt-childThis example checks policy and provenance declarations. Its supplied crypto-context report is not a new BBS verification; use the selective-disclosure example for proof generation and supply an independently produced context report in a real integration.
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.
An accepted report names the checks that passed under your inputs
A chio.disclosure.lineage-verifier-report.v1 report establishes the graph, signature, policy, reference, and ledger-coverage checks for the supplied artifacts. It carries the lineage-subgraph and leakage-ledger claim identifiers, then the two permitted crypto-context claims.
The pure library requires the report’s verified verdict, its cryptographic_proof_verified flag, the required claims, field agreement, and a trusted signature. It does not re-evaluate rejected_checks, evidence_class, or context_id; the CLI composition adds the proof and context comparison, and your integration supplies trustworthy context inputs.
An accepted report covers what the producer declared. It confirms each referenced receipt by identifier, finds no undeclared inference, and proves no hidden comparison.
Lineage bundle checks
Search for frontier, residual, max_allowed_leakage_score, or a failure message.
Bundle artifacts
| Artifact | Authentication | Checked by the verifier |
|---|---|---|
Disclosure capsule | No separate signature | Shape, references, disclosure policy |
Privacy profile | Selected by the caller | Shape, count limits, allow and forbid lists |
Signed lineage subgraph | Lineage signer set | Graph structure, digests, signature |
Leakage ledger | No separate signature | Identity, policy, arithmetic, coverage |
Crypto-context report | Report signer set | Verdict, claims, field agreement, signature |
The crypto report is optional during JSON loading and required for acceptance. The bundle and artifact types reject unknown JSON fields.
Reference bindings
| Relationship | Required agreement |
|---|---|
Capsule to subgraph and ledger | The capsule’s references equal the supplied subgraph and ledger identifiers. |
Capsule to privacy profile | The capsule and ledger profile references equal the profile identifier, and so does the ledger’s policy-profile identifier. |
Transaction passport | The capsule, subgraph, ledger, and profile carry the same transaction- |
Ledger audience | The ledger audience equals the profile’s required audience. |
Capsule and crypto report | The report identifier equals the capsule’s report reference, and the report’s artifact reference equals the capsule identifier. |
Projection and reveal set | The report and capsule name the same projection manifest and disclose the same field set. |
These checks compare supplied objects and resolve nothing. The subgraph’s own audience and policy-profile identifier are signed with the subgraph, and the profile comparison runs against the ledger. The subgraph’s nonce and generation time are carried and signed; this library applies no freshness check to them.
Graph checks
| Part | Check |
|---|---|
Root | A named node exists, has a matching receipt reference, depth 0, and no parents. The verifier validates the root record and loads no receipt body. |
Declared parent | Every non-root node has a parent that exists, has smaller depth, and has an edge to the child. An extra edge may be absent from parent_ids. |
Evidence class | Node rank meets the subgraph’s floor: asserted 0, observed 1, verified or derived 2. Derived meets a verified floor. Edge classes are recognized and left out of the floor comparison. |
Redacted node | A matching redaction entry gives privacy_profile, data_, or legal_hold. The reference, source table, tenant hash, and other node fields remain. Edge redaction carries no reason check. |
Graph frontier | Nodes with no outgoing edge, their id:digest:depth strings sorted, joined with |, and hashed. Redacted leaves count; an extra edge can change membership. |
Node kinds include receipts, capability snapshots, session anchors, request-lineage records, receipt-lineage statements, continuations, checkpoints, BBS projections and proofs, passport presentations, governed intents, approvals, and runtime-assurance records.
Edge relations are continued, derived, or delegated. Edge kinds are parent_of, issued_under, observed_in_session, continued_by, signed_lineage_statement, included_in_checkpoint, proves_projection, governs_transaction, approves_intent, and presented_by. Node identifiers, edge identifiers, and edge (from, to, relation) triples are unique.
Digest meanings
| Digest | What this verifier recomputes |
|---|---|
Node artifact and source-id hashes | SHA-256 of receipt_ref as a string, which establishes identifier consistency. |
Edge source-artifact hash | SHA-256 of the edge identifier string. |
Edge statement hash | SHA-256 of from|to|kind. |
frontier_sha256 | The sorted leaf representation in Graph checks. |
checkpoint_ | SHA-256 of checkpoint_ref|frontier_sha256, a reference digest rather than a Merkle inclusion proof. |
subgraph_sha256 | Canonical subgraph material excluding schema, this digest, and signature. The signature authenticates the digest string. |
Ledger, projection- | Their SHA-256 hexadecimal shape only; this library does not recompute them from the separate artifacts. |
A trusted subgraph signature protects the declared values from modification. A shape-checked digest stays a declaration until something compares it with its artifact; the ledger’s subject-artifact digest is likewise shape-checked.
Leakage entry kinds
| Entry kind | Coverage key | Classification rule |
|---|---|---|
disclosed_field | The disclosed field name | Field belongs to the named sensitivity class |
hidden_ | The declared predicate identifier | Identifier belongs to the named class |
derived_fact | A declared inference identifier | The sensitivity class exists |
The producer sets accepted and allowed_by_profile, and the verifier requires both to be true. It rechecks the field and predicate allow and forbid lists itself; derived facts have no allowlist. With no disclosed field and no declared predicate, the coverage rule permits an empty ledger.
Count limits and scores
The profile supplies max_disclosed_fields and max_hidden_predicates. The ledger supplies total_leakage_score and max_allowed_leakage_score.
| Limit | Source | Enforcement |
|---|---|---|
Disclosed-field and predicate counts | The supplied privacy profile | Refuse above either count, then check allow and forbid lists |
Total leakage score | The ledger’s entries and its own maximum | Recompute the saturating u32 sum, match the declared total, compare with the maximum |
The score ceiling is the ledger’s own declared cap; the profile sets counts, and a verifier risk limit is a separate choice you make. Scores are declared per entry and are independent of sensitivity classes and cross-tenant risk. The residual-inference note explains a declared risk; it is a statement, and the verifier computes no information measure from it.
Fixture ledger
| Entry | Kind | Score |
|---|---|---|
capability_id | disclosed_field | 1 |
tool_name | disclosed_field | 1 |
amount_lte_100 | hidden_ | 2 |
derived. | derived_fact | 1 |
derived. | derived_fact | 1 |
derived. | derived_fact | 1 |
The amount declaration carries the note “predicate reveals capped amount band.” The note records the fixture’s claim about the band.
Predicate declarations
The lineage catalog accepts one declaration: amount_lte_100, kind amount_cap, field amount, operator <=, operand 100, unit USD, proof reference selective-disclosure-proof, projection slot 2, and result true. A changed identifier or required property is refused, and a new declaration is a code change to the catalog.
The check is a declaration check. The CLI also confirms that the manifest declares the predicate, matches its field and operator, and places it in the named hidden slot. Neither check establishes that an unopened amount satisfies the inequality.
The fixture builds a custom three-message vector (capability_id, tool_name, amount) and labels it with the receipt-v1 projection identifier. The canonical receipt projection has fourteen slots; encode receipts from the published receipt slot table.
Refusal messages
| Failure text | What to inspect |
|---|---|
lineage subgraph signer untrusted | The lineage signer set and the signer in the signature carrier. |
disclosed field absent from leakage ledger | A matching allowed disclosed_field entry for the named field. |
unknown lineage root receipt | The root identifiers and the supplied graph nodes. |
disclosed field forbidden by privacy profile | The reveal set and the approved profile’s forbid list. |
lineage node depth not greater than parent | The parent relation and depth ordering. |
lineage node artifact digest mismatch | The node’s declared digest against its receipt reference. |
disclosure leakage budget exceeded | The profile’s disclosed-field and predicate count limits. |
sensitive leakage entry requires residual inference note | The note on an entry that declares a predicate, scores above 1, or declares cross-tenant risk. |
crypto context report was not verified | The report verdict and cryptographic_ flag, then the underlying context checks. |
Source
The lineage verifier, the CLI composition, and the positive fixture that chio proof fixture generate disclosure-lineage-ledger writes define this behavior. The generator writes demonstration evidence and a trust file marked profile = "fixture"; pin your own approved keys and profile for any other bundle.