Skip to content

Commit

Permalink
[tls] Allow TLS certificate and key to come from bytes (#504)
Browse files Browse the repository at this point in the history
`ConfigTls` is now an enum, and can either by supplied `AsFile` or `AsBytes`.

Fixes #490
  • Loading branch information
smklein authored Dec 14, 2022
1 parent 94967b8 commit 9e12b70
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ https://github.com/oxidecomputer/dropshot/compare/v0.8.0\...HEAD[Full list of co

=== Breaking Changes

* https://github.com/oxidecomputer/dropshot/pull/504[#504] Dropshot allows TLS configuration to be supplied either by path or as bytes. For compatibility, the `AsFile` variant of `ConfigTls` contains the `cert_file` and `key_file` fields, and may be used similarly to the old variant.
* https://github.com/oxidecomputer/dropshot/pull/502[#502] Dropshot exposes a `refresh_tls` method to update the TLS certificates being used by a running server. If you previously tried to access `DropshotState.tls`, you can access the `DropshotState.using_tls()` method instead.

=== Other notable Changes
Expand Down
18 changes: 16 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,30 @@ include:
|No
|Specifies the maximum number of bytes allowed in a request body. Larger requests will receive a 400 error. Defaults to 1024.

|`tls.type`
|`"AsFile", "AsBytes"`
|No
|Specifies if and how TLS certificate and key information is provided.

|`tls.cert_file`
|`"/path/to/cert.pem"`
|Only if `tls.key_file` is set
|Only if `tls.type = AsFile`
|Specifies the path to a PEM file containing a certificate chain for the server to identify itself with. The first certificate is the end-entity certificate, and the remaining are intermediate certificates on the way to a trusted CA. If specified, the server will only listen for TLS connections.

|`tls.key_file`
|`"/path/to/key.pem"`
|Only if `tls.cert_file` is set
|Only if `tls.type = AsFile`
|Specifies the path to a PEM-encoded PKCS #8 file containing the private key the server will use. If specified, the server will only listen for TLS connections.

|`tls.certs`
|`Vec<u8> of certificate data`
|Only if `tls.type = AsBytes`
|Identical to `tls.cert_file`, but provided as a buffer.

|`tls.key`
|`Vec<u8> of key data`
|Only if `tls.type = AsBytes`
|Identical to `tls.key_file`, but provided as a buffer.
|===

=== Logging
Expand Down
2 changes: 1 addition & 1 deletion dropshot/examples/https.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async fn main() -> Result<(), String> {
* In addition, we'll make this an HTTPS server.
*/
let config_dropshot = ConfigDropshot {
tls: Some(ConfigTls {
tls: Some(ConfigTls::AsFile {
cert_file: cert_file.path().to_path_buf(),
key_file: key_file.path().to_path_buf(),
}),
Expand Down
74 changes: 63 additions & 11 deletions dropshot/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::path::PathBuf;
* request_body_max_bytes = 1024
* ## Optional, to enable TLS
* [http_api_server.tls]
* type = "AsFile"
* cert_file = "/path/to/certs.pem"
* key_file = "/path/to/key.pem"
*
Expand Down Expand Up @@ -61,17 +62,68 @@ pub struct ConfigDropshot {
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ConfigTls {
/** Path to a PEM file containing a certificate chain for the
* server to identify itself with. The first certificate is the
* end-entity certificate, and the remaining are intermediate
* certificates on the way to a trusted CA.
*/
pub cert_file: PathBuf,
/** Path to a PEM-encoded PKCS #8 file containing the private key the
* server will use.
*/
pub key_file: PathBuf,
#[serde(tag = "type")]
pub enum ConfigTls {
AsFile {
/** Path to a PEM file containing a certificate chain for the
* server to identify itself with. The first certificate is the
* end-entity certificate, and the remaining are intermediate
* certificates on the way to a trusted CA.
*/
cert_file: PathBuf,
/** Path to a PEM-encoded PKCS #8 file containing the private key the
* server will use.
*/
key_file: PathBuf,
},
AsBytes {
certs: Vec<u8>,
key: Vec<u8>,
},
}

impl ConfigTls {
pub(crate) fn cert_reader(
&self,
) -> std::io::Result<Box<dyn std::io::BufRead + '_>> {
match self {
ConfigTls::AsFile { cert_file, .. } => {
let certfile = std::fs::File::open(cert_file).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"failed to open {}: {}",
cert_file.display(),
e
),
)
})?;
Ok(Box::new(std::io::BufReader::new(certfile)))
}
ConfigTls::AsBytes { certs, .. } => {
Ok(Box::new(std::io::BufReader::new(certs.as_slice())))
}
}
}

pub(crate) fn key_reader(
&self,
) -> std::io::Result<Box<dyn std::io::BufRead + '_>> {
match self {
ConfigTls::AsFile { key_file, .. } => {
let keyfile = std::fs::File::open(key_file).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to open {}: {}", key_file.display(), e),
)
})?;
Ok(Box::new(std::io::BufReader::new(keyfile)))
}
ConfigTls::AsBytes { key, .. } => {
Ok(Box::new(std::io::BufReader::new(key.as_slice())))
}
}
}
}

