jcp: C25 — flow was a declared clause with no check; now it has one #357

Merged
dcharlot merged 1 commit from feat/c27-flow-conformance into main 2026-10-20 13:06:03 -04:00
2 changed files with 106 additions and 0 deletions

View file

@ -1285,6 +1285,7 @@ A conforming implementation passes, with real checks:
forged, stale, and regressing statements are refused; post-settlement the
merged ledger binds and any overdraft is provable.
- **C18 — DKG:** a dealer-free k-of-n actor signs a standard Ed25519 signature;
- **C19 — injection resistance:** a tool-poisoning corpus presented through an untrusted frame cannot steer a decision; the trust tag is structural, so data cannot become control. (Implemented in the harness since before this entry: §9 listed every other clause but omitted this one.)
one share cannot.
- **C-neg:** expired, forged, and wrong-caller frames are denied with the precise
reason; canonical CBOR rejects floats/bools.

View file

@ -29,6 +29,13 @@
//! writer; a second holder is refused, the claim is released with the holder
//! so a restart can reopen it, and a platform that cannot lock fails the
//! open rather than proceeding.
//! - **C19 — injection resistance:** a tool-poisoning corpus cannot steer a
//! decision through an untrusted frame.
//! - **C25 — flow:** a propagated label must sit at or below the grant's
//! ceiling and clear the enforcement point's taint floor; facts that were
//! never computed fail closed against a ceilinged grant; several ceilings
//! compose to the strictest; a flow denial debits nothing; and a
//! ceiling-free grant behaves and encodes as it did before flow existed.
//! - **C-neg:** expired grants, forged grants, and wrong callers are denied with
//! the precise reason; canonical CBOR rejects floats/bools.
@ -73,6 +80,7 @@ pub fn checks() -> Vec<Check> {
Check { id: "C23 receipt binds its payload + jcp/1 still verifies", run: c23_payload_binding },
Check { id: "C24 caveats: one-shot, per-call cost, model, monotone attenuation", run: c24_caveats },
Check { id: "C26 one writer per record: a second holder is refused", run: c26_single_writer },
Check { id: "C25 flow: label under ceiling, floor, fail-closed, no debit", run: c25_flow },
Check { id: "C-neg expired / forged / wrong-caller", run: cneg_denials },
Check { id: "C-neg canonical rejects floats", run: cneg_canonical },
]
@ -936,6 +944,103 @@ fn c26_single_writer() -> Result<(), String> {
Ok(())
}
/// C25 — §4.10's flow ceiling and taint floor. §9 has listed this clause by
/// this id since before any check existed for it; this is the check.
///
/// `jcp-flow` covers provenance *scanning*; this covers the decision: what the
/// runtime does with a label once it has one.
fn c25_flow() -> Result<(), String> {
use jcp_schema::flow::{FlowCeiling, FlowFacts, Integrity, Label, Origin, Origins, Readers};
let r = ContentId::of_bytes(b"db");
let holder = did(&sk(2));
let ceilinged = |caveats: Vec<Caveat>| -> Result<SignedGrant, String> {
let mut g = grant(&sk(1), holder, r, &["read"], 1_000_000, 0).grant;
g.caveats = caveats;
issue(&sk(1), g).map_err(|e| e.to_string())
};
// A ceiling admitting only principal-clean, origin-free values.
let principal_only =
|| FlowCeiling::new(Label { integrity: Integrity::Principal, origins: Origins::none(), readers: Readers::Public });
let permissive = || FlowCeiling { ceiling: Label::top(), tainted_budget_uj: None };
let tainted = |channel: u8| FlowFacts {
label: Label {
integrity: Integrity::Untrusted,
origins: Origins::one(Origin::channel(ContentId::of_bytes(&[channel; 4]))),
readers: Readers::Public,
},
witness_id: ContentId([0u8; 32]),
eval_uj: 5,
eval_prov: Provenance::ModelBased,
};
// (a) A label at or below the ceiling passes; one above it does not.
let sg = ceilinged(vec![Caveat::FlowBelow(principal_only())])?;
let f = frame(&sg, r, "read", Trust::Control);
let mut rt = Runtime::new();
if !rt.decide_flow(&f, &sg, holder, Energy::measured(10), 10, &FlowFacts::bottom()).is_allow() {
return Err("a clean label was denied under a ceiling that admits it".into());
}
let mut rt = Runtime::new();
let denied = rt.decide_flow(&f, &sg, holder, Energy::measured(10), 10, &tainted(3));
if denied.is_allow() {
return Err("a label above the grant's ceiling was allowed".into());
}
// (e) A flow denial debits nothing: a tainted call costs zero joules.
if rt.spent(&sg.grant.id()) != 0 {
return Err(format!("a denied tainted call debited {} uJ", rt.spent(&sg.grant.id())));
}
// (b) The enforcement point's own taint floor binds even under a ceiling
// permissive enough to admit the value.
let sg_open = ceilinged(vec![Caveat::FlowBelow(permissive())])?;
let f_open = frame(&sg_open, r, "read", Trust::Control);
let mut rt = Runtime::new();
if !rt.decide_flow(&f_open, &sg_open, holder, Energy::measured(10), 10, &tainted(4)).is_allow() {
return Err("a permissive ceiling denied a label it admits, before any floor was set".into());
}
let mut rt = Runtime::new();
rt.forbid_flow(r, Origins::one(Origin::channel(ContentId::of_bytes(&[4u8; 4]))));
if rt.decide_flow(&f_open, &sg_open, holder, Energy::measured(10), 10, &tainted(4)).is_allow() {
return Err("the resource's taint floor was not applied under a permissive ceiling".into());
}
// (c) Facts that were never computed fail closed against a ceilinged grant,
// rather than passing as clean. This is the unknown-is-not-a-value rule:
// "no provenance" must not read as "no taint".
let mut rt = Runtime::new();
if rt.decide(&f, &sg, holder, Energy::measured(10), 10).is_allow() {
return Err("a ceilinged grant allowed a call whose provenance was never computed".into());
}
// (d) Several ceilings compose to the strictest, so adding a permissive one
// cannot widen a strict one.
let sg_both = ceilinged(vec![Caveat::FlowBelow(principal_only()), Caveat::FlowBelow(permissive())])?;
let f_both = frame(&sg_both, r, "read", Trust::Control);
let mut rt = Runtime::new();
if rt.decide_flow(&f_both, &sg_both, holder, Energy::measured(10), 10, &tainted(5)).is_allow() {
return Err("a permissive ceiling widened a strict one instead of composing to the strictest".into());
}
// (f) A ceiling-free grant is what every grant minted before flow existed
// is: it must behave identically whatever the facts say, and carry no
// flow field on the wire.
let plain = ceilinged(Vec::new())?;
let f_plain = frame(&plain, r, "read", Trust::Control);
for facts in [FlowFacts::bottom(), tainted(6)] {
let mut rt = Runtime::new();
if !rt.decide_flow(&f_plain, &plain, holder, Energy::measured(10), 10, &facts).is_allow() {
return Err("a ceiling-free grant was gated by flow facts".into());
}
}
let bytes = canonical_cbor(&plain.grant).map_err(|e| e.to_string())?;
if String::from_utf8_lossy(&bytes).contains("flow") {
return Err("a ceiling-free grant carries a flow field on the wire".into());
}
Ok(())
}
fn c14_replay() -> Result<(), String> {
let issuer = sk(1);
let (worker_a, worker_b) = (sk(3), sk(4));