aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 02:49:25 +0000
committertv <tv@krebsco.de>2026-11-09 02:49:25 +0000
commitc2e566599aed088973b5a5be2bb04ee69cee5c7a (patch)
tree11fb923c99318f6e927bcaff65ed918865b6ab54
parent51c7a4c2588b3f4064ba116276796741b3170de6 (diff)
access: keep an SSH key's comment
A key's line was stored as the key alone (`ssh-ed25519 <base64>`), whatever comment it came with, both from `PUT /auth/keys/<name>` and from `--bootstrap-keys`. Now its comment is kept after it (`PublicKey::kept_line`), the spaces between its words made one. The comment checks nothing: every use of a key parses the line, and the parse reads the key alone.
-rw-r--r--docs/access.md4
-rw-r--r--src/access/registry.rs4
-rw-r--r--src/access/sshsig.rs19
-rw-r--r--src/api/users.rs4
4 files changed, 25 insertions, 6 deletions
diff --git a/docs/access.md b/docs/access.md
index 4bb5945..7392bb9 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -30,8 +30,8 @@ password hashes: every member reads the document).
An entry that doesn't read as above gives less, never more: a capability that
doesn't read is left out, and anything but `false` disables.
* **`keys`**: the user's SSH keys, each by a name of its choosing, as an
- `authorized_keys` line has it (`"laptop": "ssh-ed25519 AAAA…"`; Ed25519 only): what
- it signs values with (Values signed by their users, below) and logs in with. A user
+ `authorized_keys` line has it (`"laptop": "ssh-ed25519 AAAA… alice@laptop"`; Ed25519
+ only; the comment is kept and checks nothing): what it signs values with (Values signed by their users, below) and logs in with. A user
manages its own, whatever else it may do: `GET /auth/keys`, `PUT /auth/keys/<name>`
with the line, `DELETE /auth/keys/<name>`; under a strict policy, by signing its
entry with only its keys changed (`continuum-signed key-add laptop
diff --git a/src/access/registry.rs b/src/access/registry.rs
index 4ee51ff..01e6ba9 100644
--- a/src/access/registry.rs
+++ b/src/access/registry.rs
@@ -206,7 +206,7 @@ pub(crate) fn first_users(
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)
+ let kept = super::sshsig::PublicKey::kept_line(line)
.map_err(|why| format!("bootstrap-keys: {name}'s key: {why}"))?;
let comment = line
.split_whitespace()
@@ -219,7 +219,7 @@ pub(crate) fn first_users(
.find(|id| !keys.contains_key(id))
.expect("a free name"),
};
- keys.insert(id, key.line());
+ keys.insert(id, kept);
}
Ok(users)
}
diff --git a/src/access/sshsig.rs b/src/access/sshsig.rs
index 6091528..799cb87 100644
--- a/src/access/sshsig.rs
+++ b/src/access/sshsig.rs
@@ -65,6 +65,19 @@ impl PublicKey {
format!("{KEY_TYPE} {}", BASE64.encode(self.blob()))
}
+ /// `line` (`ssh-ed25519 <base64> [comment]`) as a registry keeps it: the key as
+ /// `line` makes it, then its comment, if any, as it was (its spaces between words
+ /// made one). The comment is for whoever reads it: the key alone checks a signature.
+ pub(crate) fn kept_line(line: &str) -> Result<String, String> {
+ let key = Self::parse(line)?;
+ let comment: Vec<&str> = line.split_whitespace().skip(2).collect();
+ Ok(if comment.is_empty() {
+ key.line()
+ } else {
+ format!("{} {}", key.line(), comment.join(" "))
+ })
+ }
+
/// Its fingerprint, as `ssh-keygen -l` shows it: `SHA256:` and the base64 of its
/// encoding's SHA-256, unpadded.
pub(crate) fn fingerprint(&self) -> String {
@@ -244,6 +257,12 @@ DNg4vBWp1j1gLRiBMOF+gwYNegDg==
assert!(verify(OPENSSH_SIGNATURE, "unittest", b"This is a test").is_err());
assert_eq!(PublicKey::parse(&key.line()), Ok(key));
assert!(key.fingerprint().starts_with("SHA256:"));
+ // Kept with its comment, which doesn't change the key.
+ let commented =
+ PublicKey::kept_line(&format!("{} alice@laptop 2026", key.line())).unwrap();
+ assert_eq!(commented, format!("{} alice@laptop 2026", key.line()));
+ assert_eq!(PublicKey::parse(&commented), Ok(key));
+ assert_eq!(PublicKey::kept_line(&key.line()), Ok(key.line()));
}
/// What `sign` makes checks, and reads as `ssh-keygen` writes it.
diff --git a/src/api/users.rs b/src/api/users.rs
index d6950af..fc3f6dc 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -171,8 +171,8 @@ pub(crate) async fn put_own_key(
Path(id): Path<String>,
body: String,
) -> Result<StatusCode, ApiError> {
- let key = crate::access::sshsig::PublicKey::parse(body.trim()).map_err(|why| bad(&why))?;
- own_key(&state, &user.0, id, Some(key.line()), false).await
+ let line = crate::access::sshsig::PublicKey::kept_line(body.trim()).map_err(|why| bad(&why))?;
+ own_key(&state, &user.0, id, Some(line), false).await
}
/// `DELETE /auth/keys/{id}`.