This commit is contained in:
Mateusz Gruszczyński committed 2026-10-20 22:09:50 +02:00
1 parent 1fd81eaf5f
commit ee174bc283
36 files changed
+908 -183

No files matched your search

+5
View File
@@ -285,6 +285,11 @@ pub fn for_all_frontends(
if let Some(id) = &vhost.certificate_id {
let hosts = vhost.routing_hosts()?;
assign(&hosts, id, "vhost")?;
if vhost.allow_certificate_mismatch {
for host in hosts {
result.forced_mismatches.insert((id.clone(), host));
}
}
}
}
for redirect in redirects {
+9 -4
View File
@@ -372,10 +372,15 @@ pub fn compile(settings: &Settings, directory: Option<&Path>) -> Result<Option<A
.strip_prefix("*.")
.map(|base| format!("rustproxy-wildcard-check.{base}"))
.unwrap_or_else(|| host.clone());
let mut checker = ResolvesServerCertUsingSni::new();
checker
.add(&validation_name, key.clone())
.with_context(|| format!("certificate does not cover {host}"))?;
if !settings.forced_mismatches.contains(&(cert.id.clone(), host.clone())) {
let mut checker = ResolvesServerCertUsingSni::new();
checker
.add(&validation_name, key.clone())
.with_context(|| format!("certificate does not cover {host}; explicit Vhost consent is required to override"))?;
} else {
tracing::warn!(certificate = %cert.id, hostname = %host,
"Explicitly serving a TLS certificate without hostname verification; clients may reject it");
}
names.insert(host, Arc::new(key.clone()));
}
if settings.default_certificate.as_deref() == Some(cert.id.as_str()) {
+42
View File
@@ -431,4 +431,46 @@ mod tests {
);
assert!(certificate_covered_hosts(dir.path(), "../legacy.pem").is_err());
}
#[test]
fn mismatched_named_certificate_requires_explicit_per_vhost_consent() {
let dir = tempfile::tempdir().unwrap();
initialize_default(dir.path()).unwrap();
generate(dir.path(), "named", vec!["right.example.test".into()]).unwrap();
let settings = Settings {
certificates: vec![Certificate {
id: "named".into(),
issuer: None,
covered_hosts: vec!["right.example.test".into()],
hosts: vec![],
cert_file: "named.pem".into(),
key_file: "named.key".into(),
}],
..Default::default()
};
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
let vhost = &mut config.vhosts[0];
vhost.certificate_id = Some("named".into());
vhost.frontend.hosts = vec!["other.example.test".into()];
let blocked = for_vhosts(&settings, &config.vhosts).unwrap();
assert!(compile(&blocked, Some(dir.path())).is_err());
assert!(blocked.forced_mismatches.is_empty());
vhost.allow_certificate_mismatch = true;
let forced = for_vhosts(&settings, &config.vhosts).unwrap();
assert!(forced.forced_mismatches.contains(&("named".into(), "other.example.test".into())));
assert!(compile(&forced, Some(dir.path())).is_ok());
let saved = serde_json::to_value(&forced).unwrap();
assert!(saved.get("forced_mismatches").is_none());
let restored: Settings = serde_json::from_value(saved).unwrap();
assert!(restored.forced_mismatches.is_empty());
assert!(compile(&restored, Some(dir.path())).is_err());
let mut other = config.vhosts[0].clone();
other.id = "another".into();
other.frontend.hosts = vec!["third.example.test".into()];
other.allow_certificate_mismatch = false;
config.vhosts.push(other);
let partial = for_vhosts(&settings, &config.vhosts).unwrap();
assert!(compile(&partial, Some(dir.path())).is_err());
}
}
+4
View File
@@ -25,6 +25,9 @@ pub struct Settings {
#[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 {
@@ -32,6 +35,7 @@ impl Default for Settings {
default_certificate: None,
certificates: Vec::new(),
policy: ProtocolPolicy::default(),
forced_mismatches: HashSet::new(),
}
}
}