use std::net::IpAddr;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use axum_server::tls_rustls::RustlsConfig;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
#[cfg(has_bundled_cert)]
const BUNDLED_CERT: &[u8] = include_bytes!("../assets/aws.qaidvoid.dev/cert.pem");
#[cfg(has_bundled_cert)]
const BUNDLED_KEY: &[u8] = include_bytes!("../assets/aws.qaidvoid.dev/key.pem");
#[cfg(has_bundled_cert)]
pub const BUNDLED_DOMAIN: &str = "aws.qaidvoid.dev";
pub struct TlsAssets {
pub cert_path: PathBuf,
pub config: RustlsConfig,
pub public_trust: bool,
pub domain: Option<String>,
pub generated: bool,
}
impl TlsAssets {
pub fn admin_info(&self, https_port: u16) -> TlsAdminInfo {
TlsAdminInfo {
https_port,
cert_path: self.cert_path.clone(),
public_trust: self.public_trust,
domain: self.domain.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct TlsAdminInfo {
pub https_port: u16,
pub cert_path: PathBuf,
pub public_trust: bool,
pub domain: Option<String>,
}
pub enum CertSource<'a> {
Byo { cert: &'a Path, key: &'a Path },
#[cfg(has_bundled_cert)]
Bundled { dir: PathBuf },
#[cfg_attr(has_bundled_cert, allow(dead_code))]
Managed { dir: PathBuf },
}
pub async fn load_or_generate(source: CertSource<'_>) -> Result<TlsAssets> {
let _ = rustls::crypto::ring::default_provider().install_default();
let resolved = resolve(source).await?;
let config = RustlsConfig::from_pem_file(&resolved.cert_path, &resolved.key_path)
.await
.with_context(|| {
format!(
"loading TLS cert/key from {} + {}",
resolved.cert_path.display(),
resolved.key_path.display()
)
})?;
let cert_path = std::path::absolute(&resolved.cert_path).unwrap_or(resolved.cert_path);
Ok(TlsAssets {
cert_path,
config,
public_trust: resolved.public_trust,
domain: resolved.domain,
generated: resolved.generated,
})
}
struct ResolvedSource {
cert_path: PathBuf,
key_path: PathBuf,
public_trust: bool,
domain: Option<String>,
generated: bool,
}
async fn resolve(source: CertSource<'_>) -> Result<ResolvedSource> {
match source {
CertSource::Byo { cert, key } => Ok(ResolvedSource {
cert_path: cert.to_path_buf(),
key_path: key.to_path_buf(),
public_trust: false,
domain: None,
generated: false,
}),
#[cfg(has_bundled_cert)]
CertSource::Bundled { dir } => {
let (cert_path, key_path, generated) = materialise_bundled_cert(&dir).await?;
Ok(ResolvedSource {
cert_path,
key_path,
public_trust: true,
domain: Some(BUNDLED_DOMAIN.to_string()),
generated,
})
}
CertSource::Managed { dir } => {
let (cert_path, key_path, generated) = ensure_managed_cert(&dir).await?;
Ok(ResolvedSource {
cert_path,
key_path,
public_trust: false,
domain: None,
generated,
})
}
}
}
#[cfg(has_bundled_cert)]
async fn materialise_bundled_cert(dir: &Path) -> Result<(PathBuf, PathBuf, bool)> {
tokio::fs::create_dir_all(dir)
.await
.with_context(|| format!("creating TLS bundle dir {}", dir.display()))?;
let cert_path = dir.join("awsim-bundled-cert.pem");
let key_path = dir.join("awsim-bundled-key.pem");
let prior = tokio::fs::try_exists(&cert_path).await.unwrap_or(false)
&& tokio::fs::try_exists(&key_path).await.unwrap_or(false);
write_secret(&key_path, BUNDLED_KEY).await?;
tokio::fs::write(&cert_path, BUNDLED_CERT)
.await
.with_context(|| format!("writing bundled TLS cert to {}", cert_path.display()))?;
Ok((cert_path, key_path, !prior))
}
async fn ensure_managed_cert(dir: &Path) -> Result<(PathBuf, PathBuf, bool)> {
tokio::fs::create_dir_all(dir)
.await
.with_context(|| format!("creating TLS cache dir {}", dir.display()))?;
let cert_path = dir.join("awsim-cert.pem");
let key_path = dir.join("awsim-key.pem");
if cert_path.exists() && key_path.exists() {
return Ok((cert_path, key_path, false));
}
let (cert_pem, key_pem) = generate_self_signed()?;
write_secret(&key_path, key_pem.as_bytes()).await?;
tokio::fs::write(&cert_path, cert_pem.as_bytes())
.await
.with_context(|| format!("writing TLS cert to {}", cert_path.display()))?;
Ok((cert_path, key_path, true))
}
fn generate_self_signed() -> Result<(String, String)> {
let sans = vec![
SanType::DnsName("localhost".try_into()?),
SanType::DnsName("*.localhost".try_into()?),
SanType::IpAddress(IpAddr::from([127, 0, 0, 1])),
SanType::IpAddress(IpAddr::from([0, 0, 0, 0])),
SanType::IpAddress(IpAddr::from([0u16; 8])),
SanType::IpAddress(IpAddr::from([0, 0, 0, 0, 0, 0, 0, 1])),
];
let mut params = CertificateParams::default();
params.subject_alt_names = sans;
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "AWSim Local CA");
dn.push(DnType::OrganizationName, "AWSim");
params.distinguished_name = dn;
let key_pair = KeyPair::generate().context("generating TLS key pair")?;
let cert = params
.self_signed(&key_pair)
.context("signing self-signed TLS cert")?;
Ok((cert.pem(), key_pair.serialize_pem()))
}
#[cfg(unix)]
async fn write_secret(path: &Path, data: &[u8]) -> Result<()> {
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.await
.with_context(|| format!("creating TLS key at {}", path.display()))?;
file.write_all(data)
.await
.with_context(|| format!("writing TLS key to {}", path.display()))?;
file.flush().await.ok();
Ok(())
}
#[cfg(not(unix))]
async fn write_secret(path: &Path, data: &[u8]) -> Result<()> {
tokio::fs::write(path, data)
.await
.with_context(|| format!("writing TLS key to {}", path.display()))?;
Ok(())
}
pub fn default_cache_dir() -> PathBuf {
if let Ok(home) = std::env::var("HOME")
&& !home.is_empty()
{
let xdg = std::env::var("XDG_CACHE_HOME")
.ok()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(&home).join(".cache"));
return xdg.join("awsim").join("tls");
}
std::env::temp_dir().join("awsim-tls")
}