aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/strict.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 18:17:14 +0000
committertv <tv@krebsco.de>2026-11-08 19:29:10 +0100
commit96ba15cf1a6f8725f70f429695a5a9661f706eb0 (patch)
tree36feecb42a8bfa5317f27a052266f74489312dfc /src/access/strict.rs
parent4ce3bde36bb8283d82b914f0f73e30d89c935113 (diff)
strict: a manifest serves one remake, at its base
Review 2, N1: a strict manifest wasn't bound to its base. Any member could propose an old, legitimately signed one again at the current heads; nodes would verify it and vote, and every value at a signed place changed or added since (a ban, a new user, a rotated key, a changed userSigned or sealed declaration) would be deleted, nothing after the base being left to replay. The manifest now names the epoch and the heads it was made at, and a node takes it (proposing or voting) only for a remake of that epoch with exactly that base. GET /admin/strict-manifest makes it so; continuum-signed strict proposes at those heads already.
Diffstat (limited to 'src/access/strict.rs')
-rw-r--r--src/access/strict.rs53
1 files changed, 50 insertions, 3 deletions
diff --git a/src/access/strict.rs b/src/access/strict.rs
index 0fa6577..0b294f8 100644
--- a/src/access/strict.rs
+++ b/src/access/strict.rs
@@ -27,6 +27,10 @@ pub(crate) const NAMESPACE: &str = "continuum-strict";
pub(crate) struct Manifest {
/// The swarm it is for.
pub(crate) swarm: String,
+ /// The epoch it is for, and the heads it was made at: the base a remake taking it
+ /// must have, so it serves once, for the values it names as they were then.
+ pub(crate) epoch: crate::document::replica::EpochRecord,
+ pub(crate) base: Vec<String>,
/// The policy the next epoch has (`"values": "signed"`).
pub(crate) policy: JsonValue,
/// The values kept at the places values are signed (`envelope::signed_paths`), each
@@ -46,6 +50,29 @@ pub(crate) struct Kept {
}
impl Manifest {
+ /// Whether it is for a remake of `epoch` at `base` (hex heads): why not, if not.
+ pub(crate) fn for_base(
+ &self,
+ epoch: &crate::document::replica::EpochRecord,
+ base: Option<&[String]>,
+ ) -> Result<(), String> {
+ if &self.epoch != epoch {
+ return Err(format!(
+ "made for epoch {}:{}, not this one",
+ self.epoch.generation, self.epoch.id
+ ));
+ }
+ let sorted = |heads: &[String]| {
+ let mut heads = heads.to_vec();
+ heads.sort();
+ heads
+ };
+ match base {
+ Some(base) if sorted(base) == sorted(&self.base) => Ok(()),
+ _ => Err("made at other heads than the remake's base".to_owned()),
+ }
+ }
+
/// Whether it keeps `value` at `path`.
pub(crate) fn keeps(&self, path: &[String], value: &JsonValue) -> bool {
let digest = digest(value);
@@ -104,17 +131,24 @@ pub(crate) fn held(doc: &impl ReadDoc) -> Vec<(Vec<String>, JsonValue)> {
out
}
-/// A manifest for `doc` of `swarm`: keeping every value held at a signed place now, the
-/// policy `policy`, for `user` to look over and sign at `at`. Its exact line.
+/// A manifest for `doc` (at heads `base`, of `epoch` of `swarm`): keeping every value
+/// held at a signed place now, the policy `policy`, for `user` to look over and sign at
+/// `at`. Its exact line.
pub(crate) fn manifest_of(
doc: &impl ReadDoc,
swarm: &str,
+ epoch: crate::document::replica::EpochRecord,
+ base: &[crate::document::ChangeHash],
policy: JsonValue,
user: &str,
at: u64,
) -> String {
+ let mut base: Vec<String> = base.iter().map(ToString::to_string).collect();
+ base.sort();
let manifest = Manifest {
swarm: swarm.to_owned(),
+ epoch,
+ base,
policy,
keep: held(doc)
.into_iter()
@@ -202,7 +236,10 @@ mod tests {
store::write_path(doc, &path("notes"), &json!(1)).unwrap();
});
let policy = json!({"version": 1, "values": "signed"});
- let line = manifest_of(&doc, "s", policy, "alice", 1);
+ let epoch =
+ crate::document::replica::EpochRecord::from(crate::document::replica::Epoch::GENESIS);
+ let heads = doc.get_heads();
+ let line = manifest_of(&doc, "s", epoch.clone(), &heads, policy, "alice", 1);
let key = SigningKey::from_bytes(&[3; 32]);
let public = sshsig::PublicKey(key.verifying_key().to_bytes());
let text = format!("{line}\n{}", sshsig::sign(&key, NAMESPACE, line.as_bytes()));
@@ -229,5 +266,15 @@ mod tests {
assert!(verify(&elsewhere, std::slice::from_ref(&public), "s").is_err());
let tampered = text.replacen("alice", "mallory", 1);
assert!(verify(&tampered, std::slice::from_ref(&public), "s").is_err());
+ // For its epoch and base only.
+ let base: Vec<String> = heads.iter().map(ToString::to_string).collect();
+ assert!(manifest.for_base(&epoch, Some(&base)).is_ok());
+ assert!(manifest.for_base(&epoch, None).is_err());
+ assert!(manifest.for_base(&epoch, Some(&[])).is_err());
+ let later = crate::document::replica::EpochRecord {
+ generation: 1,
+ id: "ab".to_owned(),
+ };
+ assert!(manifest.for_base(&later, Some(&base)).is_err());
}
}