impl Default for ConfigDropshot {
Expand Down
37 changes: 15 additions & 22 deletions dropshot/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ use std::convert::TryFrom;
use std::future::Future;
use std::net::SocketAddr;
use std::num::NonZeroU32;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
Expand Down Expand Up @@ -445,8 +444,8 @@ impl TryFrom<&ConfigTls> for rustls::ServerConfig {
type Error = std::io::Error;

fn try_from(config: &ConfigTls) -> std::io::Result<Self> {
let certs = load_certs(&config.cert_file)?;
let private_key = load_private_key(&config.key_file)?;
let certs = load_certs(&config)?;
let private_key = load_private_key(&config)?;
let mut cfg = rustls::ServerConfig::builder()
// TODO: We may want to expose protocol configuration in our
// config
Expand Down Expand Up @@ -914,37 +913,31 @@ impl<C: ServerContext> Service<Request<Body>> for ServerRequestHandler<C> {
}
}

fn error(err: String) -> std::io::Error {
fn io_error(err: String) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, err)
}

// Load public certificate from file.
fn load_certs(filename: &Path) -> std::io::Result<Vec<rustls::Certificate>> {
// Open certificate file.
let certfile = std::fs::File::open(filename).map_err(|e| {
error(format!("failed to open {}: {}", filename.display(), e))
})?;
let mut reader = std::io::BufReader::new(certfile);
// Load public certificate from config.
fn load_certs(config: &ConfigTls) -> std::io::Result<Vec<rustls::Certificate>> {
let mut reader = config.cert_reader()?;

// Load and return certificate.
rustls_pemfile::certs(&mut reader)
.map_err(|err| error(format!("failed to load certificate: {err}")))
.map_err(|err| io_error(format!("failed to load certificate: {err}")))
.map(|mut chain| chain.drain(..).map(rustls::Certificate).collect())
}

// Load private key from file.
fn load_private_key(filename: &Path) -> std::io::Result<rustls::PrivateKey> {
// Open keyfile.
let keyfile = std::fs::File::open(filename).map_err(|e| {
error(format!("failed to open {}: {}", filename.display(), e))
})?;
let mut reader = std::io::BufReader::new(keyfile);
// Load private key from config.
fn load_private_key(config: &ConfigTls) -> std::io::Result<rustls::PrivateKey> {
let mut reader = config.key_reader()?;

// Load and return a single private key.
let keys = rustls_pemfile::pkcs8_private_keys(&mut reader)
.map_err(|err| error(format!("failed to load private key: {err}")))?;
let keys =
rustls_pemfile::pkcs8_private_keys(&mut reader).map_err(|err| {
io_error(format!("failed to load private key: {err}"))
})?;
if keys.len() != 1 {
return Err(error("expected a single private key".into()));
return Err(io_error("expected a single private key".into()));
}
Ok(rustls::PrivateKey(keys[0].clone()))
}
Expand Down
36 changes: 28 additions & 8 deletions dropshot/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,27 +134,47 @@ fn make_temp_file() -> std::io::Result<NamedTempFile> {
tempfile::Builder::new().prefix("dropshot-test-").rand_bytes(5).tempfile()
}

/// Write keys to a temporary file for passing to the server config
pub fn tls_key_to_file(
pub fn tls_key_to_buffer(
certs: &Vec<rustls::Certificate>,
key: &rustls::PrivateKey,
) -> (NamedTempFile, NamedTempFile) {
let mut cert_file = make_temp_file().expect("failed to create cert_file");
) -> (Vec<u8>, Vec<u8>) {
let mut serialized_certs = vec![];
let mut cert_writer = std::io::BufWriter::new(&mut serialized_certs);
for cert in certs {
let encoded_cert = pem::encode(&pem::Pem {
tag: "CERTIFICATE".to_string(),
contents: cert.0.clone(),
});
cert_file
cert_writer
.write(encoded_cert.as_bytes())
.expect("failed to write cert_file");
.expect("failed to serialize cert");
}
drop(cert_writer);

let mut key_file = make_temp_file().expect("failed to create key_file");
let mut serialized_key = vec![];
let mut key_writer = std::io::BufWriter::new(&mut serialized_key);
let encoded_key = pem::encode(&pem::Pem {
tag: "PRIVATE KEY".to_string(),
contents: key.0.clone(),
});
key_file.write(encoded_key.as_bytes()).expect("failed to write key_file");
key_writer.write(encoded_key.as_bytes()).expect("failed to serialize key");
drop(key_writer);

(serialized_certs, serialized_key)
}

/// Write keys to a temporary file for passing to the server config
pub fn tls_key_to_file(
certs: &Vec<rustls::Certificate>,
key: &rustls::PrivateKey,
) -> (NamedTempFile, NamedTempFile) {
let mut cert_file = make_temp_file().expect("failed to create cert_file");
let mut key_file = make_temp_file().expect("failed to create key_file");

let (certs, key) = tls_key_to_buffer(certs, key);

cert_file.write(certs.as_slice()).expect("Failed to write certs");
key_file.write(key.as_slice()).expect("Failed to write key");

(cert_file, key_file)
}
Loading

0 comments on commit 9e12b70

Please sign in to comment.