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 | 8523e9c67e375747147a0b05ec191ad81ce2e318 (patch) | |
| tree | 5263ee759296f08a38091890b743f8d64b557c30 /src/api | |
| parent | 8f3f7b24f5a4e7cce4a006ec45b8061fe5cdd87c (diff) | |
signatures: changes signed by their node
Every node signs the changes it commits with its node key, an Ed25519
signature over the change's hash, kept beside the document. One
signature on an actor's latest change vouches for its earlier ones.
Protocol data and paths listed in _continuum/access/signed are signed,
and writes with ?signed=true.
Auth plan M5.
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/login.rs | 3 | ||||
| -rw-r--r-- | src/api/store.rs | 32 | ||||
| -rw-r--r-- | src/api/users.rs | 3 |
3 files changed, 31 insertions, 7 deletions
diff --git a/src/api/login.rs b/src/api/login.rs index 023408e..b84a412 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -14,6 +14,7 @@ //! Every request may bring a token instead of a password: `Authorization: Bearer ct1…` //! (`bearer`). +use crate::document::signatures::Sign; use std::sync::Arc; use axum::{ @@ -261,7 +262,7 @@ pub(crate) async fn revoke_tokens(state: &AppState, name: &str, by: &str) -> Res // Issued in this very millisecond: revoked too. let after = state.clock.now_millis().as_u64() + 1; state - .mutate_doc_for(Some(by.to_owned()), move |doc| { + .mutate_doc_for(Some(by.to_owned()), Sign::Protocol, move |doc| { if !registry::exists(&*doc) { return Err(ApiError( StatusCode::CONFLICT, diff --git a/src/api/store.rs b/src/api/store.rs index 48fc2c6..7307d92 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -1,5 +1,6 @@ //! /store and /stream handlers. +use crate::document::signatures::Sign; use std::io::Error as IoError; use axum::{ @@ -90,12 +91,18 @@ pub(crate) struct PostQuery { /// With `cast`, how timestamps are read from and written as strings (`rfc3339` by /// default; `typed::TimestampFormat`). format: Option<String>, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } #[derive(Debug, Deserialize)] pub(crate) struct DeleteQuery { #[serde(default)] prune: bool, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } /// `GET /store`: the whole store (`docs/openapi.yaml`). @@ -349,6 +356,9 @@ pub(crate) struct PutQuery { /// default); if not, one that would is `409` (`store::Writing::retype`). #[serde(default = "yes")] retype: bool, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } fn yes() -> bool { @@ -377,6 +387,10 @@ pub(crate) async fn put_store_entry( } let path = parse_write_path(&state, &user, &access, &key, "PUT")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; if let Some(redirect) = to_positive(&state, &path, false, None, raw_query.as_deref()).await? { return Ok(redirect); } @@ -387,7 +401,7 @@ pub(crate) async fn put_store_entry( // A value that can't be shown as asked is refused, and the write with it. let writer = user.0.clone(); let ((retyped, read), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let retyped = store::write_val_path(doc, &path, &body.value, writing) .map_err(|e| store_status(e, "Failed to write store value"))?; @@ -706,6 +720,10 @@ pub(crate) async fn post_store_entry( return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } let path = parse_write_path(&state, &user, &access, &key, "POST")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; if query.increment.is_some() && query.cast.is_some() { return Err(ApiError( StatusCode::BAD_REQUEST, @@ -723,7 +741,7 @@ pub(crate) async fn post_store_entry( if let Some(by) = query.increment { let writer = user.0.clone(); let (value, heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let value = store::increment_path(doc, &path, by) .map_err(|e| store_status(e, "Failed to increment a counter"))?; @@ -759,7 +777,7 @@ pub(crate) async fn post_store_entry( // A value that can't be shown as asked is refused, and the cast with it. let writer = user.0.clone(); let ((retyped, read), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let retyped = store::cast_path(doc, &path, to, timestamps) .map_err(|e| store_status(e, "Failed to cast a store value"))?; @@ -775,7 +793,7 @@ pub(crate) async fn post_store_entry( let value = body_value(&headers, &body)?; let writer = user.0.clone(); let ((), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); store::insert_val_path(doc, &path, &value.value) .map_err(|e| store_status(e, "Failed to insert into a list"))?; @@ -803,6 +821,10 @@ pub(crate) async fn delete_store_entry( RawQuery(raw_query): RawQuery, ) -> Result<Response, ApiError> { let path = parse_write_path(&state, &user, &access, &key, "DELETE")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; tracing::Span::current().record("path", path.join("/").as_str()); @@ -826,7 +848,7 @@ pub(crate) async fn delete_store_entry( }); let writer = user.0.clone(); let ((), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); store::delete_path_pruning(doc, &path, prune) .map_err(|e| store_status(e, "Failed to delete store value"))?; diff --git a/src/api/users.rs b/src/api/users.rs index 1d01af7..3b8174d 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -23,6 +23,7 @@ //! the document, logged, named by who made it, and refused (`409`) where it would lock //! its maker out (`access::registry_kept`). +use crate::document::signatures::Sign; use axum::{ Extension, Json, extract::{Path, State}, @@ -166,7 +167,7 @@ async fn change( let writer = user.to_owned(); let since = state.clock.now_millis().as_u64(); state - .mutate_doc_for(Some(user.to_owned()), move |doc| { + .mutate_doc_for(Some(user.to_owned()), Sign::Protocol, move |doc| { let before = Before::of(&*doc, &writer); f(doc, &by, since)?; registry_kept(&*doc, ®istry::users_dir(), before, &writer) |
