diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | 41657a4d749b1f1d44663ff6504f2e784f091537 (patch) | |
| tree | 3f4f866f60988696bf047fa718351457a3309b75 /src/document | |
| parent | a3e936154e07ba3b95157d26ffeae9364f3d5cef (diff) | |
access: strict policy, signed paths
"values": "signed" (strict, requiring signatures too): the registry,
_continuum/access, bans and the paths in _continuum/access/userSigned
(* for any key) hold values their users signed. Envelopes read as the
values they hold where the node reads protocol data.
Auth plan M7.
Diffstat (limited to 'src/document')
| -rw-r--r-- | src/document/signatures.rs | 2 | ||||
| -rw-r--r-- | src/document/store.rs | 53 |
2 files changed, 54 insertions, 1 deletions
diff --git a/src/document/signatures.rs b/src/document/signatures.rs index 63b19a5..5d8731b 100644 --- a/src/document/signatures.rs +++ b/src/document/signatures.rs @@ -67,7 +67,7 @@ fn related(signed: &[String], path: &[String]) -> bool { /// strings is left out. pub(crate) fn listed(doc: &Document) -> Vec<Vec<String>> { let at: Vec<String> = SIGNED_PATHS.iter().map(|s| (*s).to_owned()).collect(); - let Ok(Some(serde_json::Value::Array(entries))) = super::store::read_path(doc, &at) else { + let Ok(Some(serde_json::Value::Array(entries))) = super::store::read_plain(doc, &at) else { return Vec::new(); }; entries diff --git a/src/document/store.rs b/src/document/store.rs index 778307f..c946fef 100644 --- a/src/document/store.rs +++ b/src/document/store.rs @@ -454,6 +454,30 @@ fn read_path_inner( Ok(Some(resolved_to_json(doc, ¤t, form, log)?)) } +/// [`read_path`] of a value its user may have signed (`access::envelope`): an envelope +/// read as the value it holds, unchecked (the policy checked it before it went in), a +/// signed `null` as nothing. +pub(crate) fn read_plain( + doc: &impl ReadDoc, + path: &[String], +) -> Result<Option<JsonValue>, StoreError> { + Ok(match read_path(doc, path)? { + Some(JsonValue::String(text)) if text.starts_with("cv1.") => { + enveloped(&text).filter(|value| !value.is_null()) + } + other => other, + }) +} + +/// The value an envelope (`cv1.<payload>.<signature>`) holds; None if it isn't one. +fn enveloped(text: &str) -> Option<JsonValue> { + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; + let payload = text.split('.').nth(1)?; + let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?; + let mut payload: JsonValue = serde_json::from_slice(&bytes).ok()?; + Some(payload.get_mut("value")?.take()) +} + /// Reads a path; the empty path is the whole store. pub(crate) fn read_path( doc: &impl ReadDoc, @@ -1673,6 +1697,35 @@ mod tests { at.split('/').map(str::to_owned).collect() } + /// An envelope reads as the value it holds, a signed `null` as nothing, anything + /// else as it is. + #[test] + fn plain_reads_open_envelopes() { + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; + let sealed = |value: JsonValue| { + let payload = + json!({"path": ["a"], "value": value, "user": "alice", "at": 1, "prev": ""}); + format!("cv1.{}.c2ln", URL_SAFE_NO_PAD.encode(payload.to_string())) + }; + let mut doc = crate::document::Document::new(); + try_change(&mut doc, |doc| { + write_path(doc, &path("a"), &json!(sealed(json!({"mode": "fast"}))))?; + write_path(doc, &path("gone"), &json!(sealed(JsonValue::Null)))?; + write_path(doc, &path("plain"), &json!("cv1-ish")) + }) + .unwrap(); + assert_eq!( + read_plain(&doc, &path("a")).unwrap(), + Some(json!({"mode": "fast"})) + ); + assert_eq!(read_plain(&doc, &path("gone")).unwrap(), None); + assert_eq!( + read_plain(&doc, &path("plain")).unwrap(), + Some(json!("cv1-ish")) + ); + assert_eq!(read_plain(&doc, &path("none")).unwrap(), None); + } + #[test] fn changes_name_the_paths_their_operations_touch() { fn touched( |
