diff options
| author | tv <tv@krebsco.de> | 2026-11-08 15:43:45 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 16:49:56 +0100 |
| commit | ed27acb707d940c5b796b1079d3d74698484aeba (patch) | |
| tree | 2761073c72f91ad8f3f0ed4b38844bb8ab8d14a2 /src/access/envelope.rs | |
| parent | 1cdf5ce6e2a935ec17f800706aa8f289036d90f7 (diff) | |
users: a user manages its own keys
Only a user who may manage users could change a user's keys: a user
couldn't add a new laptop's key, or remove a lost one.
A user now manages its own keys, whatever else it may do:
* GET /auth/keys: its keys, its entry, and the envelope holding it
under a strict policy (`signed`), to sign against.
* PUT /auth/keys/{id} with an authorized_keys line, DELETE
/auth/keys/{id}: one change, as the user. The rules take a write of
_continuum/users/<name>/keys/… from the user it names.
* Under a strict policy, the user signs its entry with only its keys
changed, with a key it holds already (envelope::check takes that from
the user itself), PUT to its own entry even without write on the
registry; continuum-signed key-add and key-remove do so.
Diffstat (limited to 'src/access/envelope.rs')
| -rw-r--r-- | src/access/envelope.rs | 52 |
1 files changed, 51 insertions, 1 deletions
diff --git a/src/access/envelope.rs b/src/access/envelope.rs index e0c8909..7cd0083 100644 --- a/src/access/envelope.rs +++ b/src/access/envelope.rs @@ -207,7 +207,7 @@ pub(crate) fn check( if !user.keys.values().any(|known| *known == key) { return Err(format!("{} has no key {}", payload.user, key.fingerprint())); } - if !user.caps().allows(Right::Write, path) { + if !user.caps().allows(Right::Write, path) && !own_keys_only(doc, &payload, &user, path) { return Err(format!( "{} may not write /{}", payload.user, @@ -217,6 +217,33 @@ pub(crate) fn check( Ok(Signed { payload, key }) } +/// Whether `payload`, signed by its user (`user`, enabled), is that user's own entry +/// with only its keys changed from the one at `path` in `doc`: a user manages its own +/// keys, whatever else it may do. +fn own_keys_only( + doc: &impl ReadDoc, + payload: &Payload, + user: ®istry::User, + path: &[String], +) -> bool { + if user.disabled || path != registry::user_path(&payload.user) { + return false; + } + let Ok(Some(mut before)) = store::read_plain(doc, path) else { + return false; + }; + let mut after = payload.value.clone(); + for entry in [&mut before, &mut after] { + match entry.as_object_mut() { + Some(entry) => { + entry.remove("keys"); + } + None => return false, + } + } + before == after +} + #[cfg(test)] mod tests { use super::*; @@ -307,6 +334,29 @@ mod tests { assert!(check(&doc, &elsewhere, &path("config"), None).is_err()); } + /// A user signs its own entry with its keys changed, though it may not write the + /// registry; not with anything else changed, nor another's. + #[test] + fn users_sign_their_own_keys() { + let key = SigningKey::from_bytes(&[5; 32]); + let doc = registry_with(&key); + let entry = store::read_plain(&doc, ®istry::user_path("alice")) + .unwrap() + .unwrap(); + let line = + sshsig::PublicKey(SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes()).line(); + let mut rekeyed = entry.clone(); + rekeyed["keys"]["phone"] = json!(line); + let at = "_continuum/users/alice"; + let signed_as = |user: &str, value: JsonValue| signed(&key, &payload(user, at, value, "")); + assert!(check(&doc, &signed_as("alice", rekeyed.clone()), &path(at), None).is_ok()); + let mut more = rekeyed.clone(); + more["caps"] = json!({"x": {"path": [], "rights": ["write"]}}); + assert!(check(&doc, &signed_as("alice", more), &path(at), None).is_err()); + // bob, holding the same key, for alice: not his entry. + assert!(check(&doc, &signed_as("bob", rekeyed), &path(at), None).is_err()); + } + #[test] fn listed_paths() { let mut doc = Document::new(); |
