aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/screen.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 21:46:41 +0000
committertv <tv@krebsco.de>2026-11-08 22:58:41 +0100
commit1fa5360a210b4b820441fdbc6f5dfbbb9eaf14f3 (patch)
treedc99ee243b33b2a9f729a38175e65b5e25253aca /src/access/screen.rs
parent51da8017d4592b31e904f505cab18ae1854df08b (diff)
screen: signatures required as the policy a change came under says
Review 3, R1: what a node received was vouched for only while its own document's policy required signatures. A joiner's first sync, and every node's message bringing `required`, went in by the rules alone: a malicious first peer could plant changes under other members' actors at their next seqs, their real ones then failing as duplicates for good. The signatures a message carries are always offered now, and the screen always has them: a change waits for one where the policy at the heads it is judged at requires it (rules::Reads::required, read once per heads), whatever the receiving document holds. Changes written under a policy not requiring signatures wait for none, as on every other node; the check for forged changes runs always. Changes waiting for a signature are judged again whenever signatures come. The test of signatures required now takes the history into an empty document; a new one takes changes written under a policy not requiring them without any.
Diffstat (limited to 'src/access/screen.rs')
-rw-r--r--src/access/screen.rs57
1 files changed, 49 insertions, 8 deletions
diff --git a/src/access/screen.rs b/src/access/screen.rs
index 3a516e3..beae149 100644
--- a/src/access/screen.rs
+++ b/src/access/screen.rs
@@ -271,7 +271,9 @@ pub(crate) struct Screened {
/// those rules allow, what they change recorded in `log`. Those already in `doc` are
/// left out; those whose dependencies are missing wait; those `refusals` holds are
/// refused again by their hash, and those building on them, and those refused now go
-/// into it. With `signatures` (required), those no signature vouches for wait too.
+/// into it. With `signatures` (the node's always; tests judge without), those written
+/// under a policy requiring signatures that no signature vouches for wait too, and
+/// those forged under another node's actor are refused.
pub(crate) fn apply(
doc: &mut Document,
epoch: Epoch,
@@ -437,10 +439,8 @@ pub(crate) fn apply(
});
continue;
}
- // No signature vouches for it yet, or for what it builds on: it waits.
- if signatures.is_some() && !change.deps().is_empty() && !vouched.contains(&hash)
- || change.deps().iter().any(|dep| unsigned.contains(dep))
- {
+ // What it builds on waits for a signature: it waits too.
+ if change.deps().iter().any(|dep| unsigned.contains(dep)) {
unsigned.insert(hash);
waiting.0.push(change);
continue;
@@ -479,6 +479,19 @@ pub(crate) fn apply(
}
};
let root = change.deps().is_empty();
+ // Written under a policy requiring signatures, and none vouches for it yet: it
+ // waits. As that policy has it, not this document's, which may have none yet
+ // (a joiner's) or another: so every node waits for the same.
+ if judging
+ && !root
+ && signatures.is_some()
+ && !vouched.contains(&hash)
+ && reads.required(doc, &seen_at)
+ {
+ unsigned.insert(hash);
+ waiting.0.push(change);
+ continue;
+ }
let judged = if judging || root {
rules::judge_reading(
doc,
@@ -655,8 +668,8 @@ pub(crate) struct Outcome {
/// what `screening` keeps for document `epoch_seq` (of `epoch`). Bytes that don't load
/// as changes, or with a misplaced author (`changes::misplaced_author`), are refused
/// whole.
-/// With `signatures` (required), those no signature vouches for wait too; and with no
-/// bytes, those waiting are judged again (signatures came since).
+/// With `signatures`, as [`apply`] has them; and with no bytes, those waiting are
+/// judged again (signatures came since).
pub(crate) fn apply_bytes(
doc: &mut Document,
epoch: Epoch,
@@ -1177,6 +1190,11 @@ mod tests {
},
|doc| {
store::write_path(doc, &path("_continuum/swarm"), &json!("s"))?;
+ store::write_path(
+ doc,
+ &path("_continuum/access/policy"),
+ &json!({"version": 1, "signatures": "required"}),
+ )?;
store::write_path(doc, &path("_continuum/users/alice"), &alice)
},
)
@@ -1216,7 +1234,9 @@ mod tests {
)
.unwrap()
};
- // Unsigned: only the root goes in (no node signs it).
+ // Into an empty document, a joiner's, which has no policy yet: as the policy the
+ // changes were written under has it. Unsigned: only the root goes in (no node
+ // signs it).
let screened = required(
&mut ours,
&mut waiting,
@@ -1267,4 +1287,25 @@ mod tests {
.is_none()
);
}
+
+ /// Written under a policy not requiring signatures: none waits for one, whatever the
+ /// receiving node holds.
+ #[test]
+ fn unsigned_changes_taken_where_not_required() {
+ let mut theirs = genesis();
+ write(&mut theirs, "a", Some("alice"), "notes/x", json!(1));
+ let mut ours = Document::new();
+ let screened = apply(
+ &mut ours,
+ Epoch::GENESIS,
+ received(&theirs, &[]),
+ &mut Waiting::default(),
+ &mut Refusals::default(),
+ Some(&mut Signatures::default()),
+ &mut crate::document::PatchLog::inactive(),
+ )
+ .unwrap();
+ assert!(screened.refused.is_empty());
+ assert_eq!(ours.get_heads(), theirs.get_heads());
+ }
}