From 4ccdaf472518b8ae8c965287f6eb05242850bff4 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 30 Oct 2021 14:35:18 +0100 Subject: [PATCH] Store the SSH server's private keys in the database --- Cargo.lock | 1 + chartered-db/Cargo.toml | 1 + chartered-db/src/lib.rs | 1 + chartered-db/src/schema.rs | 9 +++++++++ chartered-db/src/server_private_key.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-git/src/main.rs | 13 +++++++++++-- migrations/2021-08-31-214501_create_crates_table/down.sql | 1 + migrations/2021-08-31-214501_create_crates_table/up.sql | 6 ++++++ 8 files changed, 143 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60afafa..ec64b38 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -465,6 +465,7 @@ "thiserror", "thrussh-keys", "tokio", + "tracing", "uuid", ] diff --git a/chartered-db/Cargo.toml b/chartered-db/Cargo.toml index aa8f3fb..beefcd0 100644 --- a/chartered-db/Cargo.toml +++ a/chartered-db/Cargo.toml @@ -26,6 +26,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "1" +tracing = "0.1" tokio = "1" uuid = "0.8" dotenv = "0.15" diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index d58cd18..0254930 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -36,6 +36,7 @@ pub mod organisations; pub mod permissions; pub mod schema; +pub mod server_private_key; pub mod users; pub mod uuid; diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index cebf439..8472e62 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -41,6 +41,14 @@ } table! { + server_private_keys (id) { + id -> Integer, + ssh_key_type -> Text, + ssh_private_key -> Binary, + } +} + +table! { user_crate_permissions (id) { id -> Integer, user_id -> Integer, @@ -110,6 +118,7 @@ crate_versions, crates, organisations, + server_private_keys, user_crate_permissions, user_organisation_permissions, user_sessions, diff --git a/chartered-db/src/server_private_key.rs b/chartered-db/src/server_private_key.rs new file mode 100644 index 0000000..dadd292 100644 --- /dev/null +++ a/chartered-db/src/server_private_key.rs @@ -1,0 +1,113 @@ +use crate::{schema::server_private_keys, ConnectionPool, Error as CrateError}; +use diesel::{ + insert_into, prelude::*, result::DatabaseErrorKind, result::Error as DieselError, Associations, + Identifiable, Queryable, +}; +use displaydoc::Display; +use thiserror::Error; +use thrussh_keys::key::{self, KeyPair}; +use tracing::{info, info_span}; + +/// Represents a single SSH private key for the server. +/// +/// We store these in the database as we need consistency across all hosts that may be +/// running `chartered-git` so clients don't get MITM warnings. +#[derive(Identifiable, Queryable, Associations, Default, PartialEq, Eq, Hash, Debug)] +pub struct ServerPrivateKey { + pub id: i32, + pub ssh_key_type: String, + pub ssh_private_key: Vec, +} + +impl ServerPrivateKey { + /// Creates all the required keys for the server (currently just ed25519), ignoring any + /// UNIQUE constraint errors. + pub async fn create_if_not_exists(conn: ConnectionPool) -> Result<(), PrivateKeyError> { + tokio::task::spawn_blocking(move || { + info_span!("create_if_not_exists").in_scope(move || { + let conn = conn.get()?; + + // diesel-tracing prints the UNIQUE constraint error even though we ignore it + info!( + "Generating an ed25519 key if it doesn't already exist, UNIQUE constraint \ + errors here can be safely ignored." + ); + + let ed25519_key = key::KeyPair::generate_ed25519() + .ok_or(PrivateKeyError::KeyGenerate("ed25519"))?; + + let res = insert_into(server_private_keys::table) + .values(( + server_private_keys::ssh_key_type.eq("ed25519"), + server_private_keys::ssh_private_key.eq(private_key_bytes(&ed25519_key)?), + )) + .execute(&conn); + + match res { + Ok(_) + | Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => { + Ok(()) + } + Err(e) => Err(e.into()), + } + }) + }) + .await? + } + + pub async fn fetch_all(conn: ConnectionPool) -> Result, CrateError> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + server_private_keys::table + .select(server_private_keys::all_columns) + .load(&conn) + .map_err(Into::into) + }) + .await? + } + + /// Converts this `ServerPrivateKey` to thrussh's `KeyPair` type. + pub fn into_private_key(self) -> Result { + match self.ssh_key_type.as_str() { + "ed25519" => { + if self.ssh_private_key.len() != 64 { + return Err(PrivateKeyError::InvalidPrivateKey(self.ssh_key_type)); + } + + let mut private_key = [0_u8; 64]; + private_key.copy_from_slice(&self.ssh_private_key); + + Ok(KeyPair::Ed25519(key::ed25519::SecretKey { + key: private_key, + })) + } + _ => Err(PrivateKeyError::UnknownPrivateKeyType(self.ssh_key_type)), + } + } +} + +/// Grabs the private key bytes out of a `thrussh_keys::KeyPair`. +fn private_key_bytes(key: &KeyPair) -> Result, PrivateKeyError> { + #[allow(unreachable_patterns)] + match key { + KeyPair::Ed25519(key::ed25519::SecretKey { key }) => Ok(key.to_vec()), + _ => Err(PrivateKeyError::KeyGenerate(key.name())), + } +} + +#[derive(Error, Display, Debug)] +pub enum PrivateKeyError { + /// Failed to generate {0} private key + KeyGenerate(&'static str), + /// Invalid {0} private key + InvalidPrivateKey(String), + /// Found {0} private key but chartered cannot handle this type + UnknownPrivateKeyType(String), + /// Failed to initialise to database connection pool + Connection(#[from] diesel::r2d2::PoolError), + /// Failed to complete query task + TaskJoin(#[from] tokio::task::JoinError), + /// {0} + Query(#[from] DieselError), +} diff --git a/chartered-git/src/main.rs b/chartered-git/src/main.rs index 8d7afa7..35c70ff 100644 --- a/chartered-git/src/main.rs +++ a/chartered-git/src/main.rs @@ -20,6 +20,7 @@ use arrayvec::ArrayVec; use bytes::BytesMut; +use chartered_db::server_private_key::ServerPrivateKey; use clap::Parser; use futures::future::Future; use std::{fmt::Write, path::PathBuf, pin::Pin, sync::Arc}; @@ -66,16 +67,24 @@ tracing_subscriber::fmt::init(); + let db = chartered_db::init(&config.database_uri)?; + + ServerPrivateKey::create_if_not_exists(db.clone()).await?; + let keys = ServerPrivateKey::fetch_all(db.clone()).await?; + let trussh_config = Arc::new(thrussh::server::Config { methods: thrussh::MethodSet::PUBLICKEY, - keys: vec![key::KeyPair::generate_ed25519().unwrap()], + keys: keys + .into_iter() + .map(ServerPrivateKey::into_private_key) + .collect::>()?, ..thrussh::server::Config::default() }); let bind_address = config.bind_address; let server = Server { - db: chartered_db::init(&config.database_uri)?, + db, config: Arc::new(config), }; diff --git a/migrations/2021-08-31-214501_create_crates_table/down.sql b/migrations/2021-08-31-214501_create_crates_table/down.sql index cefd3da..8322caf 100644 --- a/migrations/2021-08-31-214501_create_crates_table/down.sql +++ a/migrations/2021-08-31-214501_create_crates_table/down.sql @@ -6,3 +6,4 @@ DROP TABLE user_crate_permissions; DROP TABLE user_ssh_keys; DROP TABLE user_sessions; +DROP TABLE server_private_keys; diff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql index ffa58e9..9d19859 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -97,3 +97,9 @@ FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (user_ssh_key_id) REFERENCES user_ssh_keys (id) ); + +CREATE TABLE server_private_keys ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ssh_key_type VARCHAR(255) NOT NULL UNIQUE, + ssh_private_key BLOB NOT NULL UNIQUE +); -- rgit 0.1.3