use std::error::Error;
use std::io;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use dynamic_config::Builder;
use dynamic_config_server::{router, serve_tls, Server, ServerConfig};
use rcgen::{
BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
KeyUsagePurpose,
};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{ClientConfig, RootCertStore};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
const TOKEN: &str = "example-token-0123456789abcdefg0";
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let directory = tempfile::tempdir()?;
let files = Files::new(directory.path())?;
println!(
"certificates and configuration in {}\n",
files.root.display()
);
println!(
"--- server.toml ---\n{}",
std::fs::read_to_string(&files.server_toml)?
);
let config: ServerConfig = Builder::new("server")
.file(files.server_toml.to_str().unwrap())
.load()?;
let server = Arc::new(Server::start(&config)?);
let listener = tokio::net::TcpListener::bind(server.address()).await?;
let address = listener.local_addr()?;
let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
println!(
"listening on {address} (protection: {})\n",
server.posture()
);
let held = Arc::clone(&server);
let serving = tokio::spawn(async move {
serve_tls(listener, router(Arc::clone(&held)), &held, async {
let _ = stopped.await;
})
.await
});
let served = request(address, &files, true, Some(TOKEN)).await?;
println!("with a certificate and a token:\n {served}\n");
let refused = request(address, &files, true, None).await?;
println!("with a certificate and no token:\n {refused}\n");
match request(address, &files, false, Some(TOKEN)).await {
Ok(response) => println!("without a certificate: {response} — expected a refusal!"),
Err(error) => println!("without a certificate:\n refused at the handshake: {error}"),
}
let _ = stop.send(());
let _ = serving.await?;
Ok(())
}
async fn request(
address: SocketAddr,
files: &Files,
certificate: bool,
token: Option<&str>,
) -> Result<String, Box<dyn Error>> {
let mut roots = RootCertStore::empty();
roots.add(CertificateDer::from_pem_file(&files.ca)?)?;
let builder =
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(roots);
let config = if certificate {
builder.with_client_auth_cert(
CertificateDer::pem_file_iter(&files.client_certificate)?
.collect::<Result<Vec<_>, _>>()?,
PrivateKeyDer::from_pem_file(&files.client_key)?,
)?
} else {
builder.with_no_client_auth()
};
let stream = TcpStream::connect(address).await?;
let name = ServerName::from(address.ip());
let mut stream = TlsConnector::from(Arc::new(config))
.connect(name, stream)
.await?;
let authorization = token.map_or_else(String::new, |token| {
format!("Authorization: Bearer {token}\r\n")
});
stream
.write_all(
format!(
"GET /billing/prod HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\
{authorization}\r\n"
)
.as_bytes(),
)
.await?;
let mut response = String::new();
stream
.read_to_string(&mut response)
.await
.map_err(|error| io::Error::new(error.kind(), format!("the connection ended: {error}")))?;
Ok(response.lines().next().unwrap_or_default().to_owned())
}
struct Files {
root: PathBuf,
ca: PathBuf,
client_certificate: PathBuf,
client_key: PathBuf,
server_toml: PathBuf,
}
impl Files {
fn new(root: &Path) -> Result<Self, Box<dyn Error>> {
let mut authority = CertificateParams::new(Vec::new())?;
authority.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
authority
.distinguished_name
.push(DnType::CommonName, "dynamic-config example CA");
authority.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let authority_key = KeyPair::generate()?;
let ca = authority.self_signed(&authority_key)?;
let issuer = Issuer::new(authority, authority_key);
let server = leaf(
vec!["127.0.0.1".to_owned(), "localhost".to_owned()],
"config-server",
ExtendedKeyUsagePurpose::ServerAuth,
&issuer,
)?;
let client = leaf(
vec!["billing-pod".to_owned()],
"billing-pod",
ExtendedKeyUsagePurpose::ClientAuth,
&issuer,
)?;
let files = Self {
root: root.to_owned(),
ca: root.join("ca.pem"),
client_certificate: root.join("client.pem"),
client_key: root.join("client.key"),
server_toml: root.join("server.toml"),
};
write(&files.ca, &ca.pem(), 0o644)?;
write(&root.join("server.pem"), &server.0, 0o644)?;
write(&root.join("server.key"), &server.1, 0o600)?;
write(&files.client_certificate, &client.0, 0o644)?;
write(&files.client_key, &client.1, 0o600)?;
write(
&root.join("billing.toml"),
"[billing]\nhost = 'db.internal'\npassword = 'hunter2'\n",
0o600,
)?;
write(
&files.server_toml,
&format!(
r#"[server]
bind = "127.0.0.1:0"
[server.tls]
certificate = "{root}/server.pem"
key = "{root}/server.key"
# Present, so every caller must present a certificate chaining to it.
# Remove this one line and the server keeps TLS and stops asking.
client_ca = "{root}/ca.pem"
[[server.sections]]
application = "billing"
profile = "prod"
files = ["{root}/billing.toml"]
[[server.clients]]
name = "billing-pod"
token = "{TOKEN}"
applications = ["billing"]
"#,
root = root.display().to_string().replace('\\', "/")
),
0o600,
)?;
Ok(files)
}
}
fn leaf(
names: Vec<String>,
common_name: &str,
usage: ExtendedKeyUsagePurpose,
issuer: &Issuer<'_, KeyPair>,
) -> Result<(String, String), Box<dyn Error>> {
let mut params = CertificateParams::new(names)?;
params
.distinguished_name
.push(DnType::CommonName, common_name);
params.extended_key_usages = vec![usage];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
let key = KeyPair::generate()?;
let certificate = params.signed_by(&key, issuer)?;
Ok((certificate.pem(), key.serialize_pem()))
}
fn write(path: &Path, contents: &str, mode: u32) -> io::Result<()> {
std::fs::write(path, contents)?;
chmod(path, mode)
}
#[cfg(unix)]
fn chmod(path: &Path, mode: u32) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
#[cfg(not(unix))]
fn chmod(_path: &Path, _mode: u32) -> io::Result<()> {
Ok(())
}