// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Public keys: SSH (push auth + signatures) and GPG (signatures). use model::{Key, KeyKind}; use sqlx::any::AnyRow; use sqlx::{AssertSqlSafe, Row}; use crate::{Store, StoreError, map_conflict, new_id, now_ms}; /// The columns of `keys` in a fixed order, shared by every `SELECT`. const KEY_COLUMNS: &str = "id, user_id, kind, name, fingerprint, public_key, ordinal, \ last_used_at, verified_at, created_at"; /// Fields needed to register a key. The fingerprint is computed by the caller /// (which parses and validates the key material); the store assigns the id and /// ordinal. #[derive(Debug, Clone)] pub struct NewKey { /// Owning user id. pub user_id: String, /// SSH or GPG. pub kind: KeyKind, /// Optional label. pub name: Option, /// `SHA256:…` (SSH) or hex (GPG) fingerprint. pub fingerprint: String, /// The `authorized_keys` line or armored block. pub public_key: String, } impl Store { /// Register a key, assigning the next ordinal for its owner. /// /// Ordinals are assigned unique **per user** (across both kinds), not merely /// per `(user, kind)`, so `fabrica key del ` needs no `--type` to /// be unambiguous. Deleting a key never renumbers the survivors. /// /// # Errors /// /// * [`StoreError::Conflict`] if the fingerprint is already registered. /// * [`StoreError::Query`] on any other database failure. pub async fn create_key(&self, new: NewKey) -> Result { let next: i64 = sqlx::query( "SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM keys WHERE user_id = $1", ) .bind(&new.user_id) .fetch_one(&self.pool) .await? .try_get("next")?; let key = Key { id: new_id(), user_id: new.user_id, kind: new.kind, name: new.name, fingerprint: new.fingerprint, public_key: new.public_key, ordinal: next, last_used_at: None, verified_at: None, created_at: now_ms(), }; let sql = "INSERT INTO keys \ (id, user_id, kind, name, fingerprint, public_key, ordinal, last_used_at, \ verified_at, created_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"; sqlx::query(sql) .bind(key.id.clone()) .bind(key.user_id.clone()) .bind(key.kind.as_str()) .bind(key.name.clone()) .bind(key.fingerprint.clone()) .bind(key.public_key.clone()) .bind(key.ordinal) .bind(key.last_used_at) .bind(key.verified_at) .bind(key.created_at) .execute(&self.pool) .await .map_err(|e| map_conflict(e, "key", &[("fingerprint", "fingerprint")]))?; Ok(key) } /// List a user's keys, ordered by ordinal (i.e. registration order). /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn keys_by_user(&self, user_id: &str) -> Result, StoreError> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys WHERE user_id = $1 ORDER BY ordinal ASC"); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(user_id) .fetch_all(&self.pool) .await?; rows.iter() .map(map_key) .collect::>() .map_err(Into::into) } /// Every registered key on the instance, for signature verification (the /// signer may be any registered user). /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn all_keys(&self) -> Result, StoreError> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys"); let rows = sqlx::query(AssertSqlSafe(sql)) .fetch_all(&self.pool) .await?; rows.iter() .map(map_key) .collect::>() .map_err(Into::into) } /// Fetch a user's key by its 1-based ordinal, or `None`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn key_by_ordinal( &self, user_id: &str, ordinal: i64, ) -> Result, StoreError> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys WHERE user_id = $1 AND ordinal = $2"); let row = sqlx::query(AssertSqlSafe(sql)) .bind(user_id) .bind(ordinal) .fetch_optional(&self.pool) .await?; Ok(row.as_ref().map(map_key).transpose()?) } /// Look up any key by `(kind, fingerprint)` — used to reject a duplicate before /// insert and name the existing owner, and by SSH auth to bind a session. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn key_by_fingerprint( &self, kind: KeyKind, fingerprint: &str, ) -> Result, StoreError> { let sql = format!("SELECT {KEY_COLUMNS} FROM keys WHERE kind = $1 AND fingerprint = $2"); let row = sqlx::query(AssertSqlSafe(sql)) .bind(kind.as_str()) .bind(fingerprint) .fetch_optional(&self.pool) .await?; Ok(row.as_ref().map(map_key).transpose()?) } /// Stamp a key's `last_used_at` with the current time (SSH auth binding). /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn touch_key_used(&self, key_id: &str) -> Result<(), StoreError> { sqlx::query("UPDATE keys SET last_used_at = $1 WHERE id = $2") .bind(now_ms()) .bind(key_id) .execute(&self.pool) .await?; Ok(()) } /// Mark a key's ownership verified (idempotent; sets `verified_at` once). /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn set_key_verified(&self, key_id: &str) -> Result<(), StoreError> { sqlx::query("UPDATE keys SET verified_at = $1 WHERE id = $2 AND verified_at IS NULL") .bind(now_ms()) .bind(key_id) .execute(&self.pool) .await?; Ok(()) } /// Delete a key by id. Returns `true` if a row was removed. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn delete_key(&self, key_id: &str) -> Result { let deleted = sqlx::query("DELETE FROM keys WHERE id = $1") .bind(key_id) .execute(&self.pool) .await? .rows_affected(); Ok(deleted > 0) } } /// Map a `keys` row (selected via [`KEY_COLUMNS`]) to a [`Key`]. fn map_key(row: &AnyRow) -> Result { let kind_str: String = row.try_get("kind")?; let kind = KeyKind::from_token(&kind_str) .ok_or_else(|| sqlx::Error::Decode(format!("invalid key kind {kind_str:?}").into()))?; Ok(Key { id: row.try_get("id")?, user_id: row.try_get("user_id")?, kind, name: row.try_get("name")?, fingerprint: row.try_get("fingerprint")?, public_key: row.try_get("public_key")?, ordinal: row.try_get("ordinal")?, last_used_at: row.try_get("last_used_at")?, verified_at: row.try_get("verified_at")?, created_at: row.try_get("created_at")?, }) }