v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,392 @@
|
||||
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;
|
||||
#[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,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_certificate: None,
|
||||
certificates: Vec::new(),
|
||||
policy: ProtocolPolicy::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
fn read_pem(path: &Path, private: bool) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.custom_flags(libc::O_NOFOLLOW);
|
||||
}
|
||||
let file = options
|
||||
.open(path)
|
||||
.with_context(|| format!("cannot open certificate input {}", path.display()))?;
|
||||
let meta = file.metadata()?;
|
||||
ensure!(
|
||||
meta.is_file() && meta.len() <= MAX_PEM,
|
||||
"certificate inputs must be regular files up to 128 KiB"
|
||||
);
|
||||
#[cfg(unix)]
|
||||
if private {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
ensure!(
|
||||
meta.permissions().mode() & 0o077 == 0,
|
||||
"private key permissions must be 600: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = private;
|
||||
let mut data = Zeroizing::new(Vec::new());
|
||||
file.take(MAX_PEM + 1).read_to_end(&mut data)?;
|
||||
ensure!(
|
||||
data.len() <= MAX_PEM as usize,
|
||||
"PEM file grew beyond its limit"
|
||||
);
|
||||
Ok(data)
|
||||
}
|
||||
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))
|
||||
}
|
||||
Reference in new issue
Block a user