Files
RustProxy/src/tls/types.rs
T
2026-10-20 22:09:50 +02:00

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))
}