Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@ _run_workspace_tests() {

cargo test "${cargo_test_args[@]}"

# Run the network-dependent certificate validation tests (marked #[ignore]) for all DB
# backends. For postgresql/mysql/redis-findex these tests were already included in the
# --ignored run above; for sqlite they need an explicit additional invocation.
if [ "$KMS_TEST_DB" = "sqlite" ]; then
local -a cargo_test_validate_args
cargo_test_validate_args=(--workspace --lib)
if [ ${#FEATURES_FLAG[@]} -gt 0 ]; then
cargo_test_validate_args+=("${FEATURES_FLAG[@]}")
fi
cargo_test_validate_args+=(-- --nocapture --ignored test_validate_with_certificates)
cargo test "${cargo_test_validate_args[@]}"
fi

# For database backends (postgresql, mysql, redis), also run the regular non-ignored tests
# For sqlite, skip this step since all non-ignored tests already ran above
if [ "$KMS_TEST_DB" != "sqlite" ]; then
Expand Down
13 changes: 12 additions & 1 deletion .github/scripts/release/nix_build_update_hash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ build_attr() {
echo "$output"

# Parse hash mismatches
local last_drv="" updated=0
local last_drv="" updated=0 skipped=0
while IFS= read -r line; do
if [[ "$line" =~ hash\ mismatch\ in\ fixed-output\ derivation.*\'(/nix/store/[^\']+)\' ]]; then
local drv_path="${BASH_REMATCH[1]}"
Expand All @@ -118,6 +118,9 @@ build_attr() {
echo " Updating $(basename "$target_file"): $got_hash"
echo "$got_hash" > "$target_file"
updated=$((updated + 1))
else
echo " Skipping (managed by peer OS runner): $last_drv"
skipped=$((skipped + 1))
fi
last_drv=""
fi
Expand All @@ -129,6 +132,14 @@ build_attr() {
continue
fi

# All remaining failures are hash files owned by the peer OS runner (e.g.
# ui.vendor.*.sha256 on macOS, or ui.pnpm.darwin.sha256 on Linux). Those
# will be fixed by the other matrix job; this runner is done.
if [[ "$updated" -eq 0 && "$skipped" -gt 0 ]]; then
echo "==> ${skipped} hash mismatch(es) are managed by the peer OS runner — skipping."
return 0
fi

echo "ERROR: nix-build -A ${attr} failed after ${attempt} attempt(s)." >&2
return 1
done
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
timeout-minutes: 120

steps:
- name: Clean workspace (fix self-hosted runner permission issues)
run: sudo chown -R "$USER:$USER" "$GITHUB_WORKSPACE" || true

- uses: actions/checkout@v6
with:
submodules: recursive
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ jobs:
with:
toolchain: 1.90.0

- name: Regenerate Cargo.lock
run: cargo generate-lockfile
- name: Update Cargo.lock
run: cargo build --workspace

- name: Regenerate CBOM
run: |
Expand Down
5 changes: 4 additions & 1 deletion crate/clients/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ pem = { workspace = true }
serde = { workspace = true }
serde-wasm-bindgen = "0.6.5"
serde_json = { workspace = true }
wasm-bindgen = "0.2.108"
# Pinned to exact version: nix/ui.nix builds wasm-bindgen-cli at the same version.
# If you upgrade this, also update `wasmBindgenCli.version` in nix/ui.nix
# (along with its src sha256 and cargoHash).
wasm-bindgen = "=0.2.108"
x509-cert = { workspace = true, features = ["pem"] }
zeroize = { workspace = true }

Expand Down
2 changes: 1 addition & 1 deletion crate/server/src/core/operations/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub(crate) async fn import(
}) = &request.object
{
if let Ok(cert) = X509::from_der(certificate_value) {
match verify_crls(vec![cert]).await {
match verify_crls(vec![cert], kms.params.proxy_params.as_ref()).await {
Err(KmsError::Certificate(_)) => {
debug!(
"Import: certificate is revoked per CRL check, \
Expand Down
59 changes: 54 additions & 5 deletions crate/server/src/core/operations/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use openssl::{
};

use crate::{
config::ProxyParams,
core::{KMS, retrieve_object_utils::retrieve_object_for_operation},
error::KmsError,
result::KResult,
Expand Down Expand Up @@ -130,7 +131,7 @@ pub(crate) async fn validate_operation(

verify_chain_signature(&certificates)?;
validate_chain_date(&certificates, &request.validity_time)?;
verify_crls(certificates).await?;
verify_crls(certificates, kms.params.proxy_params.as_ref()).await?;

Ok(ValidateResponse {
validity_indicator: ValidityIndicator::Valid,
Expand Down Expand Up @@ -433,7 +434,10 @@ enum UriType {
/// - There is an error in retrieving the CRL from a URL.
/// - There is an error in reading the CRL from a file path.
/// ```
async fn get_crl_bytes(uri_list: Vec<String>) -> KResult<HashMap<String, Vec<u8>>> {
async fn get_crl_bytes(
uri_list: Vec<String>,
proxy_params: Option<&ProxyParams>,
) -> KResult<HashMap<String, Vec<u8>>> {
trace!("get_crl_bytes: entering: uri_list: {uri_list:?}");

let mut result = HashMap::new();
Expand Down Expand Up @@ -470,7 +474,49 @@ async fn get_crl_bytes(uri_list: Vec<String>) -> KResult<HashMap<String, Vec<u8>
continue;
}

let response = reqwest::Client::new().get(&url).send().await?;
let mut client_builder = reqwest::Client::builder();
if let Some(proxy_params) = proxy_params {
let mut proxy = reqwest::Proxy::all(proxy_params.url.clone()).map_err(|e| {
KmsError::Certificate(format!(
"Failed to configure the HTTPS proxy for CRL fetch: {e}"
))
})?;
if let Some(ref username) = proxy_params.basic_auth_username {
proxy = proxy.basic_auth(
username,
proxy_params
.basic_auth_password
.as_deref()
.unwrap_or_default(),
);
} else if let Some(ref custom_auth_header) = proxy_params.custom_auth_header {
proxy = proxy.custom_http_auth(
reqwest::header::HeaderValue::from_str(custom_auth_header).map_err(
|e| {
KmsError::Certificate(format!(
"Failed to set custom HTTP auth header for CRL fetch: {e}"
))
},
)?,
);
}
if !proxy_params.exclusion_list.is_empty() {
proxy = proxy.no_proxy(reqwest::NoProxy::from_string(
&proxy_params.exclusion_list.join(","),
));
}
client_builder = client_builder.proxy(proxy);
}
let response = client_builder
.build()
.map_err(|e| {
KmsError::Certificate(format!(
"Failed to build reqwest client for CRL fetch: {e}"
))
})?
.get(&url)
.send()
.await?;
debug!("after getting CRL: url: {url}");
if response.status().is_success() {
let crl_bytes =
Expand Down Expand Up @@ -545,7 +591,10 @@ async fn get_crl_bytes(uri_list: Vec<String>) -> KResult<HashMap<String, Vec<u8>
/// * If the CRL signature is invalid.
/// * If there is an issue fetching the CRL bytes from the URIs.
/// ```
pub(crate) async fn verify_crls(certificates: Vec<X509>) -> KResult<ValidityIndicator> {
pub(crate) async fn verify_crls(
certificates: Vec<X509>,
proxy_params: Option<&ProxyParams>,
) -> KResult<ValidityIndicator> {
let mut current_crls: HashMap<String, Vec<u8>> = HashMap::new();

for (idx, certificate) in certificates.iter().enumerate() {
Expand Down Expand Up @@ -590,7 +639,7 @@ pub(crate) async fn verify_crls(certificates: Vec<X509>) -> KResult<ValidityIndi
}
}

current_crls = get_crl_bytes(uri_list).await?;
current_crls = get_crl_bytes(uri_list, proxy_params).await?;

// Test if certificate is in current CRLs
//
Expand Down
75 changes: 75 additions & 0 deletions crate/server/src/tests/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,28 @@ pub(crate) fn https_clap_config() -> ClapConfig {
https_clap_config_opts(None)
}

/// Like `https_clap_config`, but additionally captures any `HTTPS_PROXY` / `HTTP_PROXY`
/// environment variable that is set *before* the test helpers clear it, then assigns it
/// to the server's `proxy_params` so that server-side outbound requests (e.g. CRL fetches)
/// are routed through the configured corporate proxy.
pub(crate) fn https_clap_config_with_external_proxy() -> ClapConfig {
// Capture proxy URL before disable_proxies_for_tests() removes it from the environment.
let proxy_url = std::env::var("HTTPS_PROXY")
.ok()
.or_else(|| std::env::var("https_proxy").ok())
.or_else(|| std::env::var("HTTP_PROXY").ok())
.or_else(|| std::env::var("http_proxy").ok());
let mut config = https_clap_config_opts(None);
if let Some(url) = proxy_url {
config.proxy.proxy_url = Some(url);
}
config
}

pub(crate) fn https_clap_config_opts(kms_public_url: Option<String>) -> ClapConfig {
// Ensure local test traffic bypasses any corporate proxy
ensure_no_proxy_for_localhost();
disable_proxies_for_tests();
let sqlite_path = get_tmp_sqlite_path();

// In FIPS mode, disable TLS with P12 certificates since PKCS12KDF is not FIPS-approved
Expand Down Expand Up @@ -82,6 +103,60 @@ pub(crate) fn https_clap_config_opts(kms_public_url: Option<String>) -> ClapConf
}
}

/// Ensure localhost bypasses any corporate proxy for tests.
fn ensure_no_proxy_for_localhost() {
let has_http_proxy = std::env::var_os("HTTP_PROXY").is_some()
|| std::env::var_os("http_proxy").is_some()
|| std::env::var_os("HTTPS_PROXY").is_some()
|| std::env::var_os("https_proxy").is_some();

if !has_http_proxy {
return;
}

let existing = std::env::var("NO_PROXY")
.ok()
.or_else(|| std::env::var("no_proxy").ok())
.unwrap_or_default();

let required = ["localhost", "127.0.0.1", "::1"];
let mut parts: Vec<String> = existing
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();

for &r in &required {
if !parts.iter().any(|p| p.eq_ignore_ascii_case(r)) {
parts.push(r.to_owned());
}
}

let updated = parts.join(",");
// Set both uppercase and lowercase to cover different libraries' expectations
unsafe {
std::env::set_var("NO_PROXY", &updated);
std::env::set_var("no_proxy", &updated);
}
}

/// Clear proxy env vars for the test process so localhost traffic is never proxied.
fn disable_proxies_for_tests() {
let has_proxy = std::env::var_os("HTTP_PROXY").is_some()
|| std::env::var_os("http_proxy").is_some()
|| std::env::var_os("HTTPS_PROXY").is_some()
|| std::env::var_os("https_proxy").is_some();
if !has_proxy {
return;
}
unsafe {
std::env::remove_var("HTTP_PROXY");
std::env::remove_var("http_proxy");
std::env::remove_var("HTTPS_PROXY");
std::env::remove_var("https_proxy");
}
}

pub(crate) fn get_tmp_sqlite_path() -> PathBuf {
// Set the absolute path of the project directory
let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
Expand Down
27 changes: 6 additions & 21 deletions crate/server/src/tests/test_validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,13 @@ use cosmian_kms_server_database::reexport::cosmian_kmip::{
use cosmian_logger::debug;

use crate::{
config::ServerParams, core::KMS, error::KmsError, tests::test_utils::https_clap_config,
config::ServerParams, core::KMS, error::KmsError,
tests::test_utils::https_clap_config_with_external_proxy,
};

#[ignore = "Requires network access to perform certificate validation"]
#[ignore = "Requires network access to perform certificate validation since CRL is fetched from https://package.cosmian.com/kms/crl_tests/intermediate.crl.pem"]
#[tokio::test]
pub(crate) async fn test_validate_with_certificates_bytes() -> Result<(), KmsError> {
// Skip this test in Nix sandbox (no network access)
if option_env!("IN_NIX_SHELL").is_some() || std::env::var("IN_NIX_SHELL").is_ok() {
eprintln!(
"Skipping test_validate_with_certificates_bytes: running in Nix sandbox without network access"
);
return Ok(());
}

cosmian_logger::log_init(None);
let root_path = path::Path::new("../../test_data/certificates/chain/ca.cert.der");
let intermediate_path =
Expand All @@ -39,7 +32,7 @@ pub(crate) async fn test_validate_with_certificates_bytes() -> Result<(), KmsErr
let leaf1_cert = fs::read(leaf1_path)?;
let leaf2_cert = fs::read(leaf2_path)?;

let clap_config = https_clap_config();
let clap_config = https_clap_config_with_external_proxy();
let kms = Arc::new(KMS::instantiate(Arc::new(ServerParams::try_from(clap_config)?)).await?);
let owner = "eyJhbGciOiJSUzI1Ni";
let request = Validate {
Expand Down Expand Up @@ -116,17 +109,9 @@ pub(crate) async fn test_validate_with_certificates_bytes() -> Result<(), KmsErr
Ok(())
}

#[ignore = "Requires network access to perform certificate validation"]
#[ignore = "Requires network access to perform certificate validation since CRL is fetched from https://package.cosmian.com/kms/crl_tests/intermediate.crl.pem"]
#[tokio::test]
pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError> {
// Skip this test in Nix sandbox (no network access)
if option_env!("IN_NIX_SHELL").is_some() || std::env::var("IN_NIX_SHELL").is_ok() {
eprintln!(
"Skipping test_validate_with_certificates_ids: running in Nix sandbox without network access"
);
return Ok(());
}

cosmian_logger::log_init(None);
let root_path = path::Path::new("../../test_data/certificates/chain/ca.cert.der");
let intermediate_path =
Expand All @@ -139,7 +124,7 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError
let leaf1_cert = fs::read(leaf1_path)?;
let leaf2_cert = fs::read(leaf2_path)?;

let clap_config = https_clap_config();
let clap_config = https_clap_config_with_external_proxy();
let kms = Arc::new(KMS::instantiate(Arc::new(ServerParams::try_from(clap_config)?)).await?);
let owner = "eyJhbGciOiJSUzI1Ni";
// add certificates to kms
Expand Down
Loading
Loading