From f7465da797f1198092054d6d1bc4c1bda357bee7 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 18 Oct 2021 17:28:01 -0600 Subject: [PATCH] Certificate Imports/Exports (#687) * allows for root/int certificate imports, certain invariants are not checked at this time * refactor ssl module to only import root certificate * implements certificate export, propagates import functionality all the way to NetController::init --- appmgr/src/context/rpc.rs | 1 + appmgr/src/net/mod.rs | 14 +++++++++- appmgr/src/net/nginx.rs | 34 +++++++++++++----------- appmgr/src/net/ssl.rs | 54 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/appmgr/src/context/rpc.rs b/appmgr/src/context/rpc.rs index 957a9f16a..931f94926 100644 --- a/appmgr/src/context/rpc.rs +++ b/appmgr/src/context/rpc.rs @@ -156,6 +156,7 @@ impl RpcContext { base.tor_control .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), secret_store.clone(), + None, ) .await?; let managers = ManagerMap::default(); diff --git a/appmgr/src/net/mod.rs b/appmgr/src/net/mod.rs index 60054a001..b6eb413b9 100644 --- a/appmgr/src/net/mod.rs +++ b/appmgr/src/net/mod.rs @@ -1,6 +1,8 @@ use std::net::{Ipv4Addr, SocketAddr}; use std::path::PathBuf; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; use rpc_toolkit::command; use sqlx::SqlitePool; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; @@ -10,6 +12,7 @@ use self::interface::{Interface, InterfaceId}; #[cfg(feature = "avahi")] use self::mdns::MdnsController; use self::nginx::NginxController; +use self::ssl::SslManager; use self::tor::TorController; use crate::net::interface::TorConfig; use crate::net::nginx::InterfaceMetadata; @@ -42,12 +45,17 @@ impl NetController { embassyd_tor_key: TorSecretKeyV3, tor_control: SocketAddr, db: SqlitePool, + import_root_ca: Option<(PKey, X509)>, ) -> Result { + let ssl_manager = match import_root_ca { + None => SslManager::init(db).await, + Some(a) => SslManager::import_root_ca(db, a.0, a.1).await, + }?; Ok(Self { tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?, #[cfg(feature = "avahi")] mdns: MdnsController::init(), - nginx: NginxController::init(PathBuf::from("/etc/nginx"), db).await?, + nginx: NginxController::init(PathBuf::from("/etc/nginx"), ssl_manager).await?, }) } @@ -129,4 +137,8 @@ impl NetController { nginx_res?; Ok(()) } + + pub async fn export_root_ca(&self) -> Result<(PKey, X509), Error> { + self.nginx.ssl_manager.export_root_ca().await + } } diff --git a/appmgr/src/net/nginx.rs b/appmgr/src/net/nginx.rs index 42338ae62..ec94a2a45 100644 --- a/appmgr/src/net/nginx.rs +++ b/appmgr/src/net/nginx.rs @@ -18,12 +18,14 @@ use crate::{Error, ErrorKind, ResultExt}; pub struct NginxController { nginx_root: PathBuf, + pub ssl_manager: SslManager, inner: Mutex, } impl NginxController { - pub async fn init(nginx_root: PathBuf, db: SqlitePool) -> Result { + pub async fn init(nginx_root: PathBuf, ssl_manager: SslManager) -> Result { Ok(NginxController { - inner: Mutex::new(NginxControllerInner::init(&nginx_root, db).await?), + inner: Mutex::new(NginxControllerInner::init(&nginx_root, &ssl_manager).await?), + ssl_manager, nginx_root, }) } @@ -39,7 +41,13 @@ impl NginxController { self.inner .lock() .await - .add(&self.nginx_root, package, ipv4, interfaces) + .add( + &self.nginx_root, + &self.ssl_manager, + package, + ipv4, + interfaces, + ) .await } pub async fn remove(&self, package: &PackageId) -> Result<(), Error> { @@ -53,22 +61,17 @@ impl NginxController { pub struct NginxControllerInner { interfaces: BTreeMap, - ssl_manager: SslManager, } impl NginxControllerInner { - #[instrument(skip(db))] - async fn init(nginx_root: &Path, db: SqlitePool) -> Result { + #[instrument] + async fn init(nginx_root: &Path, ssl_manager: &SslManager) -> Result { let inner = NginxControllerInner { interfaces: BTreeMap::new(), - ssl_manager: SslManager::init(db).await?, }; - let (key, cert) = inner - .ssl_manager - .certificate_for(&get_hostname().await?) - .await?; + let (key, cert) = ssl_manager.certificate_for(&get_hostname().await?).await?; let ssl_path_key = nginx_root.join(format!("ssl/embassy_main.key.pem")); let ssl_path_cert = nginx_root.join(format!("ssl/embassy_main.cert.pem")); - futures::try_join!( + tokio::try_join!( tokio::fs::write(&ssl_path_key, key.private_key_to_pem_pkcs8()?), tokio::fs::write( &ssl_path_cert, @@ -83,6 +86,7 @@ impl NginxControllerInner { async fn add>( &mut self, nginx_root: &Path, + ssl_manager: &SslManager, package: PackageId, ipv4: Ipv4Addr, interfaces: I, @@ -108,9 +112,9 @@ impl NginxControllerInner { nginx_root.join(format!("ssl/{}/{}.key.pem", package, id)); let ssl_path_cert = nginx_root.join(format!("ssl/{}/{}.cert.pem", package, id)); - let (key, chain) = self.ssl_manager.certificate_for(&meta.dns_base).await?; + let (key, chain) = ssl_manager.certificate_for(&meta.dns_base).await?; // write nginx ssl certs - futures::try_join!( + tokio::try_join!( tokio::fs::write(&ssl_path_key, key.private_key_to_pem_pkcs8()?).map( |res| res.with_ctx(|_| ( ErrorKind::Filesystem, @@ -197,7 +201,7 @@ impl NginxControllerInner { nginx_root.join(format!("sites-enabled/{}_{}.conf", package, id)); let available_path = nginx_root.join(format!("sites-available/{}_{}.conf", package, id)); - let _ = futures::try_join!( + let _ = tokio::try_join!( async { if tokio::fs::metadata(&package_path).await.is_ok() { tokio::fs::remove_dir_all(&package_path) diff --git a/appmgr/src/net/ssl.rs b/appmgr/src/net/ssl.rs index af75bcd14..a484e4b09 100644 --- a/appmgr/src/net/ssl.rs +++ b/appmgr/src/net/ssl.rs @@ -17,6 +17,7 @@ use crate::{Error, ErrorKind}; static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. +#[derive(Debug)] pub struct SslManager { store: SslStore, root_cert: X509, @@ -24,6 +25,7 @@ pub struct SslManager { int_cert: X509, } +#[derive(Debug)] struct SslStore { secret_store: SqlitePool, } @@ -79,6 +81,19 @@ impl SslStore { } } #[instrument(skip(self))] + async fn import_root_certificate( + &self, + root_key: &PKey, + root_cert: &X509, + ) -> Result<(), Error> { + // remove records for both root and intermediate CA + sqlx::query!("DELETE FROM certificates WHERE id = 0 OR id = 1;") + .execute(&self.secret_store) + .await?; + self.save_root_certificate(root_key, root_cert).await?; + Ok(()) + } + #[instrument(skip(self))] async fn save_certificate( &self, key: &PKey, @@ -170,6 +185,45 @@ impl SslManager { }) } + // TODO: currently the burden of proof is on the caller to ensure that all of the arguments to this function are + // consistent. The following properties are assumed and not verified: + // 1. `root_cert` is self-signed and contains the public key that matches the private key `root_key` + // 2. certificate is not past its expiration date + // Warning: If this function ever fails, you must either call it again or regenerate your certificates from scratch + // since it is possible for it to fail after successfully saving the root certificate but before successfully saving + // the intermediate certificate + #[instrument(skip(db))] + pub async fn import_root_ca( + db: SqlitePool, + root_key: PKey, + root_cert: X509, + ) -> Result { + let store = SslStore::new(db)?; + store.import_root_certificate(&root_key, &root_cert).await?; + let int_key = generate_key()?; + let int_cert = make_int_cert((&root_key, &root_cert), &int_key)?; + store + .save_intermediate_certificate(&int_key, &int_cert) + .await?; + Ok(SslManager { + store, + root_cert, + int_key, + int_cert, + }) + } + + #[instrument(skip(self))] + pub async fn export_root_ca(&self) -> Result<(PKey, X509), Error> { + match self.store.load_root_certificate().await? { + None => Err(Error::new( + eyre!("Failed to export root certificate: root certificate has not been generated"), + ErrorKind::OpenSsl, + )), + Some(a) => Ok(a), + } + } + #[instrument(skip(self))] pub async fn certificate_for( &self,