Skip to content

Commit

Permalink
Switched to CertificateChain
Browse files Browse the repository at this point in the history
In my original implementation, I had missed the CertificateChain type
entirely. Now, the certificate chain is always stored, with the only
complication being that if you want to use the pinned certificate, you
must extract it from the chain first -- we can make this a little less
verbose, I'm just not sure what direction I want to take it right now.

The other change is that the cli command to install a certificate now
accepts PEMs. This has been tested manually using a PEM generated from
certbot, so hopefully #38 will be a breeze to hook up as well.

Also want to mention khonsulabs/fabruic#26 here to show how easy the PEM
conversion is. However, since there is additional validation able to be
done, I suspect that the implementation in fabruic should be more
extensive than what I've done here. For now, installing will work with
an invalid certificate, but it will obviously fail to actually work
(returning errors).
  • Loading branch information
ecton committed Nov 10, 2021
1 parent 52482ad commit 61c42ef
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 44 deletions.
3 changes: 2 additions & 1 deletion crates/bonsaidb-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ homepage = "https://dev.bonsaidb.io/"
[features]
default = ["full"]
full = ["cli", "websockets"]
cli = ["structopt"]
cli = ["structopt", "pem"]
test-util = ["bonsaidb-core/test-util"]
websockets = ["bonsaidb-core/websockets", "tokio-tungstenite", "bincode"]
tracing = ["pot/tracing"]
Expand All @@ -40,6 +40,7 @@ bincode = { version = "1", optional = true }
actionable = "0.1.0-rc.2"
pot = "0.1.0-alpha.2"
fabruic = { version = "0.0.1-dev.2", features = ["dangerous"] }
pem = { version = "1", optional = true }

[dev-dependencies]
bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0-dev.4", default-features = false, features = [
Expand Down
34 changes: 21 additions & 13 deletions crates/bonsaidb-server/src/cli/certificate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use fabruic::{Certificate, PrivateKey};
use fabruic::{Certificate, CertificateChain, PrivateKey};
use structopt::StructOpt;
use tokio::io::AsyncReadExt;

Expand All @@ -27,16 +27,16 @@ pub enum Command {
#[structopt(short, long)]
overwrite: bool,
},
/// Installs a X.509 certificate and associated private key in binary DER
/// format.
/// Installs a X.509 certificate and associated private key in PEM format.
///
/// This command reads the files `private_key` and `certificate` and
/// executes [`Server::install_certificate()`](crate::CustomServer::install_certificate).
/// executes
/// [`Server::install_certificate()`](crate::CustomServer::install_certificate).
Install {
/// A private key used to generate `certificate` in binary DER format.
/// A private key used to generate `certificate` in the ASCII PEM format.
private_key: PathBuf,
/// The X.509 certificate in binary DER format.
certificate: PathBuf,
/// The X.509 certificate chain in the ASCII PEM format.
certificate_chain: PathBuf,
},
}

