Store the SSH server's private keys in the database
Diff
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(-)
@@ -465,6 +465,7 @@
"thiserror",
"thrussh-keys",
"tokio",
"tracing",
"uuid",
]
@@ -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"
@@ -36,6 +36,7 @@
pub mod organisations;
pub mod permissions;
pub mod schema;
pub mod server_private_key;
pub mod users;
pub mod uuid;
@@ -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,
@@ -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};
#[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<u8>,
}
impl ServerPrivateKey {
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()?;
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<Vec<Self>, 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?
}
pub fn into_private_key(self) -> Result<KeyPair, PrivateKeyError> {
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)),
}
}
}
fn private_key_bytes(key: &KeyPair) -> Result<Vec<u8>, 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 {
KeyGenerate(&'static str),
InvalidPrivateKey(String),
UnknownPrivateKeyType(String),
Connection(#[from] diesel::r2d2::PoolError),
TaskJoin(#[from] tokio::task::JoinError),
Query(#[from] DieselError),
}
@@ -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::<Result<_, _>>()?,
..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),
};
@@ -6,3 +6,4 @@
DROP TABLE user_crate_permissions;
DROP TABLE user_ssh_keys;
DROP TABLE user_sessions;
DROP TABLE server_private_keys;
@@ -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
);