From f02ef365a1c6f128535d1810739ba945c4d722e8 Mon Sep 17 00:00:00 2001 From: SimonThormeyer Date: Thu, 2 Apr 2026 15:46:33 +0200 Subject: [PATCH 1/4] test(e2ei): add CRL distribution point to stepca intermediate We need to set the host port of CRL server at runtime, because the CRL is fetched via the host port, so we need to alter the config after the host port has been determined. --- e2e-identity/tests/utils/stepca.rs | 61 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/e2e-identity/tests/utils/stepca.rs b/e2e-identity/tests/utils/stepca.rs index 9f9ae4fc66..5fbe088e03 100644 --- a/e2e-identity/tests/utils/stepca.rs +++ b/e2e-identity/tests/utils/stepca.rs @@ -104,25 +104,35 @@ fn generate_authority_config(cfg: &CaCfg) -> serde_json::Value { }) } -const INTERMEDIATE_CERT_TEMPLATE: &str = r#" - { - "subject": "Wire Intermediate CA", - "keyUsage": ["certSign", "crlSign"], - "basicConstraints": { - "isCA": true, - "maxPathLen": 0 - }, - "nameConstraints": { - "critical": true, - "permittedDNSDomains": ["localhost", "stepca"], - "permittedURIDomains": ["wire.localhost"] - } - } -"#; - -pub(crate) const ACME_PROVISIONER: &str = "wire"; +const ACME_PROVISIONER: &str = "wire"; const PORT: ContainerPort = ContainerPort::Tcp(9000); +// We need to set the host port of CRL server at runtime, because the CRL +// is fetched via the host port, so we need to alter the config after the +// host port has been determined. +const CRL_DISTRIBUTION_POINT_PLACEHOLDER: &str = "__CRL_DISTRIBUTION_POINT__"; + +fn intermediate_cert_template() -> String { + format!( + r#" + {{ + "subject": "Wire Intermediate CA", + "keyUsage": ["certSign", "crlSign"], + "basicConstraints": {{ + "isCA": true, + "maxPathLen": 0 + }}, + "nameConstraints": {{ + "critical": true, + "permittedDNSDomains": ["localhost", "stepca"], + "permittedURIDomains": ["wire.localhost"] + }}, + "crlDistributionPoints": ["{CRL_DISTRIBUTION_POINT_PLACEHOLDER}"] + }} + "# + ) +} + /// This returns the Smallstep certificate template for leaf certificates, i.e. the ones /// issued by the intermediate CA. fn leaf_cert_template(org: &str) -> String { @@ -147,6 +157,9 @@ fn alter_configuration(host_volume: &Path, ca_cfg: &CaCfg) { cfg.as_object_mut() .unwrap() .insert("authority".to_string(), generate_authority_config(ca_cfg)); + cfg.as_object_mut() + .unwrap() + .insert("crl".to_string(), json!({ "enabled": true })); std::fs::write(&cfg_file, serde_json::to_string_pretty(&cfg).unwrap()).unwrap(); } @@ -180,7 +193,7 @@ pub(crate) async fn start_acme_server(ca_cfg: &CaCfg) -> AcmeServer { std::fs::set_permissions(&host_volume, permissions).unwrap(); std::fs::write( host_volume.join("intermediate.template"), - INTERMEDIATE_CERT_TEMPLATE.to_string().into_bytes(), + intermediate_cert_template().into_bytes(), ) .unwrap(); } @@ -201,6 +214,18 @@ pub(crate) async fn start_acme_server(ca_cfg: &CaCfg) -> AcmeServer { .with_cmd(["bash", "-c", "sleep 1h"]); let node = image.start().await.expect("Error running Step CA image"); + let crl_distribution_point = format!( + "https://{}:{}/1.0/crl", + ca_cfg.host, + node.get_host_port_ipv4(PORT).await.unwrap() + ); + std::fs::write( + host_volume.join("intermediate.template"), + intermediate_cert_template() + .replace(CRL_DISTRIBUTION_POINT_PLACEHOLDER, &crl_distribution_point) + .into_bytes(), + ) + .unwrap(); // Generate the root certificate. run_command(&node, "bash -c 'dd if=/dev/random bs=1 count=20 | base64 > password'").await; From dd56389064d66c0655b1a77df9f10af9aa53681e Mon Sep 17 00:00:00 2001 From: SimonThormeyer Date: Thu, 2 Apr 2026 14:57:57 +0200 Subject: [PATCH 2/4] test(e2ei/pki): add e2e test to fetch CRLs --- e2e-identity/tests/e2e.rs | 59 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/e2e-identity/tests/e2e.rs b/e2e-identity/tests/e2e.rs index d6a322ea9f..0267c74456 100644 --- a/e2e-identity/tests/e2e.rs +++ b/e2e-identity/tests/e2e.rs @@ -23,7 +23,11 @@ #![cfg(not(target_os = "unknown"))] -use std::{collections::HashMap, net::SocketAddr, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + net::SocketAddr, + sync::Arc, +}; use core_crypto_keystore::{ConnectionType, Database, DatabaseKey}; use jwt_simple::prelude::*; @@ -37,7 +41,11 @@ use utils::{ rand_client_id, rand_str, stepca::CaCfg, }; -use wire_e2e_identity::{X509CredentialAcquisition, acquisition::X509CredentialConfiguration, pki_env::PkiEnvironment}; +use wire_e2e_identity::{ + X509CredentialAcquisition, acquisition::X509CredentialConfiguration, pki_env::PkiEnvironment, + x509_check::extract_crl_uris, +}; +use x509_cert::{crl::CertificateList, der::Decode as _}; #[path = "utils/mod.rs"] mod utils; @@ -246,6 +254,53 @@ async fn x509_cert_acquisition_works(test_env: TestEnvironment, #[case] sign_alg .unwrap(); } +#[tokio::test] +#[rstest] +#[case(JwsAlgorithm::P256)] +#[case(JwsAlgorithm::P384)] +#[case(JwsAlgorithm::P521)] +#[case(JwsAlgorithm::Ed25519)] +async fn fetching_crls_works(test_env: TestEnvironment, #[case] sign_alg: JwsAlgorithm) { + let (pki_env, config) = prepare_pki_env_and_config(&test_env, sign_alg).await; + let acq = X509CredentialAcquisition::try_new(Arc::new(pki_env.clone()), config).unwrap(); + let (_sign_kp, certs) = acq + .complete_dpop_challenge() + .await + .unwrap() + .complete_oidc_challenge() + .await + .unwrap(); + + let crl_uris: HashSet = certs + .iter() + .map(|cert| x509_cert::Certificate::from_der(cert).expect("certificate in chain parses")) + .filter_map(|cert| extract_crl_uris(&cert).expect("CRL distribution points can be extracted")) + .flatten() + .collect(); + + assert!( + !crl_uris.is_empty(), + "issued certificate chain should advertise at least one CRL" + ); + + let result = pki_env + .fetch_crls(crl_uris.iter().map(String::as_str)) + .await + .expect("fetched CRL URLs"); + + assert_eq!(result.len(), crl_uris.len(), "each advertised CRL should be fetched"); + assert_eq!( + result.keys().cloned().collect::>(), + crl_uris, + "fetched CRLs should match the advertised distribution points", + ); + + for crl_der in result.values() { + assert!(!crl_der.is_empty(), "fetched CRL should not be empty"); + let _ = CertificateList::from_der(crl_der).expect("fetched body is a valid DER CRL"); + } +} + // @SF.PROVISIONING @TSFI.ACME // TODO: ignore this test for now, until the relevant PKI environment checks are in place #[ignore] From b49f4492febf862660c8d99523521e92951182d0 Mon Sep 17 00:00:00 2001 From: SimonThormeyer Date: Fri, 10 Apr 2026 11:54:10 +0200 Subject: [PATCH 3/4] chore(e2ei/test): use custom reqwest client in `TestPkiEnvironmentHooks` Without it, requests on TLS endpoints would fail --- e2e-identity/tests/utils/hooks.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e-identity/tests/utils/hooks.rs b/e2e-identity/tests/utils/hooks.rs index 97b44aa027..be0ba0a2d0 100644 --- a/e2e-identity/tests/utils/hooks.rs +++ b/e2e-identity/tests/utils/hooks.rs @@ -6,7 +6,8 @@ use wire_e2e_identity::pki_env::hooks::{ }; use crate::utils::{ - OauthCfg, WireServer, default_http_client, + OauthCfg, WireServer, + ctx::ctx_get_http_client_builder, idp::{IdpServer, OidcProvider, fetch_id_token}, stepca::AcmeServer, }; @@ -30,7 +31,7 @@ impl PkiEnvironmentHooks for TestPkiEnvironmentHooks { mut headers: Vec, body: Vec, ) -> Result { - let client = default_http_client() + let client = ctx_get_http_client_builder() .add_root_certificate(self.acme.ca_cert.clone()) .build() .unwrap(); From 8c3f47d5280c5008e0bcbf695e5ef4139d473783 Mon Sep 17 00:00:00 2001 From: SimonThormeyer Date: Mon, 13 Apr 2026 10:36:33 +0200 Subject: [PATCH 4/4] refactor(e2ei/test): simplify intermediate template setup - We don't need `#[cfg(unix)]` because it applies for all our testing environments anyway - We don't need to write the template file twice --- e2e-identity/tests/utils/stepca.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/e2e-identity/tests/utils/stepca.rs b/e2e-identity/tests/utils/stepca.rs index 5fbe088e03..69d2e07818 100644 --- a/e2e-identity/tests/utils/stepca.rs +++ b/e2e-identity/tests/utils/stepca.rs @@ -185,18 +185,10 @@ pub(crate) async fn start_acme_server(ca_cfg: &CaCfg) -> AcmeServer { let host_volume = std::env::temp_dir().join(rand_str(12)); std::fs::create_dir(&host_volume).unwrap(); - #[cfg(unix)] - { - // Allow container user to write to our host volume. - use std::os::unix::fs::PermissionsExt; - let permissions = std::fs::Permissions::from_mode(0o777); - std::fs::set_permissions(&host_volume, permissions).unwrap(); - std::fs::write( - host_volume.join("intermediate.template"), - intermediate_cert_template().into_bytes(), - ) - .unwrap(); - } + // Allow container user to write to our host volume. + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(0o777); + std::fs::set_permissions(&host_volume, permissions).unwrap(); // Prepare the container image. Note that instead of just starting the image as-is, we're // overriding the command to be a long sleep, in order to be able to issue commands inside