diff --git a/jcp/SPEC.md b/jcp/SPEC.md index 5f27d1314..b1f6ad44c 100644 --- a/jcp/SPEC.md +++ b/jcp/SPEC.md @@ -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. diff --git a/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs b/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs index 1aa76b418..56f35ea98 100644 --- a/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-conformance/src/lib.rs @@ -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 { 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| -> Result { + 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));