🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-30 14:35:18.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-30 14:35:18.0 +01:00:00
commit
4ccdaf472518b8ae8c965287f6eb05242850bff4 [patch]
tree
727beeb9dbf4e08b280a701eecee8b20a8863b84
parent
1649b7bbeb553316398826702d292075ef07fc08
download
4ccdaf472518b8ae8c965287f6eb05242850bff4.tar.gz

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(-)

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<u8>,
}

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<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?
    }

    /// Converts this `ServerPrivateKey` to thrussh's `KeyPair` type.

    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)),
        }
    }
}

/// Grabs the private key bytes out of a `thrussh_keys::KeyPair`.

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 {
    /// 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::<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),
    };

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
);