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 | b6f815882a6d4a32c1198bd3666edaeaf1f7bca3 (patch) | |
| tree | 5beab4b31fd4511446c8e3a7445412ce7f33f97a /src/access | |
| parent | e696d2f40636a6613eddfb1a3afdf87aa7ffdedd (diff) | |
access: --bootstrap-keys
The first users' SSH keys, recorded at genesis with their entries, so a
swarm can be strict from its start.
Auth plan M7.
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/registry.rs | 72 |
1 files changed, 62 insertions, 10 deletions
diff --git a/src/access/registry.rs b/src/access/registry.rs index 618402b..4ee51ff 100644 --- a/src/access/registry.rs +++ b/src/access/registry.rs @@ -180,15 +180,48 @@ pub(crate) fn capability(by: &str, since: u64, path: &[String], rights: &[Right] } /// The first users of a new swarm, as its genesis node records them (`first_users`), -/// each with everything (`all_caps`). -pub(crate) type FirstUsers = Vec<String>; +/// each with everything (`all_caps`), and with its keys, by name (`User::keys`). +pub(crate) type FirstUsers = BTreeMap<String, BTreeMap<String, String>>; -/// The first users of a new swarm, from the genesis node's `--bootstrap-users`, sorted. -/// None without: then the swarm has no registry, and its users are all alike -/// (`access.md`, A swarm without a registry). -pub(crate) fn first_users(bootstrap_users: &[String]) -> FirstUsers { - let users: std::collections::BTreeSet<&String> = bootstrap_users.iter().collect(); - users.into_iter().cloned().collect() +/// The first users of a new swarm, from the genesis node's `--bootstrap-users`, with +/// their keys from `--bootstrap-keys` (`USER=ssh-ed25519 <base64> [comment]`, named by +/// the comment if it is a name, else `key1`, `key2`…). None without: then the swarm has +/// no registry, and its users are all alike (`access.md`, A swarm without a registry). +/// A key of a user not among them, or that isn't one, is refused, saying why. +pub(crate) fn first_users( + bootstrap_users: &[String], + bootstrap_keys: &[String], +) -> Result<FirstUsers, String> { + let mut users: FirstUsers = bootstrap_users + .iter() + .map(|name| (name.clone(), BTreeMap::new())) + .collect(); + for item in bootstrap_keys { + let Some((name, line)) = item.split_once('=') else { + return Err(format!( + "bootstrap-keys: {item:?} isn't USER=ssh-ed25519 <base64> [comment]" + )); + }; + let name = name.trim(); + let keys = users + .get_mut(name) + .ok_or_else(|| format!("bootstrap-keys: {name:?} isn't among bootstrap-users"))?; + let key = super::sshsig::PublicKey::parse(line) + .map_err(|why| format!("bootstrap-keys: {name}'s key: {why}"))?; + let comment = line + .split_whitespace() + .nth(2) + .filter(|comment| valid_name(comment)); + let id = match comment { + Some(comment) if !keys.contains_key(comment) => comment.to_owned(), + _ => (1..) + .map(|n| format!("key{n}")) + .find(|id| !keys.contains_key(id)) + .expect("a free name"), + }; + keys.insert(id, key.line()); + } + Ok(users) } /// Everything, as a first user has it (`first_users`): the swarm's data and protocol @@ -281,12 +314,31 @@ mod tests { #[test] fn the_first_users() { let names = |list: &[&str]| list.iter().map(|n| (*n).to_owned()).collect::<Vec<_>>(); + let users = first_users(&names(&["bob", "alice", "bob"]), &[]).unwrap(); assert_eq!( - first_users(&names(&["bob", "alice", "bob"])), + users.keys().cloned().collect::<Vec<_>>(), names(&["alice", "bob"]) ); // Without bootstrap users, no registry. - assert!(first_users(&[]).is_empty()); + assert!(first_users(&[], &[]).unwrap().is_empty()); + // Their keys: by the comment if it is a name, else numbered. + let key = super::super::sshsig::PublicKey([4; 32]).line(); + let keyed = first_users( + &names(&["alice"]), + &[ + format!("alice={key} laptop"), + format!("alice={key}"), + format!("alice={key} not/a/name"), + ], + ) + .unwrap(); + assert_eq!( + keyed["alice"].keys().cloned().collect::<Vec<_>>(), + names(&["key1", "key2", "laptop"]) + ); + assert!(first_users(&names(&["alice"]), &[format!("bob={key}")]).is_err()); + assert!(first_users(&names(&["alice"]), &["alice=ssh-rsa AAAA".to_owned()]).is_err()); + assert!(first_users(&names(&["alice"]), &[key]).is_err()); let all = Caps::new( all_caps() .into_iter() |
