use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use openssl::asn1::Asn1Time;
use openssl::bn::{BigNum, MsbOption};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use openssl::x509::extension::{BasicConstraints, KeyUsage, SubjectAlternativeName};
use openssl::x509::{X509, X509NameBuilder};
use crate::config::TlsSettings;
const VALID_DAYS: u32 = 365;
pub fn acceptor(tls: &TlsSettings) -> Result<SslAcceptorBuilder> {
for (what, path) in [("certificate", &tls.cert), ("key", &tls.key)] {
if !path.exists() {
bail!(
"TLS {what} {} does not exist.\n\
Run `sightingdb --install-selfsigned-keys` to generate a self-signed pair for \
testing, point ssl_cert/ssl_key at your own, or set ssl = false in [daemon] to \
serve plain HTTP.",
path.display()
);
}
}
let mut builder =
SslAcceptor::mozilla_intermediate(SslMethod::tls()).context("creating the TLS acceptor")?;
builder
.set_private_key_file(&tls.key, SslFiletype::PEM)
.with_context(|| format!("reading TLS key {}", tls.key.display()))?;
builder
.set_certificate_chain_file(&tls.cert)
.with_context(|| format!("reading TLS certificate {}", tls.cert.display()))?;
Ok(builder)
}
pub fn install_self_signed(tls: &TlsSettings) -> Result<()> {
for path in [&tls.cert, &tls.key] {
if path.exists() {
bail!(
"{} already exists. Remove it first if you really mean to replace it.",
path.display()
);
}
}
let (certificate, key) = generate()?;
write_private(&tls.key, &key)?;
if let Some(parent) = tls.cert.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
fs::write(&tls.cert, &certificate)
.with_context(|| format!("writing {}", tls.cert.display()))?;
log::info!("Wrote a self-signed certificate to {}", tls.cert.display());
log::info!("Wrote its private key to {}", tls.key.display());
log::warn!(
"This certificate is self-signed and valid for {VALID_DAYS} days. Clients will need to \
skip verification (curl -k). Use a real certificate for anything that matters."
);
Ok(())
}
fn generate() -> Result<(Vec<u8>, Vec<u8>)> {
let rsa = Rsa::generate(2048).context("generating an RSA key")?;
let key = PKey::from_rsa(rsa).context("wrapping the RSA key")?;
let mut name = X509NameBuilder::new()?;
name.append_entry_by_text("CN", "localhost")?;
name.append_entry_by_text("O", "SightingDB self-signed")?;
let name = name.build();
let mut builder = X509::builder()?;
builder.set_version(2)?; builder.set_subject_name(&name)?;
builder.set_issuer_name(&name)?;
builder.set_pubkey(&key)?;
let mut serial = BigNum::new()?;
serial.rand(159, MsbOption::MAYBE_ZERO, false)?;
let serial = serial.to_asn1_integer()?;
builder.set_serial_number(&serial)?;
let not_before = Asn1Time::days_from_now(0)?;
let not_after = Asn1Time::days_from_now(VALID_DAYS)?;
builder.set_not_before(¬_before)?;
builder.set_not_after(¬_after)?;
builder.append_extension(BasicConstraints::new().critical().build()?)?;
builder.append_extension(
KeyUsage::new()
.critical()
.digital_signature()
.key_encipherment()
.build()?,
)?;
let san = SubjectAlternativeName::new()
.dns("localhost")
.ip("127.0.0.1")
.ip("::1")
.build(&builder.x509v3_context(None, None))?;
builder.append_extension(san)?;
builder.sign(&key, MessageDigest::sha256())?;
let certificate = builder.build();
Ok((
certificate.to_pem().context("encoding the certificate")?,
key.private_key_to_pem_pkcs8()
.context("encoding the private key")?,
))
}
fn write_private(path: &Path, contents: &[u8]) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.with_context(|| format!("creating {}", path.display()))?;
file.write_all(contents)
.with_context(|| format!("writing {}", path.display()))?;
}
#[cfg(not(unix))]
fs::write(path, contents).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
struct TempDir(PathBuf);
impl TempDir {
fn new(tag: &str) -> Self {
let path = std::env::temp_dir().join(format!("sightingdb-tls-{tag}"));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).unwrap();
TempDir(path)
}
fn settings(&self) -> TlsSettings {
TlsSettings {
cert: self.0.join("ssl/cert.pem"),
key: self.0.join("ssl/key.pem"),
}
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn a_generated_pair_is_usable_by_the_acceptor() {
let dir = TempDir::new("generate");
let tls = dir.settings();
install_self_signed(&tls).unwrap();
assert!(tls.cert.exists() && tls.key.exists());
acceptor(&tls).unwrap();
}
#[test]
fn the_certificate_names_localhost() {
let dir = TempDir::new("san");
let tls = dir.settings();
install_self_signed(&tls).unwrap();
let pem = fs::read(&tls.cert).unwrap();
let parsed = X509::from_pem(&pem).unwrap();
let cn = parsed
.subject_name()
.entries_by_nid(openssl::nid::Nid::COMMONNAME)
.next()
.unwrap()
.data()
.to_string()
.unwrap();
assert_eq!(cn, "localhost");
assert!(parsed.subject_alt_names().is_some());
}
#[test]
fn an_existing_file_is_never_replaced() {
let dir = TempDir::new("noclobber");
let tls = dir.settings();
install_self_signed(&tls).unwrap();
let original = fs::read(&tls.cert).unwrap();
let err = install_self_signed(&tls).unwrap_err().to_string();
assert!(err.contains("already exists"), "{err}");
assert_eq!(fs::read(&tls.cert).unwrap(), original);
}
#[cfg(unix)]
#[test]
fn the_private_key_is_not_world_readable() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new("perms");
let tls = dir.settings();
install_self_signed(&tls).unwrap();
let mode = fs::metadata(&tls.key).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "key mode was {mode:o}");
}
#[test]
fn a_missing_certificate_explains_itself() {
let dir = TempDir::new("missing");
let err = match acceptor(&dir.settings()) {
Ok(_) => panic!("a missing certificate should not have produced an acceptor"),
Err(e) => e.to_string(),
};
assert!(err.contains("does not exist"), "{err}");
assert!(err.contains("--install-selfsigned-keys"), "{err}");
assert!(err.contains("ssl = false"), "{err}");
}
}