0.34.9
This commit is contained in:
1 parent
70388578cb
commit
2b7c67d755
22 files changed
+369
-59
No files matched your search
@@ -24,6 +24,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/CACHE.md", "guides"),
|
||||
reader_doc!("docs/RESPONSE_DELIVERY.md", "guides"),
|
||||
reader_doc!("docs/TRANSPORT.md", "guides"),
|
||||
reader_doc!("docs/TLS_POST_QUANTUM.md", "guides"),
|
||||
reader_doc!("docs/STREAMS.md", "guides"),
|
||||
reader_doc!("docs/ACME_PROVIDERS.md", "guides"),
|
||||
reader_doc!("docs/DOCKER_DISCOVERY.md", "guides"),
|
||||
|
||||
@@ -6,13 +6,22 @@ mod tests {
|
||||
let value: Value = serde_json::from_slice(reader_bundle()).unwrap();
|
||||
let documents = value["documents"].as_array().unwrap();
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "README.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/TLS_POST_QUANTUM.md"));
|
||||
assert!(documents.iter().all(|doc| {
|
||||
doc["path"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["category"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["content"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["category"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& doc["content"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+35
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.34.8",
|
||||
"version": "0.34.9",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -622,6 +622,11 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"post_quantum": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Experimental hybrid X25519MLKEM768 TLS 1.3 key exchange. Can only be enabled for a Vhost when enabled in the global TLS policy. HTTP/3 Vhosts must match the global policy."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -772,7 +777,35 @@
|
||||
},
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "object",
|
||||
"description": "Global TLS policy inherited by frontends, including experimental post-quantum key exchange.",
|
||||
"properties": {
|
||||
"tls12": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"tls13": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"cipher_suites": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "TLS cipher suites, independent of key exchange groups."
|
||||
},
|
||||
"post_quantum": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Experimental hybrid X25519MLKEM768 TLS 1.3 key exchange. Requires tls13=true, uses aws-lc-rs for frontend TLS; classic groups remain as fallback."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"cache": {
|
||||
|
||||
+3
-2
@@ -223,8 +223,9 @@ fn status_payload(state: &App) -> Value {
|
||||
});
|
||||
payload["tls"] = json!({
|
||||
"policy": &snapshot.envelope.config.tls.policy,
|
||||
"cipher_suites": crate::tls::cipher_suite_names(),
|
||||
"effective_cipher_suites": snapshot.envelope.config.tls.policy.effective_cipher_suites()
|
||||
"cipher_suites": crate::tls::cipher_suite_names_for(snapshot.envelope.config.tls.policy.post_quantum),
|
||||
"effective_cipher_suites": snapshot.envelope.config.tls.policy.effective_cipher_suites(),
|
||||
"key_exchange_groups": snapshot.envelope.config.tls.policy.key_exchange_groups()
|
||||
});
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -3071,6 +3071,19 @@
|
||||
<label><span data-i18n="tlsCiphers">Cipher suites (one per line; empty = rustls
|
||||
defaults)</span><textarea id="tls-policy-ciphers" rows="8"
|
||||
spellcheck="false"></textarea></label>
|
||||
<div class="tls-pqc-control">
|
||||
<label class="check"><input id="tls-policy-pqc" type="checkbox" />
|
||||
<span data-i18n="tlsPqcLabel">Experimental post-quantum key exchange
|
||||
(PQC)</span>
|
||||
</label>
|
||||
<p class="fine" data-i18n="tlsPqcHelp">TLS 1.3 only: prioritize hybrid
|
||||
X25519MLKEM768 with classic key exchanges as fallback. This is a key exchange
|
||||
group, not a cipher suite. Disabled by default.</p>
|
||||
<a href="/docs#doc=docs%2FTLS_POST_QUANTUM.md" target="_blank"
|
||||
rel="noopener noreferrer" data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
and testing (documentation)</a>
|
||||
</div>
|
||||
<p class="fine" id="tls-policy-key-exchange"></p>
|
||||
<p class="fine tls-policy-effective" id="tls-policy-effective"></p>
|
||||
<p class="fine" id="tls-policy-available"></p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||
|
||||
@@ -244,9 +244,11 @@
|
||||
]) { const node = $(id); if (node) node.textContent = value || t("unavailable"); }
|
||||
|
||||
const policy = envelope.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||||
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); }
|
||||
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); $("tls-policy-pqc").checked = policy.post_quantum === true; }
|
||||
const available = latestStatus?.tls?.cipher_suites || [];
|
||||
const effective = latestStatus?.tls?.effective_cipher_suites || [];
|
||||
const kxGroups = latestStatus?.tls?.key_exchange_groups || [];
|
||||
$("tls-policy-key-exchange").textContent = kxGroups.length ? `${t("tlsPqcKeyExchangeGroups")}: ${kxGroups.join(", ")}` : "";
|
||||
$("tls-policy-effective").textContent = effective.length ? `${t("tlsEffectiveCiphers")}: ${effective.join(", ")}` : "";
|
||||
$("tls-policy-available").textContent = available.length ? `${t("tlsAvailableCiphers")}: ${available.join(", ")}` : "";
|
||||
window.rustProxyCertificateApi?.render(); window.rustProxyCertificateApi?.load();
|
||||
@@ -287,7 +289,8 @@
|
||||
}, "administration");
|
||||
bindSettingsForm("tls-policy-form", next => {
|
||||
if (!$("tls-policy-12").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-12"), t("tlsPolicyVersionRequired"));
|
||||
next.tls ||= {}; next.tls.policy = { tls12: $("tls-policy-12").checked, tls13: $("tls-policy-13").checked, cipher_suites: $("tls-policy-ciphers").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean) };
|
||||
if ($("tls-policy-pqc").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-13"), t("tlsPqcTls13Required"));
|
||||
next.tls ||= {}; next.tls.policy = { tls12: $("tls-policy-12").checked, tls13: $("tls-policy-13").checked, cipher_suites: $("tls-policy-ciphers").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), post_quantum: $("tls-policy-pqc").checked };
|
||||
}, "tls");
|
||||
$("admin-tls").addEventListener("change", () => { $("admin-certificate").disabled = !$("admin-tls").checked; });
|
||||
$("prometheus-form").addEventListener("input", example);
|
||||
|
||||
@@ -328,8 +328,9 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
const tls12 = field(g, "tls-policy-12", "tls12", tlsPolicy.tls12 !== false, "checkbox");
|
||||
const tls13 = field(g, "tls-policy-13", "tls13", tlsPolicy.tls13 !== false, "checkbox");
|
||||
const tlsCiphers = field(g, "tls-policy-ciphers", "tlsCiphers", (tlsPolicy.cipher_suites || []).join("\n"), "textarea", { rows: 7, maxLength: 8192 });
|
||||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers]) input.disabled = !customTlsPolicy.checked; };
|
||||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp");
|
||||
const tlsPqc = field(g, "tls-policy-pqc", "tlsPqcLabel", tlsPolicy.post_quantum === true, "checkbox");
|
||||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers, tlsPqc]) input.disabled = !customTlsPolicy.checked; };
|
||||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp"); note(g, "tlsPqcVhostHelp");
|
||||
g = group("vhTinyauth"); const auth = value.tinyauth || {};
|
||||
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["basic", "authBasic"]] });
|
||||
note(g, "authModeHelp");
|
||||
@@ -485,19 +486,21 @@ function collect() {
|
||||
if (!tlsPorts.has(443) && tlsPorts.size !== 1) throw new Error(t("vhForceSslPortConflict"));
|
||||
}
|
||||
if (yes("tls-policy-custom") && !yes("tls-policy-12") && !yes("tls-policy-13")) throw new Error(t("tlsPolicyVersionRequired"));
|
||||
if (yes("tls-policy-custom") && yes("tls-policy-pqc") && !yes("tls-policy-13")) throw new Error(t("tlsPqcTls13Required"));
|
||||
if (yes("tls-policy-custom") && yes("tls-policy-pqc") && envelope?.config.tls?.policy?.post_quantum !== true) throw new Error(t("tlsPqcGlobalRequired"));
|
||||
const selectedUsesHttp3 = bindingSelection.some(id => {
|
||||
const endpoint = envelope?.config.listeners?.endpoints?.find(item => item.id === id);
|
||||
return !!endpoint?.binding?.http3;
|
||||
});
|
||||
if (yes("http3-enabled") && selectedUsesHttp3 && yes("tls-policy-custom")) {
|
||||
const selected = { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")).sort() };
|
||||
const selected = { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")).sort(), post_quantum: yes("tls-policy-pqc") };
|
||||
const inherited = envelope?.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||||
const globalPolicy = { tls12: inherited.tls12 !== false, tls13: inherited.tls13 !== false, cipher_suites: [...(inherited.cipher_suites || [])].sort() };
|
||||
const globalPolicy = { tls12: inherited.tls12 !== false, tls13: inherited.tls13 !== false, cipher_suites: [...(inherited.cipher_suites || [])].sort(), post_quantum: inherited.post_quantum === true };
|
||||
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
|
||||
}
|
||||
if (yes("content-type-block-enabled") && !lines(str("content-type-block-list")).length) throw new Error(t("vhContentTypeRequired"));
|
||||
return {
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), allow_certificate_mismatch: yes("allow-certificate-mismatch"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: null,
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), allow_certificate_mismatch: yes("allow-certificate-mismatch"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")), post_quantum: yes("tls-policy-pqc") } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: null,
|
||||
acl_profile_ids: window.rustProxyAclPicker.read(form.querySelector(".vh-acl-profile-choices")),
|
||||
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
|
||||
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader(), user_agent_limits: uaRatesReader() },
|
||||
|
||||
@@ -749,10 +749,27 @@ mod tests {
|
||||
tls12: true,
|
||||
tls13: false,
|
||||
cipher_suites: vec![],
|
||||
post_quantum: false,
|
||||
});
|
||||
assert!(compile(c, 1, &policy()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pqc_vhost_override_requires_global_opt_in_and_can_opt_out() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].tls_policy = Some(crate::tls::ProtocolPolicy {
|
||||
post_quantum: true,
|
||||
..crate::tls::ProtocolPolicy::default()
|
||||
});
|
||||
assert!(compile(config.clone(), 1, &policy()).is_err());
|
||||
config.tls.policy.post_quantum = true;
|
||||
assert!(compile(config.clone(), 1, &policy()).is_ok());
|
||||
config.vhosts[0].tls_policy = Some(crate::tls::ProtocolPolicy::default());
|
||||
assert!(compile(config.clone(), 1, &policy()).is_ok());
|
||||
config.tls.policy.post_quantum = false;
|
||||
assert!(compile(config, 1, &policy()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_remote_administration_requires_tls() {
|
||||
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
|
||||
+1
-10
@@ -34,16 +34,7 @@ pub fn crypto(
|
||||
policy.allows_http3(),
|
||||
"HTTP/3 requires TLS 1.3 and a TLS 1.3 cipher suite"
|
||||
);
|
||||
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)
|
||||
});
|
||||
}
|
||||
let provider = crate::tls::provider_for(policy)?;
|
||||
let mut quic = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])?
|
||||
.with_no_client_auth()
|
||||
|
||||
+64
-1
@@ -186,6 +186,7 @@ mod tests {
|
||||
tls12: true,
|
||||
tls13: false,
|
||||
cipher_suites: Vec::new(),
|
||||
post_quantum: false,
|
||||
},
|
||||
..Settings::default()
|
||||
};
|
||||
@@ -199,6 +200,7 @@ mod tests {
|
||||
tls12: false,
|
||||
tls13: false,
|
||||
cipher_suites: vec![],
|
||||
post_quantum: false,
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
@@ -214,6 +216,7 @@ mod tests {
|
||||
tls12: true,
|
||||
tls13: false,
|
||||
cipher_suites: vec![tls13.clone()],
|
||||
post_quantum: false,
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
@@ -223,6 +226,7 @@ mod tests {
|
||||
tls12: false,
|
||||
tls13: true,
|
||||
cipher_suites: vec![tls12.clone()],
|
||||
post_quantum: false,
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
@@ -231,11 +235,13 @@ mod tests {
|
||||
tls12: true,
|
||||
tls13: false,
|
||||
cipher_suites: vec![],
|
||||
post_quantum: false,
|
||||
};
|
||||
assert!(ProtocolPolicy {
|
||||
tls12: true,
|
||||
tls13: false,
|
||||
cipher_suites: vec![]
|
||||
cipher_suites: vec![],
|
||||
post_quantum: false,
|
||||
}
|
||||
.validate_narrower_than(&global)
|
||||
.is_ok());
|
||||
@@ -243,6 +249,63 @@ mod tests {
|
||||
.validate_narrower_than(&global)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn pqc_is_opt_in_and_not_a_cipher_suite() {
|
||||
let legacy: ProtocolPolicy = serde_json::from_value(serde_json::json!({
|
||||
"tls12": true, "tls13": true, "cipher_suites": []
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(!legacy.post_quantum);
|
||||
assert!(serde_json::to_value(&legacy)
|
||||
.unwrap()
|
||||
.get("post_quantum")
|
||||
.is_none());
|
||||
let mut pqc = legacy.clone();
|
||||
pqc.post_quantum = true;
|
||||
assert!(pqc.validate().is_ok());
|
||||
let groups = pqc.key_exchange_groups();
|
||||
assert_eq!(groups.first().map(String::as_str), Some("X25519MLKEM768"));
|
||||
assert!(groups.iter().any(|name| name == "X25519"));
|
||||
assert!(!legacy
|
||||
.key_exchange_groups()
|
||||
.contains(&"X25519MLKEM768".into()));
|
||||
assert!(!pqc
|
||||
.effective_cipher_suites()
|
||||
.iter()
|
||||
.any(|suite| suite.contains("MLKEM")));
|
||||
assert!(provider_for(&pqc).unwrap().kx_groups.len() > 1);
|
||||
assert!(serde_json::to_value(&pqc).unwrap()["post_quantum"] == true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pqc_policy_requires_tls13_and_global_permission() {
|
||||
let legacy = ProtocolPolicy::default();
|
||||
let mut pqc = legacy.clone();
|
||||
pqc.post_quantum = true;
|
||||
assert!(pqc.validate_narrower_than(&legacy).is_err());
|
||||
assert!(legacy.validate_narrower_than(&pqc).is_ok());
|
||||
pqc.tls13 = false;
|
||||
assert!(pqc.validate().is_err());
|
||||
pqc.tls13 = true;
|
||||
pqc.cipher_suites = vec!["X25519MLKEM768".into()];
|
||||
assert!(pqc.validate().is_err()); // KEX name is not a cipher suite
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pqc_builds_both_tcp_tls_and_quic_configs() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
initialize_default(dir.path()).unwrap();
|
||||
let mut settings = Settings::default();
|
||||
settings.policy.post_quantum = true;
|
||||
let tls = compile(&settings, Some(dir.path())).unwrap().unwrap();
|
||||
assert!(tls.alpn_protocols.contains(&b"h2".to_vec()));
|
||||
let h3 = crate::http3::crypto(&tls, &settings.policy);
|
||||
assert!(h3.is_ok(), "hybrid HTTP/3 TLS config must build");
|
||||
let mut tls12_only = settings.policy.clone();
|
||||
tls12_only.tls13 = false;
|
||||
assert!(crate::http3::crypto(&tls, &tls12_only).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn administration_tls_supports_default_and_selected_certificate() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
+54
-5
@@ -52,6 +52,12 @@ pub struct ProtocolPolicy {
|
||||
pub tls12: bool,
|
||||
pub tls13: bool,
|
||||
pub cipher_suites: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub post_quantum: bool,
|
||||
}
|
||||
|
||||
fn is_false(enabled: &bool) -> bool {
|
||||
!*enabled
|
||||
}
|
||||
impl Default for ProtocolPolicy {
|
||||
fn default() -> Self {
|
||||
@@ -59,6 +65,7 @@ impl Default for ProtocolPolicy {
|
||||
tls12: true,
|
||||
tls13: true,
|
||||
cipher_suites: Vec::new(),
|
||||
post_quantum: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,11 +75,15 @@ impl ProtocolPolicy {
|
||||
self.tls12 || self.tls13,
|
||||
"at least one TLS protocol version must be enabled"
|
||||
);
|
||||
ensure!(
|
||||
!self.post_quantum || self.tls13,
|
||||
"post-quantum key exchange requires TLS 1.3"
|
||||
);
|
||||
ensure!(
|
||||
self.cipher_suites.len() <= 16,
|
||||
"at most 16 TLS cipher suites may be selected"
|
||||
);
|
||||
let available = cipher_suite_names();
|
||||
let available = cipher_suite_names_for(self.post_quantum);
|
||||
let mut seen = HashSet::new();
|
||||
for suite in &self.cipher_suites {
|
||||
ensure!(
|
||||
@@ -97,7 +108,7 @@ impl ProtocolPolicy {
|
||||
}
|
||||
pub fn effective_cipher_suites(&self) -> Vec<String> {
|
||||
let selected = if self.cipher_suites.is_empty() {
|
||||
cipher_suite_names()
|
||||
cipher_suite_names_for(self.post_quantum)
|
||||
} else {
|
||||
self.cipher_suites.clone()
|
||||
};
|
||||
@@ -120,6 +131,10 @@ impl ProtocolPolicy {
|
||||
pub fn validate_narrower_than(&self, global: &Self) -> Result<()> {
|
||||
self.validate()?;
|
||||
global.validate()?;
|
||||
ensure!(
|
||||
!self.post_quantum || global.post_quantum,
|
||||
"per-vhost post-quantum key exchange must be enabled globally"
|
||||
);
|
||||
ensure!(
|
||||
!self.tls12 || global.tls12,
|
||||
"per-vhost TLS policy cannot enable TLS 1.2 when it is disabled globally"
|
||||
@@ -144,17 +159,51 @@ impl ProtocolPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
fn base_provider(post_quantum: bool) -> rustls::crypto::CryptoProvider {
|
||||
if !post_quantum {
|
||||
return rustls::crypto::ring::default_provider();
|
||||
}
|
||||
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||
let hybrid = rustls::crypto::aws_lc_rs::kx_group::X25519MLKEM768;
|
||||
provider
|
||||
.kx_groups
|
||||
.retain(|group| group.name() != hybrid.name());
|
||||
provider.kx_groups.insert(0, hybrid);
|
||||
provider
|
||||
}
|
||||
|
||||
pub fn cipher_suite_names() -> Vec<String> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
base_provider(false)
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.map(|suite| format!("{:?}", suite.suite()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn provider_for(policy: &ProtocolPolicy) -> Result<rustls::crypto::CryptoProvider> {
|
||||
pub fn cipher_suite_names_for(post_quantum: bool) -> Vec<String> {
|
||||
if !post_quantum {
|
||||
return cipher_suite_names();
|
||||
}
|
||||
base_provider(post_quantum)
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.map(|suite| format!("{:?}", suite.suite()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProtocolPolicy {
|
||||
pub fn key_exchange_groups(&self) -> Vec<String> {
|
||||
base_provider(self.post_quantum)
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|group| format!("{:?}", group.name()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn provider_for(policy: &ProtocolPolicy) -> Result<rustls::crypto::CryptoProvider> {
|
||||
policy.validate()?;
|
||||
let mut provider = rustls::crypto::ring::default_provider();
|
||||
let mut provider = base_provider(policy.post_quantum);
|
||||
if !policy.cipher_suites.is_empty() {
|
||||
provider.cipher_suites.retain(|suite| {
|
||||
let name = format!("{:?}", suite.suite());
|
||||
|
||||
Reference in new issue
Block a user