368 lines
12 KiB
Rust
368 lines
12 KiB
Rust
use crate::config::{normalize_host, normalize_host_pattern};
|
|
use anyhow::{ensure, Context, Result};
|
|
use rustls::{
|
|
server::{ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni},
|
|
sign::CertifiedKey,
|
|
ServerConfig,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
fs::{self, OpenOptions},
|
|
io::{self, Read, Write},
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
use zeroize::Zeroizing;
|
|
|
|
const MAX_PEM: u64 = 131_072;
|
|
const MAX_CERTIFICATE_PATH: usize = 4096;
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
#[serde(default, deny_unknown_fields)]
|
|
pub struct Settings {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub default_certificate: Option<String>,
|
|
#[serde(default)]
|
|
pub certificates: Vec<Certificate>,
|
|
pub policy: ProtocolPolicy,
|
|
/// Derived from explicitly confirmed Vhosts; never accepted from TLS JSON.
|
|
#[serde(skip)]
|
|
pub forced_mismatches: HashSet<(String, String)>,
|
|
}
|
|
impl Default for Settings {
|
|
fn default() -> Self {
|
|
Self {
|
|
default_certificate: None,
|
|
certificates: Vec::new(),
|
|
policy: ProtocolPolicy::default(),
|
|
forced_mismatches: HashSet::new(),
|
|
}
|
|
}
|
|
}
|
|
impl Settings {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.default_certificate.is_none()
|
|
&& self.certificates.is_empty()
|
|
&& self.policy == ProtocolPolicy::default()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default, deny_unknown_fields)]
|
|
pub struct ProtocolPolicy {
|
|
pub tls12: bool,
|
|
pub tls13: bool,
|
|
/// Rustls cipher-suite names. Empty keeps the ring provider defaults.
|
|
pub cipher_suites: Vec<String>,
|
|
}
|
|
impl Default for ProtocolPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
tls12: true,
|
|
tls13: true,
|
|
cipher_suites: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
impl ProtocolPolicy {
|
|
pub fn validate(&self) -> Result<()> {
|
|
ensure!(
|
|
self.tls12 || self.tls13,
|
|
"at least one TLS protocol version must be enabled"
|
|
);
|
|
ensure!(
|
|
self.cipher_suites.len() <= 16,
|
|
"at most 16 TLS cipher suites may be selected"
|
|
);
|
|
let available = cipher_suite_names();
|
|
let mut seen = HashSet::new();
|
|
for suite in &self.cipher_suites {
|
|
ensure!(
|
|
suite.len() <= 96 && available.iter().any(|name| name == suite),
|
|
"unknown TLS cipher suite: {suite}"
|
|
);
|
|
ensure!(seen.insert(suite), "duplicate TLS cipher suite: {suite}");
|
|
}
|
|
let effective = if self.cipher_suites.is_empty() {
|
|
available
|
|
} else {
|
|
self.cipher_suites.clone()
|
|
};
|
|
ensure!(
|
|
effective.iter().any(|suite| {
|
|
(self.tls13 && suite.starts_with("TLS13_"))
|
|
|| (self.tls12 && !suite.starts_with("TLS13_"))
|
|
}),
|
|
"selected TLS cipher suites do not match the enabled protocol versions"
|
|
);
|
|
Ok(())
|
|
}
|
|
pub fn effective_cipher_suites(&self) -> Vec<String> {
|
|
let selected = if self.cipher_suites.is_empty() {
|
|
cipher_suite_names()
|
|
} else {
|
|
self.cipher_suites.clone()
|
|
};
|
|
selected
|
|
.into_iter()
|
|
.filter(|suite| {
|
|
(self.tls13 && suite.starts_with("TLS13_"))
|
|
|| (self.tls12 && !suite.starts_with("TLS13_"))
|
|
})
|
|
.collect()
|
|
}
|
|
pub fn allows_http3(&self) -> bool {
|
|
self.tls13
|
|
&& (self.cipher_suites.is_empty()
|
|
|| self
|
|
.cipher_suites
|
|
.iter()
|
|
.any(|suite| suite.starts_with("TLS13_")))
|
|
}
|
|
/// Per-vhost policy may only remove protocols/suites from the global policy.
|
|
/// This keeps the global setting a real security ceiling rather than a default
|
|
/// that an individual vhost can silently widen.
|
|
pub fn validate_narrower_than(&self, global: &Self) -> Result<()> {
|
|
self.validate()?;
|
|
global.validate()?;
|
|
ensure!(
|
|
!self.tls12 || global.tls12,
|
|
"per-vhost TLS policy cannot enable TLS 1.2 when it is disabled globally"
|
|
);
|
|
ensure!(
|
|
!self.tls13 || global.tls13,
|
|
"per-vhost TLS policy cannot enable TLS 1.3 when it is disabled globally"
|
|
);
|
|
if !global.cipher_suites.is_empty() {
|
|
ensure!(
|
|
!self.cipher_suites.is_empty(),
|
|
"per-vhost TLS policy must select from the globally allowed cipher suites"
|
|
);
|
|
for suite in &self.cipher_suites {
|
|
ensure!(
|
|
global.cipher_suites.iter().any(|allowed| allowed == suite),
|
|
"per-vhost TLS cipher suite is not allowed globally: {suite}"
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn cipher_suite_names() -> Vec<String> {
|
|
rustls::crypto::ring::default_provider()
|
|
.cipher_suites
|
|
.iter()
|
|
.map(|suite| format!("{:?}", suite.suite()))
|
|
.collect()
|
|
}
|
|
|
|
fn provider_for(policy: &ProtocolPolicy) -> Result<rustls::crypto::CryptoProvider> {
|
|
policy.validate()?;
|
|
let mut provider = rustls::crypto::ring::default_provider();
|
|
if !policy.cipher_suites.is_empty() {
|
|
provider.cipher_suites.retain(|suite| {
|
|
let name = format!("{:?}", suite.suite());
|
|
policy
|
|
.cipher_suites
|
|
.iter()
|
|
.any(|selected| selected == &name)
|
|
});
|
|
}
|
|
ensure!(
|
|
!provider.cipher_suites.is_empty(),
|
|
"TLS cipher selection is empty"
|
|
);
|
|
Ok(provider)
|
|
}
|
|
|
|
fn protocol_versions(policy: &ProtocolPolicy) -> Vec<&'static rustls::SupportedProtocolVersion> {
|
|
let mut versions = Vec::new();
|
|
if policy.tls13 {
|
|
versions.push(&rustls::version::TLS13);
|
|
}
|
|
if policy.tls12 {
|
|
versions.push(&rustls::version::TLS12);
|
|
}
|
|
versions
|
|
}
|
|
|
|
fn server_config(
|
|
resolver: Arc<dyn ResolvesServerCert>,
|
|
policy: &ProtocolPolicy,
|
|
http2: bool,
|
|
) -> Result<Arc<ServerConfig>> {
|
|
let provider = provider_for(policy)?;
|
|
let versions = protocol_versions(policy);
|
|
let mut config = ServerConfig::builder_with_provider(Arc::new(provider))
|
|
.with_protocol_versions(&versions)?
|
|
.with_no_client_auth()
|
|
.with_cert_resolver(resolver);
|
|
config.alpn_protocols = if http2 {
|
|
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
|
} else {
|
|
vec![b"http/1.1".to_vec()]
|
|
};
|
|
config.max_early_data_size = 0;
|
|
Ok(Arc::new(config))
|
|
}
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Issuer {
|
|
pub provider: String,
|
|
pub environment: String,
|
|
#[serde(default = "renew_by_default")]
|
|
pub auto_renew: bool,
|
|
}
|
|
fn renew_by_default() -> bool {
|
|
true
|
|
}
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Certificate {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub issuer: Option<Issuer>,
|
|
pub id: String,
|
|
/// DNS SubjectAltName values extracted from the leaf certificate during
|
|
/// upload or ACME import. This is certificate coverage, not active SNI binding.
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub covered_hosts: Vec<String>,
|
|
/// Active/configured SNI bindings. `for_vhosts` may rewrite this list.
|
|
pub hosts: Vec<String>,
|
|
pub cert_file: String,
|
|
pub key_file: String,
|
|
}
|
|
#[derive(Debug)]
|
|
struct Resolver {
|
|
names: HashMap<String, Arc<CertifiedKey>>,
|
|
fallback: Arc<CertifiedKey>,
|
|
}
|
|
impl Resolver {
|
|
fn named(&self, server_name: &str) -> Option<Arc<CertifiedKey>> {
|
|
let host = normalize_host(server_name).ok()?;
|
|
if let Some(key) = self.names.get(&host) {
|
|
return Some(key.clone());
|
|
}
|
|
let labels: Vec<&str> = host.split('.').collect();
|
|
for index in 1..labels.len().saturating_sub(1) {
|
|
let wildcard = format!("*.{}", labels[index..].join("."));
|
|
if let Some(key) = self.names.get(&wildcard) {
|
|
return Some(key.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
impl ResolvesServerCert for Resolver {
|
|
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
|
hello
|
|
.server_name()
|
|
.and_then(|name| self.named(name))
|
|
.or_else(|| Some(self.fallback.clone()))
|
|
}
|
|
}
|
|
pub fn validate_certificate_removals(
|
|
previous: &crate::config::Config,
|
|
next: &crate::config::Config,
|
|
) -> Result<()> {
|
|
let next_ids: HashSet<&str> = next
|
|
.tls
|
|
.certificates
|
|
.iter()
|
|
.map(|certificate| certificate.id.as_str())
|
|
.collect();
|
|
for certificate in &previous.tls.certificates {
|
|
if next_ids.contains(certificate.id.as_str()) {
|
|
continue;
|
|
}
|
|
let id = certificate.id.as_str();
|
|
let mut owners = Vec::new();
|
|
if previous.tls.default_certificate.as_deref() == Some(id) {
|
|
owners.push("default TLS certificate".to_owned());
|
|
}
|
|
if previous.administration.certificate_id.as_deref() == Some(id) {
|
|
owners.push("administration panel".to_owned());
|
|
}
|
|
owners.extend(
|
|
previous
|
|
.vhosts
|
|
.iter()
|
|
.filter(|host| host.certificate_id.as_deref() == Some(id))
|
|
.map(|host| format!("vhost {}", host.id)),
|
|
);
|
|
owners.extend(
|
|
previous
|
|
.redirects
|
|
.iter()
|
|
.filter(|host| host.certificate_id.as_deref() == Some(id))
|
|
.map(|host| format!("redirect {}", host.id)),
|
|
);
|
|
owners.extend(
|
|
previous
|
|
.not_found_hosts
|
|
.iter()
|
|
.filter(|host| host.certificate_id.as_deref() == Some(id))
|
|
.map(|host| format!("404 host {}", host.id)),
|
|
);
|
|
owners.extend(
|
|
previous
|
|
.docker_agents
|
|
.agents
|
|
.iter()
|
|
.filter(|agent| agent.certificate_id.as_deref() == Some(id))
|
|
.map(|agent| format!("Docker agent {}", agent.id)),
|
|
);
|
|
ensure!(owners.is_empty(), "certificate {id} is still assigned to {}; unassign it and save before removing the certificate", owners.join(", "));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn safe_name(name: &str) -> Result<()> {
|
|
ensure!(
|
|
!name.is_empty()
|
|
&& name.len() <= 96
|
|
&& !name.contains("..")
|
|
&& name
|
|
.bytes()
|
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_')),
|
|
"certificate filenames must be simple relative filenames, not paths"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate how a configured certificate pair is addressed. Legacy/simple names
|
|
/// remain private to `<data-dir>/certs`; an explicitly selected external pair uses
|
|
/// two absolute paths. Mixing the two modes is rejected so the source is never
|
|
/// ambiguous.
|
|
fn der_tlv<'a>(input: &'a [u8], offset: &mut usize) -> Result<(u8, &'a [u8])> {
|
|
ensure!(*offset < input.len(), "truncated DER object");
|
|
let tag = input[*offset];
|
|
*offset += 1;
|
|
ensure!(*offset < input.len(), "truncated DER length");
|
|
let first = input[*offset];
|
|
*offset += 1;
|
|
let length = if first & 0x80 == 0 {
|
|
first as usize
|
|
} else {
|
|
let count = (first & 0x7f) as usize;
|
|
ensure!(count > 0 && count <= 4, "unsupported DER length");
|
|
ensure!(*offset + count <= input.len(), "truncated DER length");
|
|
let mut length = 0usize;
|
|
for byte in &input[*offset..*offset + count] {
|
|
length = length
|
|
.checked_mul(256)
|
|
.and_then(|value| value.checked_add(*byte as usize))
|
|
.context("DER length overflow")?;
|
|
}
|
|
*offset += count;
|
|
length
|
|
};
|
|
let end = (*offset)
|
|
.checked_add(length)
|
|
.context("DER length overflow")?;
|
|
ensure!(end <= input.len(), "truncated DER value");
|
|
let value = &input[*offset..end];
|
|
*offset = end;
|
|
Ok((tag, value))
|
|
}
|