Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tls] Allow TLS certificate and key to come from bytes #504

Merged
merged 10 commits into from
Dec 14, 2022
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