diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 5507cea..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,4 +0,0 @@ -[env] -DHTTP_BOOTSTRAP_URL = "https://bootstrap.genmeta.net:20002" -DHTTP_H3_DNS_SERVER = "https://ddns.genmeta.net:4433" -DHTTP_MDNS_SERVICE = "_dhttp.local" diff --git a/.env.example b/.env.example index 79a017c..0e0a2b5 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,22 @@ # Optional shell/CI overrides. Cargo defaults to the same production values. +# Run `set -a; source .env; set +a` after copying this file to .env. DHTTP_BOOTSTRAP_URL=https://bootstrap.genmeta.net:20002 DHTTP_CA_SERVICE=https://api.genmeta.net DHTTP_NAME_SERVICE=https://ddns.genmeta.net DHTTP_MDNS_SERVICE_DOMAIN="_dhttp.local" +# Keep this value in sync with dhttp/root.crt. +DHTTP_ROOT_CA_PEM="-----BEGIN CERTIFICATE----- +MIICVzCCAd2gAwIBAgIUe8kwBACY6f+MAzdCBVPmq4p+CiswCgYIKoZIzj0EAwMw +WTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5t +ZXRhIEVDQyBSb290IENBMRkwFwYDVQQDDBByb290Lmdlbm1ldGEubmV0MB4XDTI2 +MDcxMzEzMDQyOFoXDTQ2MDcxMzEzMDQyOFowWTELMAkGA1UEBhMCQ04xETAPBgNV +BAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5tZXRhIEVDQyBSb290IENBMRkwFwYD +VQQDDBByb290Lmdlbm1ldGEubmV0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO+zm +ZYL0LaqTKf7mW4tnRWeNop1p8f2ZsexhAl23GHkHwLjCihhQzBCZ8VMRPAdVcEIS +XcGY/U6+Z1IAYCRG0tdsUCXHXxzvDY0I9FZqZw1Xo94gkHnNe7mTu/jCQg3Xo2Yw +ZDAdBgNVHQ4EFgQUq1SsSWDnp0G5v5/hWi9CC7eWDTwwHwYDVR0jBBgwFoAUq1Ss +SWDnp0G5v5/hWi9CC7eWDTwwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8E +BAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwK9GqxdmRHJw7iB0z/b/WgzBv2jb7OmFS +uVPA+6ZNApjYXCZUOVQFC60KUUV7yW53AjEA5lLrdXxdGNSIuLe1h/A+v/vRrYtt +132Jzh+LkKBHdC1wcvDKjk2ZQG5WySly6VMp +-----END CERTIFICATE-----" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2bc86e8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/dhttp/root.crt text eol=lf diff --git a/Cargo.toml b/Cargo.toml index a111516..4e70d5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f dhttp-identity = "0.3.0" dhttp-home = { path = "home", version = "0.5.0" } dquic = { version = "0.7.1", default-features = false } -ddns = { package = "dyns", version = "0.7.1", features = [ +ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", rev = "332cf51093572a28364226ef7682e0d95812a6ef", features = [ "resolvers", "publishers", "h3", diff --git a/dhttp/Cargo.toml b/dhttp/Cargo.toml index e2f978f..1d439a8 100644 --- a/dhttp/Cargo.toml +++ b/dhttp/Cargo.toml @@ -39,9 +39,14 @@ ddns = { workspace = true } h3x = { workspace = true } [build-dependencies] +rustls-pemfile = "2" url = "2" +x509-parser.workspace = true [dev-dependencies] dhttp-access = { workspace = true, features = ["http", "orm"] } rcgen = "0.14" +rustls-pemfile = "2" tokio = { workspace = true, features = ["macros", "rt"] } +url = "2" +x509-parser.workspace = true diff --git a/dhttp/bootstrap_config.rs b/dhttp/bootstrap_config.rs new file mode 100644 index 0000000..114e52d --- /dev/null +++ b/dhttp/bootstrap_config.rs @@ -0,0 +1,174 @@ +use std::{error::Error, fmt}; + +use rustls_pemfile::{Item, read_one_from_slice}; + +pub const DEFAULT_BOOTSTRAP_URL: &str = "https://bootstrap.genmeta.net:20002"; +pub const DEFAULT_ROOT_CA_PEM: &str = include_str!("root.crt"); + +pub fn env_or_default(name: &str, default: &str) -> String { + std::env::var(name).unwrap_or_else(|_| default.to_owned()) +} + +pub fn bootstrap_authority(value: &str) -> Result { + let url = url::Url::parse(value).map_err(|error| error.to_string())?; + if url.scheme() != "https" { + return Err("scheme must be https".to_owned()); + } + if url.username() != "" || url.password().is_some() { + return Err("credentials are not allowed".to_owned()); + } + if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { + return Err("path, query, and fragment are not allowed".to_owned()); + } + + let host = url + .host_str() + .ok_or_else(|| "host is required".to_owned())?; + let port = url + .port() + .ok_or_else(|| "an explicit port is required".to_owned())?; + if matches!(url.host(), Some(url::Host::Ipv6(_))) { + Ok(format!("[{host}]:{port}")) + } else { + Ok(format!("{host}:{port}")) + } +} + +#[derive(Debug)] +pub enum RootCaError { + DecodePem(rustls_pemfile::Error), + MissingCertificate, + UnexpectedPemItem, + MultipleCertificates, + InvalidX509(String), + TrailingDerData, +} + +impl fmt::Display for RootCaError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DecodePem(error) => write!(formatter, "failed to decode PEM: {error:?}"), + Self::MissingCertificate => formatter.write_str("missing PEM CERTIFICATE block"), + Self::UnexpectedPemItem => { + formatter.write_str("PEM input contains a non-certificate item") + } + Self::MultipleCertificates => { + formatter.write_str("PEM input contains multiple certificates") + } + Self::InvalidX509(error) => { + write!(formatter, "certificate is not valid X.509 DER: {error}") + } + Self::TrailingDerData => formatter.write_str("certificate contains trailing DER data"), + } + } +} + +impl Error for RootCaError {} + +pub fn parse_root_ca_der(pem: &str) -> Result, RootCaError> { + let mut remainder = pem.as_bytes(); + let mut certificate = None; + + while let Some((item, next)) = read_one_from_slice(remainder).map_err(RootCaError::DecodePem)? { + remainder = next; + let Item::X509Certificate(item) = item else { + return Err(RootCaError::UnexpectedPemItem); + }; + if certificate.replace(item).is_some() { + return Err(RootCaError::MultipleCertificates); + } + } + + let certificate = certificate.ok_or(RootCaError::MissingCertificate)?; + let (remainder, _) = x509_parser::parse_x509_certificate(certificate.as_ref()) + .map_err(|error| RootCaError::InvalidX509(error.to_string()))?; + if !remainder.is_empty() { + return Err(RootCaError::TrailingDerData); + } + + Ok(certificate.as_ref().to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_bootstrap_env_uses_genmeta_production_default() { + let name = format!("__DHTTP_MISSING_BOOTSTRAP_{}", std::process::id()); + + assert_eq!( + env_or_default(&name, DEFAULT_BOOTSTRAP_URL), + "https://bootstrap.genmeta.net:20002" + ); + } + + #[test] + fn bootstrap_url_produces_stun_authority() { + assert_eq!( + bootstrap_authority("https://bootstrap.genmeta.net:20002").as_deref(), + Ok("bootstrap.genmeta.net:20002") + ); + } + + #[test] + fn bootstrap_url_requires_https_and_explicit_port() { + assert!(bootstrap_authority("http://bootstrap.genmeta.net:20002").is_err()); + assert!(bootstrap_authority("https://bootstrap.genmeta.net").is_err()); + } + + #[test] + fn default_root_ca_is_decoded_to_der() { + let der = parse_root_ca_der(DEFAULT_ROOT_CA_PEM).unwrap(); + + assert_eq!(der.first(), Some(&0x30)); + assert!(!der.starts_with(b"-----BEGIN CERTIFICATE-----")); + } + + #[test] + fn escaped_newlines_are_not_accepted_as_pem() { + let escaped = DEFAULT_ROOT_CA_PEM.replace('\n', "\\n"); + + assert!(parse_root_ca_der(&escaped).is_err()); + } + + #[test] + fn crlf_root_ca_is_accepted_by_the_pem_parser() { + let expected = parse_root_ca_der(DEFAULT_ROOT_CA_PEM).unwrap(); + let crlf = DEFAULT_ROOT_CA_PEM + .replace("\r\n", "\n") + .replace('\n', "\r\n"); + + assert_eq!(parse_root_ca_der(&crlf).unwrap(), expected); + } + + #[test] + fn malformed_x509_certificate_is_rejected() { + let pem = "-----BEGIN CERTIFICATE-----\nYm9keQ==\n-----END CERTIFICATE-----\n"; + + assert!(matches!( + parse_root_ca_der(pem), + Err(RootCaError::InvalidX509(_)) + )); + } + + #[test] + fn multiple_certificates_are_rejected() { + let pem = format!("{DEFAULT_ROOT_CA_PEM}{DEFAULT_ROOT_CA_PEM}"); + + assert!(matches!( + parse_root_ca_der(&pem), + Err(RootCaError::MultipleCertificates) + )); + } + + #[test] + fn non_certificate_pem_item_is_rejected() { + let pem = DEFAULT_ROOT_CA_PEM.replace("CERTIFICATE", "PRIVATE KEY"); + + assert!(matches!( + parse_root_ca_der(&pem), + Err(RootCaError::UnexpectedPemItem) + )); + } +} diff --git a/dhttp/build.rs b/dhttp/build.rs index 4fdcd85..36890b5 100644 --- a/dhttp/build.rs +++ b/dhttp/build.rs @@ -1,37 +1,19 @@ +mod bootstrap_config; + use std::{env, fs, path::PathBuf}; -const ROOT_CA_ENV: &str = "DHTTP_ROOT_CA"; -const BOOTSTRAP_URL_ENV: &str = "DHTTP_BOOTSTRAP_URL"; +use bootstrap_config::{ + DEFAULT_BOOTSTRAP_URL, DEFAULT_ROOT_CA_PEM, bootstrap_authority, env_or_default, + parse_root_ca_der, +}; -const DEFAULT_BOOTSTRAP_URL: &str = "https://bootstrap.genmeta.net:20002"; -const DEFAULT_ROOT_CA_PEM: &str = "\ ------BEGIN CERTIFICATE-----\n\ -MIICVzCCAd2gAwIBAgIUe8kwBACY6f+MAzdCBVPmq4p+CiswCgYIKoZIzj0EAwMw\n\ -WTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5t\n\ -ZXRhIEVDQyBSb290IENBMRkwFwYDVQQDDBByb290Lmdlbm1ldGEubmV0MB4XDTI2\n\ -MDcxMzEzMDQyOFoXDTQ2MDcxMzEzMDQyOFowWTELMAkGA1UEBhMCQ04xETAPBgNV\n\ -BAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5tZXRhIEVDQyBSb290IENBMRkwFwYD\n\ -VQQDDBByb290Lmdlbm1ldGEubmV0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO+zm\n\ -ZYL0LaqTKf7mW4tnRWeNop1p8f2ZsexhAl23GHkHwLjCihhQzBCZ8VMRPAdVcEIS\n\ -XcGY/U6+Z1IAYCRG0tdsUCXHXxzvDY0I9FZqZw1Xo94gkHnNe7mTu/jCQg3Xo2Yw\n\ -ZDAdBgNVHQ4EFgQUq1SsSWDnp0G5v5/hWi9CC7eWDTwwHwYDVR0jBBgwFoAUq1Ss\n\ -SWDnp0G5v5/hWi9CC7eWDTwwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8E\n\ -BAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwK9GqxdmRHJw7iB0z/b/WgzBv2jb7OmFS\n\ -uVPA+6ZNApjYXCZUOVQFC60KUUV7yW53AjEA5lLrdXxdGNSIuLe1h/A+v/vRrYtt\n\ -132Jzh+LkKBHdC1wcvDKjk2ZQG5WySly6VMp\n\ ------END CERTIFICATE-----\n"; +const ROOT_CA_PEM_ENV: &str = "DHTTP_ROOT_CA_PEM"; +const BOOTSTRAP_URL_ENV: &str = "DHTTP_BOOTSTRAP_URL"; fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); - let root_ca_dest = out_dir.join("root.crt"); - let root_ca = root_ca_bytes(); - fs::write(&root_ca_dest, &root_ca).unwrap_or_else(|error| { - panic!( - "failed to write generated DHTTP root CA to {}: {error}", - root_ca_dest.display() - ) - }); + let root_ca_der = root_ca_der(); let bootstrap_url = env_or_default(BOOTSTRAP_URL_ENV, DEFAULT_BOOTSTRAP_URL); let bootstrap_authority = bootstrap_authority(&bootstrap_url).unwrap_or_else(|error| { @@ -41,95 +23,23 @@ fn main() { "// @generated by build.rs; do not edit.\n\ pub const DHTTP_BOOTSTRAP_URL: &str = {bootstrap_url:?};\n\ pub const DHTTP_BOOTSTRAP_AUTHORITY: &str = {bootstrap_authority:?};\n\ - pub const DHTTP_ROOT_CA: &[u8] = &{root_ca:?};\n" + pub const DHTTP_ROOT_CA_DER: &[u8] = &{root_ca_der:?};\n" ); fs::write(out_dir.join("bootstrap.rs"), bootstrap) .expect("failed to write generated DHTTP bootstrap constants"); - println!("cargo::rerun-if-env-changed={ROOT_CA_ENV}"); + println!("cargo::rerun-if-env-changed={ROOT_CA_PEM_ENV}"); println!("cargo::rerun-if-env-changed={BOOTSTRAP_URL_ENV}"); - if let Some(root_ca) = optional_env_path(ROOT_CA_ENV) { - println!("cargo::rerun-if-changed={}", root_ca.display()); - } -} - -fn env_or_default(name: &str, default: &str) -> String { - env::var(name).unwrap_or_else(|_| default.to_owned()) -} - -fn bootstrap_authority(value: &str) -> Result { - let url = url::Url::parse(value).map_err(|error| error.to_string())?; - if url.scheme() != "https" { - return Err("scheme must be https".to_owned()); - } - if url.username() != "" || url.password().is_some() { - return Err("credentials are not allowed".to_owned()); - } - if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { - return Err("path, query, and fragment are not allowed".to_owned()); - } - - let host = url - .host_str() - .ok_or_else(|| "host is required".to_owned())?; - let port = url - .port() - .ok_or_else(|| "an explicit port is required".to_owned())?; - if matches!(url.host(), Some(url::Host::Ipv6(_))) { - Ok(format!("[{host}]:{port}")) - } else { - Ok(format!("{host}:{port}")) - } } -fn optional_env_path(name: &str) -> Option { - env::var_os(name).map(PathBuf::from) -} - -fn root_ca_bytes() -> Vec { - let Some(root_ca) = optional_env_path(ROOT_CA_ENV) else { - return DEFAULT_ROOT_CA_PEM.as_bytes().to_vec(); +fn root_ca_der() -> Vec { + let pem = match env::var(ROOT_CA_PEM_ENV) { + Ok(pem) => pem, + Err(env::VarError::NotPresent) => DEFAULT_ROOT_CA_PEM.to_owned(), + Err(env::VarError::NotUnicode(_)) => { + panic!("{ROOT_CA_PEM_ENV} must contain UTF-8 PEM text") + } }; - fs::read(&root_ca).unwrap_or_else(|error| { - panic!( - "failed to read DHTTP root CA from {}: {error}", - root_ca.display() - ) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn missing_bootstrap_env_uses_genmeta_production_default() { - let name = format!("__DHTTP_MISSING_BOOTSTRAP_{}", std::process::id()); - - assert_eq!( - env_or_default(&name, DEFAULT_BOOTSTRAP_URL), - "https://bootstrap.genmeta.net:20002" - ); - } - - #[test] - fn bootstrap_url_produces_stun_authority() { - assert_eq!( - bootstrap_authority("https://bootstrap.genmeta.net:20002").as_deref(), - Ok("bootstrap.genmeta.net:20002") - ); - } - - #[test] - fn bootstrap_url_requires_https_and_explicit_port() { - assert!(bootstrap_authority("http://bootstrap.genmeta.net:20002").is_err()); - assert!(bootstrap_authority("https://bootstrap.genmeta.net").is_err()); - } - - #[test] - fn default_root_ca_is_pem_certificate() { - assert!(DEFAULT_ROOT_CA_PEM.starts_with("-----BEGIN CERTIFICATE-----")); - assert!(DEFAULT_ROOT_CA_PEM.ends_with("-----END CERTIFICATE-----\n")); - } + parse_root_ca_der(&pem).unwrap_or_else(|error| panic!("invalid {ROOT_CA_PEM_ENV}: {error}")) } diff --git a/dhttp/root.crt b/dhttp/root.crt new file mode 100644 index 0000000..10c4ae7 --- /dev/null +++ b/dhttp/root.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICVzCCAd2gAwIBAgIUe8kwBACY6f+MAzdCBVPmq4p+CiswCgYIKoZIzj0EAwMw +WTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5t +ZXRhIEVDQyBSb290IENBMRkwFwYDVQQDDBByb290Lmdlbm1ldGEubmV0MB4XDTI2 +MDcxMzEzMDQyOFoXDTQ2MDcxMzEzMDQyOFowWTELMAkGA1UEBhMCQ04xETAPBgNV +BAgMCEhvbmdLb25nMRwwGgYDVQQKDBNHZW5tZXRhIEVDQyBSb290IENBMRkwFwYD +VQQDDBByb290Lmdlbm1ldGEubmV0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEO+zm +ZYL0LaqTKf7mW4tnRWeNop1p8f2ZsexhAl23GHkHwLjCihhQzBCZ8VMRPAdVcEIS +XcGY/U6+Z1IAYCRG0tdsUCXHXxzvDY0I9FZqZw1Xo94gkHnNe7mTu/jCQg3Xo2Yw +ZDAdBgNVHQ4EFgQUq1SsSWDnp0G5v5/hWi9CC7eWDTwwHwYDVR0jBBgwFoAUq1Ss +SWDnp0G5v5/hWi9CC7eWDTwwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8E +BAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwK9GqxdmRHJw7iB0z/b/WgzBv2jb7OmFS +uVPA+6ZNApjYXCZUOVQFC60KUUV7yW53AjEA5lLrdXxdGNSIuLe1h/A+v/vRrYtt +132Jzh+LkKBHdC1wcvDKjk2ZQG5WySly6VMp +-----END CERTIFICATE----- diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index 67e7169..77ac6f9 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -213,7 +213,7 @@ pub async fn dhttp_network_builder_with_dns( #[builder(start_fn)] builder: F, #[builder(start_fn)] dns_plan: &DhttpDnsPlan, #[builder(default = Arc::new(Vec::new()))] bind: Arc>, - #[builder(default = Arc::::from(resolvers::DHTTP_H3_DNS_SERVER))] h3_dns_server: Arc, + #[builder(default = Arc::::from(resolvers::DHTTP_NAME_SERVICE))] h3_dns_server: Arc, ) -> Result where F: FnOnce(ArcResolver) -> Arc, @@ -221,7 +221,9 @@ where let deferred_stun_resolver = Arc::new(DeferredStunResolver::new()); let stun_resolver: ArcResolver = deferred_stun_resolver.clone(); let network = builder(stun_resolver); - let mdns_driver = Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)); + let mdns_driver = Arc::new(mdns::MdnsBindDriver::new( + resolvers::DHTTP_MDNS_SERVICE_DOMAIN, + )); let final_resolver = network_stun_resolver_from_plan( dns_plan, network.clone(), @@ -244,8 +246,10 @@ where pub async fn quic_endpoint_builder_with_dns( #[builder(start_fn)] builder: F, #[builder(start_fn)] dns_plan: &DhttpDnsPlan, - #[builder(default = Arc::::from(resolvers::DHTTP_H3_DNS_SERVER))] h3_dns_server: Arc, - #[builder(default = Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)))] + #[builder(default = Arc::::from(resolvers::DHTTP_NAME_SERVICE))] h3_dns_server: Arc, + #[builder(default = Arc::new(mdns::MdnsBindDriver::new( + resolvers::DHTTP_MDNS_SERVICE_DOMAIN, + )))] mdns_driver: Arc, ) -> Result<(QuicEndpoint, publishers::Publishers), BuildQuicEndpointWithDnsError> where @@ -1006,7 +1010,9 @@ mod tests { let clients = endpoint_h3_clients_from_quic( &operations, &endpoint, - Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)), + Arc::new(mdns::MdnsBindDriver::new( + resolvers::DHTTP_MDNS_SERVICE_DOMAIN, + )), ) .await .expect("h3 dns clients should build"); @@ -1067,7 +1073,9 @@ mod tests { &operations, network.clone(), bind.clone(), - Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)), + Arc::new(mdns::MdnsBindDriver::new( + resolvers::DHTTP_MDNS_SERVICE_DOMAIN, + )), ) .await .expect("network h3 underlay should build"); diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 9cf711d..f00231e 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -153,7 +153,7 @@ impl Endpoint { identity: Option>, network: Option, - #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_H3_DNS_SERVER))] + #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_NAME_SERVICE))] h3_dns_server: Arc, #[builder(default = crate::trust::default_client_quic_config())] client: ClientQuicConfig, #[builder(default = crate::trust::default_server_quic_config())] server: ServerQuicConfig, diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 851dea9..611620b 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -81,7 +81,7 @@ impl From> for DhttpNetwork { Self { network, mdns_driver: Arc::new(MdnsBindDriver::new( - crate::ddns::resolvers::DHTTP_MDNS_SERVICE, + crate::ddns::resolvers::DHTTP_MDNS_SERVICE_DOMAIN, )), _deferred_stun_resolver: None, _stun_resolver: None, @@ -118,7 +118,7 @@ impl DhttpNetwork { stun_server: Option>>, stun_resolver: Option, #[builder(default = Arc::new(Vec::new()))] bind: Arc>, - #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_H3_DNS_SERVER))] + #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_NAME_SERVICE))] h3_dns_server: Arc, #[builder(default = Devices::global())] devices: &'static Devices, #[builder(default = Arc::new(InterfaceManager::new()))] iface_manager: Arc< @@ -144,7 +144,7 @@ impl DhttpNetwork { return Ok(Self { network, mdns_driver: Arc::new(MdnsBindDriver::new( - crate::ddns::resolvers::DHTTP_MDNS_SERVICE, + crate::ddns::resolvers::DHTTP_MDNS_SERVICE_DOMAIN, )), _deferred_stun_resolver: None, _stun_resolver: Some(stun_resolver), diff --git a/dhttp/src/trust.rs b/dhttp/src/trust.rs index febc2ad..c136770 100644 --- a/dhttp/src/trust.rs +++ b/dhttp/src/trust.rs @@ -1,22 +1,21 @@ use std::sync::{Arc, LazyLock}; use h3x::dquic::{ - cert::handy::ToCertificate, client::{ClientQuicConfig, ServerCertVerifierChoice}, server::ServerQuicConfig, }; use rustls::{ RootCertStore, client::WebPkiServerVerifier, + pki_types::CertificateDer, server::{WebPkiClientVerifier, danger::ClientCertVerifier}, }; -/// PEM-encoded DHTTP ecosystem root CA certificate embedded at build time. +/// DER-encoded DHTTP ecosystem root CA certificate embedded at build time. /// -/// Build scripts set this from `DHTTP_ROOT_CA` when that environment variable -/// is present. Otherwise, docs-only builds use the generated docs-only -/// certificate from `dhttp/build.rs`. -pub const DHTTP_ROOT_CA: &[u8] = crate::bootstrap::DHTTP_ROOT_CA; +/// The build script decodes and validates `DHTTP_ROOT_CA_PEM` when that environment +/// variable is present. Otherwise, it embeds the production default. +pub const DHTTP_ROOT_CA_DER: &[u8] = crate::bootstrap::DHTTP_ROOT_CA_DER; /// Client certificate policy for DHTTP peer authentication. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -35,7 +34,9 @@ pub enum ClientIdentityPolicy { pub fn dhttp_root_cert_store() -> &'static Arc { static STORE: LazyLock> = LazyLock::new(|| { let mut store = RootCertStore::empty(); - store.add_parsable_certificates(DHTTP_ROOT_CA.to_certificate()); + store + .add(CertificateDer::from(DHTTP_ROOT_CA_DER)) + .expect("BUG: build script embedded an invalid DHTTP trust anchor"); Arc::new(store) }); &STORE @@ -97,12 +98,12 @@ mod tests { use super::*; #[test] - fn public_root_ca_constant_is_valid_certificate_material() { + fn public_root_ca_der_is_a_valid_trust_anchor() { let mut store = RootCertStore::empty(); - let (added, ignored) = store.add_parsable_certificates(DHTTP_ROOT_CA.to_certificate()); - assert_eq!(added, 1); - assert_eq!(ignored, 0); + store.add(CertificateDer::from(DHTTP_ROOT_CA_DER)).unwrap(); + + assert_eq!(store.len(), 1); } #[test] diff --git a/dhttp/tests/bootstrap_config.rs b/dhttp/tests/bootstrap_config.rs new file mode 100644 index 0000000..914af4c --- /dev/null +++ b/dhttp/tests/bootstrap_config.rs @@ -0,0 +1,2 @@ +#[path = "../bootstrap_config.rs"] +mod bootstrap_config;