Expand All @@ -54,20 +54,28 @@ impl Command {
}
Self::Install {
private_key,
certificate,
certificate_chain,
} => {
let mut private_key_file = tokio::fs::File::open(&private_key).await?;
let mut private_key = Vec::new();
private_key_file.read_to_end(&mut private_key).await?;

let mut certificate_file = tokio::fs::File::open(&certificate).await?;
let mut certificate = Vec::new();
certificate_file.read_to_end(&mut certificate).await?;
let mut certificate_chain_file = tokio::fs::File::open(&certificate_chain).await?;
let mut certificate_chain = Vec::new();
certificate_chain_file
.read_to_end(&mut certificate_chain)
.await?;

// PEM format
let private_key = pem::parse(&private_key)?;
let certificates = pem::parse_many(&certificate_chain)?
.into_iter()
.map(|entry| Certificate::unchecked_from_der(entry.contents))
.collect::<Vec<_>>();
server
.install_certificate(
&Certificate::from_der(certificate)?,
&PrivateKey::from_der(private_key)?,
&CertificateChain::unchecked_from_certificates(certificates),
&PrivateKey::unchecked_from_der(private_key.contents),
)
.await?;
}
Expand Down
11 changes: 7 additions & 4 deletions crates/bonsaidb-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub enum Error {
/// An error occurred with a certificate.
#[error("a certificate error: {0}")]
Certificate(#[from] fabruic::error::Certificate),

/// An error occurred parsing a PEM file.
#[error("an invalid PEM file: {0}")]
#[cfg(feature = "pem")]
Pem(#[from] pem::PemError),

/// An error occurred with handling opaque-ke.
#[error("an opaque-ke error: {0}")]
Password(#[from] core::custodian_password::Error),
Expand All @@ -52,12 +58,9 @@ impl From<Error> for core::Error {
Error::Core(core) => core,
Error::Io(io) => Self::Io(io.to_string()),
Error::Transport(networking) => Self::Transport(networking),
Error::InternalCommunication(err) => Self::Server(err.to_string()),
Error::Certificate(err) => Self::Server(err.to_string()),
#[cfg(feature = "websockets")]
Error::Websocket(err) => Self::Websocket(err.to_string()),
Error::Request(err) => Self::Server(err.to_string()),
Error::Password(err) => Self::Server(err.to_string()),
err => Self::Server(err.to_string()),
}
}
}
Expand Down
40 changes: 22 additions & 18 deletions crates/bonsaidb-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use bonsaidb_local::{
jobs::{manager::Manager, Job},
OpenDatabase, Storage,
};
use fabruic::{self, Certificate, CertificateChain, Endpoint, KeyPair, PrivateKey};
use fabruic::{self, CertificateChain, Endpoint, KeyPair, PrivateKey};
use flume::Sender;
#[cfg(feature = "websockets")]
use futures::SinkExt;
Expand Down Expand Up @@ -178,25 +178,25 @@ impl<B: Backend> CustomServer<B> {
) -> Result<(), Error> {
let keypair = KeyPair::new_self_signed(server_name);

if self.certificate_path().exists() && !overwrite {
if self.certificate_chain_path().exists() && !overwrite {
return Err(Error::Core(bonsaidb_core::Error::Configuration(String::from("Certificate already installed. Enable overwrite if you wish to replace the existing certificate."))));
}

self.install_certificate(keypair.end_entity_certificate(), keypair.private_key())
self.install_certificate(keypair.certificate_chain(), keypair.private_key())
.await?;

Ok(())
}

/// Installs an X.509 certificate used for general purpose connections.
/// These currently must be in DER binary format, not ASCII PEM format.
/// Installs a certificate chain and private key used for TLS connections.
pub async fn install_certificate(
&self,
certificate: &Certificate,
certificate: &CertificateChain,
private_key: &PrivateKey,
) -> Result<(), Error> {
File::create(self.certificate_path())
.and_then(|file| file.write_all(certificate.as_ref()))
let serialized = pot::to_vec(certificate).unwrap();
File::create(self.certificate_chain_path())
.and_then(|file| file.write_all(&serialized))
.await
.map_err(|err| {
Error::Core(bonsaidb_core::Error::Configuration(format!(
Expand All @@ -217,22 +217,28 @@ impl<B: Backend> CustomServer<B> {
Ok(())
}

fn certificate_path(&self) -> PathBuf {
self.data.directory.join("public-certificate.der")
fn certificate_chain_path(&self) -> PathBuf {
self.data.directory.join("public-certificate.pot")
}

/// Returns the current certificate.
pub async fn certificate(&self) -> Result<Certificate, Error> {
Ok(File::open(self.certificate_path())
/// Returns the current certificate chain.
pub async fn certificate_chain(&self) -> Result<CertificateChain, Error> {
let certificate = File::open(self.certificate_chain_path())
.and_then(FileExt::read_all)
.await
.map(Certificate::unchecked_from_der)
.map_err(|err| {
Error::Core(bonsaidb_core::Error::Configuration(format!(
"Error reading certificate file: {}",
err
)))
})?)
})?;
let chain = pot::from_slice(&certificate).map_err(|err| {
Error::Core(bonsaidb_core::Error::Configuration(format!(
"Invalid certificate file contents: {}",
err
)))
})?;
Ok(chain)
}

fn private_key_path(&self) -> PathBuf {
Expand All @@ -242,7 +248,6 @@ impl<B: Backend> CustomServer<B> {
/// Listens for incoming client connections. Does not return until the
/// server shuts down.
pub async fn listen_on(&self, port: u16) -> Result<(), Error> {
let certificate = self.certificate().await?;
let private_key = File::open(self.private_key_path())
.and_then(FileExt::read_all)
.await
Expand All @@ -253,8 +258,7 @@ impl<B: Backend> CustomServer<B> {
err
)))
})??;
let certchain = CertificateChain::from_certificates(vec![certificate])?;
let keypair = KeyPair::from_parts(certchain, private_key)?;
let keypair = KeyPair::from_parts(self.certificate_chain().await?, private_key)?;

let mut server = Endpoint::new_server(port, keypair)?;
{
Expand Down
7 changes: 5 additions & 2 deletions crates/bonsaidb/examples/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ async fn main() -> anyhow::Result<()> {
},
)
.await?;
if server.certificate().await.is_err() {
if server.certificate_chain().await.is_err() {
server
.install_self_signed_certificate("example-server", true)
.await?;
}
let certificate = server.certificate().await?;
let certificate = server
.certificate_chain()
.await?
.into_end_entity_certificate();
server.register_schema::<Shape>().await?;
server.create_database::<Shape>("my-database", true).await?;
// ANCHOR_END: setup
Expand Down
9 changes: 7 additions & 2 deletions crates/bonsaidb/examples/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ async fn main() -> anyhow::Result<()> {
tokio::time::sleep(Duration::from_millis(10)).await;

let client = Client::build(Url::parse("bonsaidb://localhost")?)
.with_certificate(server.certificate().await?)
.with_certificate(
server
.certificate_chain()
.await?
.into_end_entity_certificate(),
)
.finish()
.await?;
let db = client.database::<Shape>("my-database").await?;
Expand Down Expand Up @@ -130,7 +135,7 @@ async fn setup_server() -> anyhow::Result<Server> {
},
)
.await?;
if server.certificate().await.is_err() {
if server.certificate_chain().await.is_err() {
server
.install_self_signed_certificate("example-server", true)
.await?;
Expand Down
13 changes: 11 additions & 2 deletions crates/bonsaidb/tests/core-suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ fn run_shared_server(certificate_sender: flume::Sender<Certificate>) -> anyhow::
let directory = TestDirectory::new("shared-server");
let server = initialize_basic_server(directory.as_ref()).await.unwrap();
certificate_sender
.send(server.certificate().await.unwrap())
.send(
server
.certificate_chain()
.await
.unwrap()
.into_end_entity_certificate(),
)
.unwrap();

#[cfg(feature = "websockets")]
Expand Down Expand Up @@ -271,7 +277,10 @@ async fn authenticated_permissions_test() -> anyhow::Result<()> {
server
.install_self_signed_certificate("authenticated-permissions-test", false)
.await?;
let certificate = server.certificate().await?;
let certificate = server
.certificate_chain()
.await?
.into_end_entity_certificate();

server.create_user("ecton").await?;
server.set_user_password_str("ecton", "hunter2").await?;
Expand Down
5 changes: 4 additions & 1 deletion crates/bonsaidb/tests/custom-api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ async fn custom_api() -> anyhow::Result<()> {
server
.install_self_signed_certificate("test", false)
.await?;
let certificate = server.certificate().await?;
let certificate = server
.certificate_chain()
.await?
.into_end_entity_certificate();
server.register_schema::<Basic>().await?;
tokio::spawn(async move { server.listen_on(12346).await });

Expand Down
5 changes: 4 additions & 1 deletion crates/bonsaidb/tests/simultaneous-connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ async fn simultaneous_connections() -> anyhow::Result<()> {
server
.install_self_signed_certificate("test", false)
.await?;
let certificate = server.certificate().await?;
let certificate = server
.certificate_chain()
.await?
.into_end_entity_certificate();
server.register_schema::<BasicSchema>().await?;
tokio::spawn(async move { server.listen_on(12345).await });

Expand Down

0 comments on commit 61c42ef

Please sign in to comment.