anvilsign in

collin/anvil

1//! SSH public keys: parsing, registration, and lookup for git-over-SSH
2//! authentication.
3
4use ssh_key::{HashAlg, PublicKey};
5
6use crate::error::{Error, Result};
7use crate::models::SshKey;
8
9/// Parse an OpenSSH public-key line (`ssh-ed25519 AAAA… comment`) into its
10/// canonical SHA256 fingerprint and normalized key line, for registration.
11pub fn parse_public_key(openssh: &str) -> Result<(String, String)> {
12 let key = PublicKey::from_openssh(openssh.trim())
13 .map_err(|e| Error::Invalid(format!("invalid ssh public key: {e}")))?;
14 let fingerprint = key.fingerprint(HashAlg::Sha256).to_string();
15 let normalized = key
16 .to_openssh()
17 .map_err(|e| Error::Invalid(format!("encoding ssh public key: {e}")))?;
18 Ok((fingerprint, normalized))
19}
20
21/// Register an SSH public key for a user.
22///
23/// `fingerprint` must be the canonical SHA256 fingerprint and `content` the
24/// normalized OpenSSH key line. Returns [`Error::AlreadyExists`] if the
25/// fingerprint is already registered.
26pub async fn add(
27 db: &toasty::Db,
28 user_id: i64,
29 title: &str,
30 fingerprint: &str,
31 content: &str,
32) -> Result<SshKey> {
33 if find_by_fingerprint(db, fingerprint).await?.is_some() {
34 return Err(Error::AlreadyExists(format!("ssh key {fingerprint}")));
35 }
36 let mut db = db.clone();
37 let key = toasty::create!(SshKey {
38 user_id: user_id,
39 title: title,
40 fingerprint: fingerprint,
41 content: content,
42 created_at: crate::now(),
43 })
44 .exec(&mut db)
45 .await?;
46 Ok(key)
47}
48
49/// Look up a key by its fingerprint.
50pub async fn find_by_fingerprint(db: &toasty::Db, fingerprint: &str) -> Result<Option<SshKey>> {
51 let mut db = db.clone();
52 let key = SshKey::filter(SshKey::fields().fingerprint().eq(fingerprint))
53 .first()
54 .exec(&mut db)
55 .await?;
56 Ok(key)
57}
58
59/// Find the user id that owns the key with this fingerprint, if any.
60pub async fn find_user_id_by_fingerprint(
61 db: &toasty::Db,
62 fingerprint: &str,
63) -> Result<Option<i64>> {
64 Ok(find_by_fingerprint(db, fingerprint)
65 .await?
66 .map(|k| k.user_id))
67}
68
69/// Delete one of `user_id`'s keys by id. No-op if the key is missing or owned
70/// by someone else.
71pub async fn delete(db: &toasty::Db, id: i64, user_id: i64) -> Result<()> {
72 let mut conn = db.clone();
73 if let Some(key) = SshKey::filter(SshKey::fields().id().eq(id))
74 .first()
75 .exec(&mut conn)
76 .await?
77 && key.user_id == user_id
78 {
79 let mut conn = db.clone();
80 key.delete().exec(&mut conn).await?;
81 }
82 Ok(())
83}
84
85/// List a user's registered SSH keys.
86pub async fn list_by_user(db: &toasty::Db, user_id: i64) -> Result<Vec<SshKey>> {
87 let mut db = db.clone();
88 let keys = SshKey::filter(SshKey::fields().user_id().eq(user_id))
89 .exec(&mut db)
90 .await?;
91 Ok(keys)
92}