jcp: C25 — flow was a declared clause with no check; now it has one #357
2 changed files with 106 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue