From 54275f9784dd7e55eee0334d7c0c09056f2771f5 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 18 Feb 2026 10:13:15 +0100 Subject: [PATCH] fix: openssl version and add GetAttributes KMIP operations in UI --- .github/scripts/test_hsm_utimaco.sh | 8 +- .github/scripts/test_pykmip.sh | 9 +- CHANGELOG.md | 20 +++ Cargo.lock | 1 + .../cli/src/tests/kms/certificates/export.rs | 10 +- crate/server/Cargo.toml | 1 + .../server/src/config/command_line/logging.rs | 2 +- crate/server/src/lib.rs | 6 +- crate/server/src/main.rs | 46 ++--- crate/server/src/openssl_providers.rs | 157 ++++++++++++++++++ crate/server/src/start_kms_server.rs | 20 +-- crate/server/src/tests/ttlv_tests/config.rs | 2 + crate/test_kms_server/src/lib.rs | 7 +- crate/test_kms_server/src/test_server.rs | 3 + .../server.vendor.dynamic.darwin.sha256 | 2 +- .../server.vendor.dynamic.linux.sha256 | 2 +- .../server.vendor.static.darwin.sha256 | 2 +- .../server.vendor.static.linux.sha256 | 2 +- nix/expected-hashes/ui.vendor.fips.sha256 | 2 +- nix/expected-hashes/ui.vendor.non-fips.sha256 | 2 +- nix/ui.nix | 11 ++ shell.nix | 62 ++++--- test_data | 2 +- ui/package.json | 2 +- ui/scripts/sync-wasm.mjs | 57 +++++++ ui/src/AccessGrant.tsx | 3 +- ui/src/AccessRevoke.tsx | 3 +- ui/vite.config.ts | 42 ++++- 28 files changed, 389 insertions(+), 97 deletions(-) create mode 100644 crate/server/src/openssl_providers.rs create mode 100644 ui/scripts/sync-wasm.mjs diff --git a/.github/scripts/test_hsm_utimaco.sh b/.github/scripts/test_hsm_utimaco.sh index 469902c806..dd401dc8ef 100644 --- a/.github/scripts/test_hsm_utimaco.sh +++ b/.github/scripts/test_hsm_utimaco.sh @@ -63,7 +63,7 @@ UTIMACO_LIB_DIR="$(dirname "$UTIMACO_PKCS11_LIB")" # Utimaco integration test (KMS) -env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES \ +env -u LD_PRELOAD \ PATH="$PATH" \ LD_LIBRARY_PATH="${UTIMACO_LIB_DIR}:${NIX_OPENSSL_OUT:+$NIX_OPENSSL_OUT/lib:}${LD_LIBRARY_PATH:-}" \ HSM_MODEL="utimaco" \ @@ -80,7 +80,7 @@ env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES \ # Utimaco loader test (pure Nix, scoped runtime) -env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES \ +env -u LD_PRELOAD \ PATH="$PATH" \ LD_LIBRARY_PATH="${UTIMACO_LIB_DIR}:${NIX_OPENSSL_OUT:+$NIX_OPENSSL_OUT/lib:}${LD_LIBRARY_PATH:-}" \ HSM_MODEL="utimaco" \ @@ -98,7 +98,7 @@ env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES \ # Optionally run Google CSE CLI tests if environment is provided if [ -n "${TEST_GOOGLE_OAUTH_CLIENT_ID:-}" ] && [ -n "${TEST_GOOGLE_OAUTH_CLIENT_SECRET:-}" ] && [ -n "${TEST_GOOGLE_OAUTH_REFRESH_TOKEN:-}" ]; then # shellcheck disable=SC2086 - env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES "PATH=$PATH" \ + env -u LD_PRELOAD "PATH=$PATH" \ LD_LIBRARY_PATH="${UTIMACO_LIB_DIR}:${NIX_OPENSSL_OUT:+$NIX_OPENSSL_OUT/lib:}${LD_LIBRARY_PATH:-}" \ HSM_MODEL="utimaco" \ HSM_USER_PASSWORD="$HSM_USER_PASSWORD" \ @@ -114,7 +114,7 @@ if [ -n "${TEST_GOOGLE_OAUTH_CLIENT_ID:-}" ] && [ -n "${TEST_GOOGLE_OAUTH_CLIENT -- --nocapture kmip_2_1_xml_pkcs11_m_1_21 --ignored # shellcheck disable=SC2086 - env -u LD_PRELOAD -u OPENSSL_CONF -u OPENSSL_MODULES "PATH=$PATH" \ + env -u LD_PRELOAD "PATH=$PATH" \ LD_LIBRARY_PATH="${UTIMACO_LIB_DIR}:${NIX_OPENSSL_OUT:+$NIX_OPENSSL_OUT/lib:}${LD_LIBRARY_PATH:-}" \ HSM_MODEL="utimaco" \ HSM_USER_PASSWORD="$HSM_USER_PASSWORD" \ diff --git a/.github/scripts/test_pykmip.sh b/.github/scripts/test_pykmip.sh index 5d024abd3a..e3349655ca 100644 --- a/.github/scripts/test_pykmip.sh +++ b/.github/scripts/test_pykmip.sh @@ -21,9 +21,12 @@ FEATURES_FLAG=(--features non-fips) : "${COSMIAN_KMS_CONF:=$REPO_ROOT/scripts/kms.toml}" export COSMIAN_KMS_CONF -# Ensure Python's ssl module can initialize: avoid custom OpenSSL config used by Rust OpenSSL. -# Do NOT clear LD_LIBRARY_PATH; keep Nix-provided runtime consistent to avoid GLIBC mismatches. -unset OPENSSL_CONF OPENSSL_MODULES || true +# Note: OPENSSL_CONF and OPENSSL_MODULES are intentionally kept set here so the KMS +# server process can find the OpenSSL providers (e.g. legacy.dylib) in the Nix store. +# The compiled-in MODULESDIR is /usr/local/cosmian/lib/ossl-modules (production path), +# which does not exist in the nix-shell dev environment. +# All Python invocations below already use `env -u OPENSSL_CONF -u OPENSSL_MODULES` +# to isolate Python's ssl module from the Rust/KMS OpenSSL configuration. # Ensure Python is available (nix.sh sets WITH_PYTHON=1 which adds python311 + virtualenv) require_cmd python3 "Python 3 is required. Re-run via 'bash .github/scripts/nix.sh test pykmip' so nix-shell can provide it." diff --git a/CHANGELOG.md b/CHANGELOG.md index 3497a12780..6973793db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ All notable changes to this project will be documented in this file. ### 🐛 Bug Fixes - Add MLKEM algorithms to the predefined DEFAULT KMIP policy +- Fix non-FIPS `openssl.cnf` provider configuration: the FIPS provider was incorrectly + activated in non-FIPS builds via `nix/openssl.nix`, blocking default-provider algorithms + (ChaCha20, secp256k1) and causing 6 crypto test failures. `nix/openssl.nix` now generates + distinct provider configurations per build variant: FIPS builds use `fips+base`, non-FIPS + builds use `default+legacy+base`. +- Fix `KResultHelper` import in `main.rs` being feature-gated to `non-fips` only, causing a + missing `.context()` method on `init_openssl_providers()` result in FIPS builds. + +### ⚙️ Build + +- Refactor OpenSSL provider management into a dedicated `openssl_providers` module in + `crate/server/src/`, consolidating `safe_openssl_version_info()`, `init_openssl_providers()` + (production), and `init_openssl_providers_for_tests()` (test environments) into a single place. +- Improve determinism of `nix/openssl.nix` OpenSSL builds: + - Patch `ENGINESDIR`/`MODULESDIR` in the generated Makefile to fixed + `/usr/local/cosmian/lib/...` paths, preventing Nix store path embedding in compiled + `libcrypto` strings. + - Scrub Nix store paths from `crypto/buildinf.h` after `make depend`. + - Set `SOURCE_DATE_EPOCH=1` and `ZERO_AR_DATE=1` in build and install phases. + - Normalize all output file timestamps with `find $out -exec touch --date=@1 {} +`. ### ⚙️ Build diff --git a/Cargo.lock b/Cargo.lock index c0db53c5be..eccee08682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,6 +1317,7 @@ dependencies = [ "native-tls", "num-bigint-dig", "openssl", + "openssl-sys", "opentelemetry 0.27.1", "opentelemetry-otlp 0.27.0", "opentelemetry_sdk 0.27.1", diff --git a/crate/cli/src/tests/kms/certificates/export.rs b/crate/cli/src/tests/kms/certificates/export.rs index 7601865270..8f5074dad9 100644 --- a/crate/cli/src/tests/kms/certificates/export.rs +++ b/crate/cli/src/tests/kms/certificates/export.rs @@ -21,7 +21,7 @@ use openssl::{ x509::{X509, store::X509StoreBuilder}, }; use tempfile::TempDir; -use test_kms_server::start_default_test_kms_server; +use test_kms_server::{init_openssl_providers_for_tests, start_default_test_kms_server}; use uuid::Uuid; use crate::{ @@ -39,6 +39,8 @@ use crate::{ #[tokio::test] async fn test_import_export_p12_25519() -> KmsCliResult<()> { log_init(option_env!("RUST_LOG")); + init_openssl_providers_for_tests(); + // load the PKCS#12 file let p12_bytes = include_bytes!("../../../../../../test_data/certificates/another_p12/ed25519.p12"); @@ -221,6 +223,8 @@ async fn test_import_export_p12_25519() -> KmsCliResult<()> { #[tokio::test] async fn test_import_p12_rsa() { + init_openssl_providers_for_tests(); + let tmp_dir = TempDir::new().unwrap(); let tmp_path = tmp_dir.path(); // load the PKCS#12 file @@ -415,6 +419,8 @@ async fn test_self_signed_export_loop() -> KmsCliResult<()> { #[tokio::test] async fn test_export_root_and_intermediate_pkcs12() -> KmsCliResult<()> { + init_openssl_providers_for_tests(); + // Create a test server let ctx = start_default_test_kms_server().await; @@ -475,6 +481,8 @@ async fn test_export_root_and_intermediate_pkcs12() -> KmsCliResult<()> { #[tokio::test] async fn test_export_import_legacy_p12() -> KmsCliResult<()> { + init_openssl_providers_for_tests(); + // Create a test server let ctx = start_default_test_kms_server().await; diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index 00530814af..0de7b1c503 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -86,6 +86,7 @@ num-bigint-dig = { workspace = true, features = [ "zeroize", ] } openssl = { workspace = true } +openssl-sys = "0.9" opentelemetry = { workspace = true } opentelemetry-otlp = { workspace = true } opentelemetry_sdk = { workspace = true } diff --git a/crate/server/src/config/command_line/logging.rs b/crate/server/src/config/command_line/logging.rs index 2a59557773..0e09a7ae47 100644 --- a/crate/server/src/config/command_line/logging.rs +++ b/crate/server/src/config/command_line/logging.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use clap::Args; use serde::{Deserialize, Serialize}; -#[expect(clippy::struct_excessive_bools)] +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Default, Args, Deserialize, Serialize, Clone)] #[serde(default)] pub struct LoggingConfig { diff --git a/crate/server/src/lib.rs b/crate/server/src/lib.rs index 2cba6d6e0b..6e295d3c71 100644 --- a/crate/server/src/lib.rs +++ b/crate/server/src/lib.rs @@ -7,10 +7,13 @@ pub mod core; pub mod cron; pub mod error; pub mod middlewares; +pub mod openssl_providers; pub mod result; pub mod routes; pub mod socket_server; pub mod start_kms_server; +pub mod tls_config; + #[expect( clippy::panic, clippy::unwrap_used, @@ -22,5 +25,4 @@ pub mod start_kms_server; dead_code )] #[cfg(test)] -mod tests; -pub mod tls_config; +pub mod tests; diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index 8caddd145e..94477fac0f 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -2,13 +2,11 @@ use std::sync::Arc; use cosmian_kms_server::{ config::{ClapConfig, ServerParams}, - result::KResult, + openssl_providers::safe_openssl_version_info, + result::{KResult, KResultHelper}, }; -#[cfg(feature = "non-fips")] -use cosmian_kms_server_database::reexport::cosmian_kmip::KmipResultHelper; use cosmian_logger::{TelemetryConfig, TracingConfig, info, tracing_init}; use dotenvy::dotenv; -use openssl::provider::Provider; use tracing::span; /// Get the default `RUST_LOG` configuration if not set @@ -85,36 +83,24 @@ async fn run() -> KResult<()> { let span = span!(tracing::Level::TRACE, "kms"); let _guard = span.enter(); - info!( - "OpenSSL version: {}, in {}, number: {:x}", - openssl::version::version(), - openssl::version::dir(), - openssl::version::number() - ); + let (ossl_version, ossl_dir, ossl_number) = safe_openssl_version_info(); + if ossl_number == 0 { + tracing::error!( + "OpenSSL does not appear to be available (version number is 0). \ + Please verify that OpenSSL is correctly installed and accessible." + ); + return Err(cosmian_kms_server::error::KmsError::ServerError( + "OpenSSL is not available – cannot start the KMS server".to_owned(), + )); + } + info!("OpenSSL version: {ossl_version}, in {ossl_dir}, number: {ossl_number:x}"); // For an explanation of OpenSSL providers, // https://docs.openssl.org/3.1/man7/crypto/#openssl-providers - // In FIPS mode, we only load the FIPS provider - #[cfg(not(feature = "non-fips"))] - { - info!("Load FIPS provider"); - Provider::load(None, "fips")?; - } - - // Not in FIPS mode and version > 3.0: load the default provider and the legacy provider - // so that we can use the legacy algorithms. - // particularly those used for old PKCS#12 formats - #[cfg(feature = "non-fips")] - if openssl::version::number() >= 0x3000_0000 { - info!("Load legacy provider"); - Provider::try_load(None, "legacy", true) - .context("unable to load the openssl legacy provider")?; - } else { - // In version < 3.0, we only load the default provider - info!("Load default provider"); - Provider::load(None, "default")?; - } + // Load the appropriate OpenSSL provider based on FIPS mode and OpenSSL version + cosmian_kms_server::openssl_providers::init_openssl_providers() + .context("unable to load the required OpenSSL provider")?; // Instantiate a config object using the env variables and the args of the binary info!("Command line / file config: {clap_config:#?}"); diff --git a/crate/server/src/openssl_providers.rs b/crate/server/src/openssl_providers.rs new file mode 100644 index 0000000000..1d59f2cf54 --- /dev/null +++ b/crate/server/src/openssl_providers.rs @@ -0,0 +1,157 @@ +use std::ffi::CStr; + +#[cfg(feature = "non-fips")] +use cosmian_logger::info; + +/// Safely retrieve OpenSSL version information without risking a segmentation +/// fault. In some edge-case environments (missing OpenSSL shared library, +/// incomplete installation, restricted container, …) the underlying +/// `OpenSSL_version` C function may return a NULL pointer or call through an +/// uninitialised function-pointer table, both of which cause a SIGSEGV. +/// +/// This helper calls the raw `openssl-sys` FFI directly so it can check for a +/// NULL return *before* converting to a Rust `&str`. +/// +/// # Returns +/// +/// Returns a tuple of `(version_string, dir_string, version_number)` where: +/// - `version_string` is the OpenSSL version text (e.g., "OpenSSL 3.6.0") +/// - `dir_string` is the OpenSSL installation directory +/// - `version_number` is the numeric version (e.g., 0x30600000 for version 3.6.0) +/// +/// If OpenSSL is not available, returns `("", "", 0)`. +#[allow(unsafe_code)] +#[must_use] +pub fn safe_openssl_version_info() -> (String, String, u64) { + // SAFETY: `OpenSSL_version_num` returns a plain integer and never + // dereferences any pointer, so it is safe to call even when OpenSSL is not + // fully initialised. We call it first as a cheap liveness check. + // Note: Returns `c_ulong` which is u32 on Windows and u64 on Unix. + // We need `as u64` for cross-platform compatibility (widening is safe). + #[allow(clippy::as_conversions)] + let num = unsafe { openssl_sys::OpenSSL_version_num() } as u64; + if num == 0 { + return ("".to_owned(), "".to_owned(), 0); + } + + // SAFETY: `OpenSSL_version` returns a `*const c_char` pointing to a static + // string. We check for NULL before creating a `CStr`. + let version = unsafe { + let ptr = openssl_sys::OpenSSL_version(openssl_sys::OPENSSL_VERSION); + if ptr.is_null() { + "".to_owned() + } else { + CStr::from_ptr(ptr) + .to_str() + .unwrap_or("") + .to_owned() + } + }; + + let dir = unsafe { + let ptr = openssl_sys::OpenSSL_version(openssl_sys::OPENSSL_DIR); + if ptr.is_null() { + "".to_owned() + } else { + CStr::from_ptr(ptr) + .to_str() + .unwrap_or("") + .to_owned() + } + }; + + (version, dir, num) +} + +/// Initialize OpenSSL providers for test environments. +/// +/// In non-FIPS mode with OpenSSL >= 3.0: loads the legacy provider for old PKCS#12 formats. +/// In non-FIPS mode with OpenSSL < 3.0: loads the default provider. +/// In FIPS mode: no-op (FIPS provider is loaded via openssl.cnf). +/// +/// Note: The default provider is already active via openssl.cnf configuration. +/// This function only adds the legacy provider on top of it. +#[cfg(feature = "non-fips")] +#[allow( + unsafe_code, + clippy::as_conversions, + clippy::missing_panics_doc, + clippy::expect_used +)] +pub fn init_openssl_providers_for_tests() { + use std::sync::OnceLock; + + use openssl::provider::Provider; + + // Keep provider alive for program lifetime — it must not be dropped + static PROVIDER: OnceLock = OnceLock::new(); + + PROVIDER.get_or_init(|| { + let ossl_number = unsafe { openssl_sys::OpenSSL_version_num() as u64 }; + if ossl_number >= 0x3000_0000 { + // OpenSSL 3.x: load the legacy provider for old PKCS#12 formats + Provider::try_load(None, "legacy", true).expect("Failed to load legacy provider") + } else { + // OpenSSL < 3.0: load the default provider + Provider::load(None, "default").expect("Failed to load default provider") + } + }); +} + +/// No-op for FIPS builds +#[cfg(not(feature = "non-fips"))] +pub const fn init_openssl_providers_for_tests() { + // No-op in FIPS mode +} + +/// Initialize OpenSSL providers for production KMS server. +/// +/// For FIPS mode: loads the FIPS provider. +/// For non-FIPS mode with OpenSSL >= 3.0: loads the legacy provider for old PKCS#12 formats. +/// For non-FIPS mode with OpenSSL < 3.0: loads the default provider. +/// +/// Note: In non-FIPS mode, the default provider is already active via openssl.cnf. +/// This function only adds the legacy provider on top of it. +/// +/// This function uses `OnceLock` to ensure providers are loaded only once and kept alive +/// for the lifetime of the program. +/// +/// # Errors +/// +/// Returns an error if the provider fails to load. +#[allow(unsafe_code, clippy::as_conversions)] +pub fn init_openssl_providers() -> Result<(), openssl::error::ErrorStack> { + use std::sync::OnceLock; + + use openssl::provider::Provider; + + #[cfg(not(feature = "non-fips"))] + { + static PROVIDER: OnceLock = OnceLock::new(); + if PROVIDER.get().is_none() { + let provider = Provider::load(None, "fips")?; + drop(PROVIDER.set(provider)); + } + Ok(()) + } + + #[cfg(feature = "non-fips")] + { + static PROVIDER: OnceLock = OnceLock::new(); + + if PROVIDER.get().is_none() { + let ossl_number = unsafe { openssl_sys::OpenSSL_version_num() as u64 }; + let provider = if ossl_number >= 0x3000_0000 { + // OpenSSL 3.x: load the legacy provider for old PKCS#12 formats + info!("Load legacy provider"); + Provider::try_load(None, "legacy", true)? + } else { + // OpenSSL < 3.0: load the default provider + info!("Load default provider"); + Provider::load(None, "default")? + }; + drop(PROVIDER.set(provider)); + } + Ok(()) + } +} diff --git a/crate/server/src/start_kms_server.rs b/crate/server/src/start_kms_server.rs index d11f8d28de..95fec7ee61 100644 --- a/crate/server/src/start_kms_server.rs +++ b/crate/server/src/start_kms_server.rs @@ -336,23 +336,9 @@ pub async fn start_kms_server( // For an explanation of OpenSSL providers, see // https://docs.openssl.org/3.1/man7/crypto/#openssl-providers - // In FIPS mode, we only load the FIPS provider - #[cfg(not(feature = "non-fips"))] - let _provider = openssl::provider::Provider::load(None, "fips")?; - - // Not in FIPS mode and version > 3.0: load the default provider and the legacy provider - // so that we can use the legacy algorithms, - // particularly those used for old PKCS#12 formats - #[cfg(feature = "non-fips")] - let _provider = if openssl::version::number() >= 0x3000_0000 { - debug!("OpenSSL: loading the legacy provider"); - openssl::provider::Provider::try_load(None, "legacy", true) - .context("OpenSSL: unable to load the openssl legacy provider")? - } else { - debug!("OpenSSL: loading the default provider"); - // In version < 3.0, we only load the default provider - openssl::provider::Provider::load(None, "default")? - }; + // Load the appropriate OpenSSL provider based on FIPS mode and OpenSSL version + crate::openssl_providers::init_openssl_providers() + .context("OpenSSL: unable to load the required provider")?; // Instantiate KMS let kms_server = Arc::new( diff --git a/crate/server/src/tests/ttlv_tests/config.rs b/crate/server/src/tests/ttlv_tests/config.rs index efce2fc3a1..54144086c2 100644 --- a/crate/server/src/tests/ttlv_tests/config.rs +++ b/crate/server/src/tests/ttlv_tests/config.rs @@ -4,12 +4,14 @@ use cosmian_logger::log_init; use openssl::pkcs12::{ParsedPkcs12_2, Pkcs12}; use crate::{ + openssl_providers::init_openssl_providers_for_tests, socket_server::{SocketServer, SocketServerParams, create_openssl_acceptor}, tests::ttlv_tests::TEST_HOST, }; // Static config for tests static TEST_P12: LazyLock = LazyLock::new(|| { + init_openssl_providers_for_tests(); let server_p12_der = include_bytes!( "../../../../../test_data/certificates/client_server/server/kmserver.acme.com.p12" ); diff --git a/crate/test_kms_server/src/lib.rs b/crate/test_kms_server/src/lib.rs index 8df73f452f..5b65e24dda 100644 --- a/crate/test_kms_server/src/lib.rs +++ b/crate/test_kms_server/src/lib.rs @@ -1,4 +1,7 @@ -pub use cosmian_kms_server::config::{DEFAULT_SQLITE_PATH, HsmConfig, MainDBConfig}; +pub use cosmian_kms_server::{ + config::{DEFAULT_SQLITE_PATH, HsmConfig, MainDBConfig}, + openssl_providers::init_openssl_providers_for_tests, +}; pub use test_server::{ ApiTokenPolicy, AuthenticationOptions, BuildServerParamsOptions, ClientAuthOptions, ClientCertPolicy, JwtAuth as ServerJwtAuth, JwtPolicy, TestsContext, TlsMode as ServerTlsMode, @@ -24,6 +27,8 @@ static INIT_LOGGING: Once = Once::new(); pub fn init_test_logging() { INIT_LOGGING.call_once(|| { cosmian_logger::log_init(option_env!("RUST_LOG")); + // Also initialize OpenSSL legacy provider for non-FIPS tests + cosmian_kms_server::openssl_providers::init_openssl_providers_for_tests(); }); } diff --git a/crate/test_kms_server/src/test_server.rs b/crate/test_kms_server/src/test_server.rs index 4acd3b7f49..f6fb48db2d 100644 --- a/crate/test_kms_server/src/test_server.rs +++ b/crate/test_kms_server/src/test_server.rs @@ -224,6 +224,9 @@ pub async fn start_default_test_kms_server() -> &'static TestsContext { /// TLS + certificate authentication pub async fn start_default_test_kms_server_with_cert_auth() -> &'static TestsContext { + // Initialize OpenSSL legacy provider before any P12 parsing + crate::init_openssl_providers_for_tests(); + trace!("Starting test server with cert auth"); ONCE_SERVER_WITH_AUTH .get_or_try_init(|| async move { diff --git a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 index d5ce78767d..642cf2a923 100644 --- a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 @@ -1 +1 @@ -sha256-BmPj70Xm6+MA9DDB6b1VfzpbLa/pjVaNg0jHboxdGKo= +sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= diff --git a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 index d5ce78767d..642cf2a923 100644 --- a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 @@ -1 +1 @@ -sha256-BmPj70Xm6+MA9DDB6b1VfzpbLa/pjVaNg0jHboxdGKo= +sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= diff --git a/nix/expected-hashes/server.vendor.static.darwin.sha256 b/nix/expected-hashes/server.vendor.static.darwin.sha256 index 3ea27356ae..0767f4bc15 100644 --- a/nix/expected-hashes/server.vendor.static.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.static.darwin.sha256 @@ -1 +1 @@ -sha256-kRNXIRY1R9I7PNG/oePUCm8Nq7OByJ80ZH15Gk4clVg= +sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= diff --git a/nix/expected-hashes/server.vendor.static.linux.sha256 b/nix/expected-hashes/server.vendor.static.linux.sha256 index 3ea27356ae..0767f4bc15 100644 --- a/nix/expected-hashes/server.vendor.static.linux.sha256 +++ b/nix/expected-hashes/server.vendor.static.linux.sha256 @@ -1 +1 @@ -sha256-kRNXIRY1R9I7PNG/oePUCm8Nq7OByJ80ZH15Gk4clVg= +sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index 56060f20cb..2aacce5131 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1 @@ -sha256-dfWR1Z0+jCoW8FupuFjvOiX1eDr+x8rlJjVNLivlP3c= +sha256-IBvwHtcoyOX45BN/4/rZRCdse/n0r5iGHqiMtKWwWos= diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index e657f9601d..9a7544f89d 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-Gviu18l8szkEK9uy106HvAd+rZuYqw43rqv0tz1DbDg= +sha256-vVEMcIsU6lVfVrRTtHzBRzOHNX5+3D41CdjFSOPd9oA= diff --git a/nix/ui.nix b/nix/ui.nix index efb14c8393..eb5d1ae4ad 100644 --- a/nix/ui.nix +++ b/nix/ui.nix @@ -125,6 +125,7 @@ let nativeBuildInputs = [ wasmBindgenCli pkgs.llvmPackages.lld + pkgs.binaryen ]; # Ensure wasm linking uses lld provided by Nix CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_LINKER = "${pkgs.llvmPackages.lld}/bin/wasm-ld"; @@ -163,6 +164,16 @@ let --out-dir $out/pkg \ "$WASM_PATH" + # Optional size optimization: shrink the emitted wasm-bindgen binary. + # binaryen is provided by Nix, so this is deterministic. + if command -v wasm-opt >/dev/null 2>&1; then + echo "Optimizing WASM with wasm-opt -Oz" + wasm-opt -Oz --enable-bulk-memory --enable-nontrapping-float-to-int "$out/pkg/cosmian_kms_client_wasm_bg.wasm" -o "$out/pkg/cosmian_kms_client_wasm_bg.wasm.opt" + mv "$out/pkg/cosmian_kms_client_wasm_bg.wasm.opt" "$out/pkg/cosmian_kms_client_wasm_bg.wasm" + else + echo "WARNING: wasm-opt not found; skipping wasm optimization" >&2 + fi + # Basic sanity check test -f "$out/pkg/cosmian_kms_client_wasm_bg.wasm" test -f "$out/pkg/cosmian_kms_client_wasm.js" diff --git a/shell.nix b/shell.nix index bb60762b41..2c6cd83ff0 100644 --- a/shell.nix +++ b/shell.nix @@ -17,7 +17,8 @@ }; in pinned, -# Explicit variant argument to avoid relying on builtins.getEnv during evaluation + # Explicit variant argument to avoid relying on builtins.getEnv during evaluation + variant ? "fips", }: let @@ -46,6 +47,22 @@ let ; static = true; }; + # Import non-FIPS OpenSSL 3.6.0 - will be used for non-FIPS builds + openssl360NonFips = import ./nix/openssl.nix { + inherit (pkgs) + stdenv + lib + fetchurl + perl + coreutils + ; + static = false; + version = "3.6.0"; + enableLegacy = true; + srcUrl = "https://package.cosmian.com/openssl/openssl-3.6.0.tar.gz"; + sha256SRI = "sha256-tqX0S362nj+jXb8VUkQFtEg3pIHUPYHa3d4/8h/LuOk="; + expectedHash = "b6a5f44b7eb69e3fa35dbf15524405b44837a481d43d81daddde3ff21fcbb8e9"; + }; # Shared (dynamic) build for components that require .so (e.g., SoftHSM2) openssl312FipsShared = import ./nix/openssl.nix { inherit (pkgs) @@ -67,15 +84,16 @@ let softhsmDrv = import ./nix/softhsm2.nix { inherit pkgs; # Use FIPS shared OpenSSL when running in FIPS variant so SoftHSM2 links to it - openssl = if (builtins.getEnv "VARIANT") == "fips" then openssl312FipsShared else pkgs.openssl; + # For non-FIPS, use OpenSSL 3.6.0 instead of pkgs.openssl (3.3.2) + openssl = if variant == "fips" then openssl312FipsShared else openssl360NonFips; }; in pkgs.mkShell { buildInputs = [ - # Provide both OpenSSL packages - the shellHook will configure which one to use + # Provide OpenSSL packages - the shellHook will configure which one to use openssl312Fips openssl312FipsShared - pkgs.openssl + openssl360NonFips pkgs.pkg-config pkgs.gcc pkgs.rust-bin.stable.latest.default @@ -120,8 +138,9 @@ pkgs.mkShell { export OPENSSL_NO_VENDOR=1 # Check which variant is requested (defaults to non-fips if not set) - # VARIANT should be set by nix.sh via the command string - VARIANT_MODE="''${VARIANT:-non-fips}" + # VARIANT should be set by nix.sh via the command string OR via --argstr + # Prefer the Nix argument passed via --argstr, fall back to environment variable + VARIANT_MODE="${variant}" if [ "$VARIANT_MODE" = "fips" ]; then # Use Nix-provided FIPS OpenSSL 3.1.2 (shared) for dynamic linking in Rust @@ -147,15 +166,15 @@ pkgs.mkShell { echo " OPENSSL_MODULES=$OPENSSL_MODULES" # Verify FIPS OpenSSL shared library presence - if [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.so.3" ]; then - echo "FIPS OpenSSL libcrypto.so.3 found (shared)" + if [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.so.3" ] || [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.3.dylib" ]; then + echo "FIPS OpenSSL 3.1.2 libcrypto library found (shared)" else - echo "WARNING: FIPS OpenSSL libcrypto.so.3 NOT found at $OPENSSL_PKG_PATH/lib" + echo "WARNING: FIPS OpenSSL libcrypto library NOT found at $OPENSSL_PKG_PATH/lib" fi # Verify FIPS module - if [ -f "$OPENSSL_MODULES/fips.so" ]; then - echo "FIPS provider module found: $OPENSSL_MODULES/fips.so" + if [ -f "$OPENSSL_MODULES/fips.so" ] || [ -f "$OPENSSL_MODULES/fips.dylib" ]; then + echo "FIPS provider module found: $OPENSSL_MODULES/" else echo "WARNING: FIPS provider module NOT found" fi @@ -174,25 +193,26 @@ pkgs.mkShell { echo "LD_PRELOAD set to bootstrap OpenSSL FIPS providers" fi else - # Use standard nixpkgs OpenSSL for non-FIPS - # Note: pkgs.openssl.dev has headers, pkgs.openssl.out has libraries - OPENSSL_PKG_PATH="${pkgs.openssl.out}" + # Use OpenSSL 3.6.0 for non-FIPS builds (matches server build) + OPENSSL_PKG_PATH="${openssl360NonFips}" export OPENSSL_DIR="$OPENSSL_PKG_PATH" export OPENSSL_LIB_DIR="$OPENSSL_PKG_PATH/lib" - export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include" + export OPENSSL_INCLUDE_DIR="$OPENSSL_PKG_PATH/include" - # Use the standard OpenSSL config from nixpkgs - export OPENSSL_CONF="$OPENSSL_PKG_PATH/etc/ssl/openssl.cnf" + # Use the OpenSSL 3.6.0 config + export OPENSSL_CONF="$OPENSSL_PKG_PATH/ssl/openssl.cnf" + export OPENSSL_MODULES="$OPENSSL_PKG_PATH/lib/ossl-modules" - echo "Using standard OpenSSL (non-FIPS): $OPENSSL_PKG_PATH" + echo "Using OpenSSL 3.6.0 (non-FIPS): $OPENSSL_PKG_PATH" echo " OPENSSL_CONF=$OPENSSL_CONF" + echo " OPENSSL_MODULES=$OPENSSL_MODULES" # Verify non-FIPS OpenSSL library presence - if [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.so.3" ]; then - echo "OpenSSL libcrypto.so.3 found" + if [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.so.3" ] || [ -f "$OPENSSL_PKG_PATH/lib/libcrypto.3.dylib" ]; then + echo "OpenSSL 3.6.0 libcrypto library found" else - echo "WARNING: OpenSSL libcrypto.so.3 NOT found at $OPENSSL_PKG_PATH/lib" + echo "WARNING: OpenSSL libcrypto library NOT found at $OPENSSL_PKG_PATH/lib" fi # Runtime library path for non-FIPS diff --git a/test_data b/test_data index 2d72a12b75..4c48349273 160000 --- a/test_data +++ b/test_data @@ -1 +1 @@ -Subproject commit 2d72a12b7548bf672fe2d6cfe8db6f36d407bfc4 +Subproject commit 4c48349273bda0bcba941b5cae0cd58c74de563a diff --git a/ui/package.json b/ui/package.json index 0ddce722c8..9f15c80112 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build:wasm": "cd .. && (cd crate/wasm && wasm-pack build --target web --release --features non-fips) && mkdir -p ui/src/wasm && rm -rf ui/src/wasm/pkg && cp -R crate/wasm/pkg ui/src/wasm/", + "build:wasm": "cd .. && wasm-pack build crate/wasm --target web --release --features non-fips && node ui/scripts/sync-wasm.mjs", "build": "tsc -b && vite build", "lint": "eslint .", "fix": "eslint . --fix", diff --git a/ui/scripts/sync-wasm.mjs b/ui/scripts/sync-wasm.mjs new file mode 100644 index 0000000000..1486a69a65 --- /dev/null +++ b/ui/scripts/sync-wasm.mjs @@ -0,0 +1,57 @@ +import { execFile } from "node:child_process"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +const repoRoot = process.cwd(); +const srcPkg = path.join(repoRoot, "crate", "wasm", "pkg"); +const dstWasmDir = path.join(repoRoot, "ui", "src", "wasm"); +const dstPkg = path.join(dstWasmDir, "pkg"); + +async function hasWasmOpt() { + try { + await execFileAsync("wasm-opt", ["--version"], { windowsHide: true }); + return true; + } catch { + return false; + } +} + +async function optimizeWasmInPlace(wasmPath) { + const tmpPath = `${wasmPath}.opt`; + await execFileAsync("wasm-opt", ["-Oz", wasmPath, "-o", tmpPath], { windowsHide: true }); + await fs.rename(tmpPath, wasmPath); +} + +async function maybeOptimizeWasm(pkgDir) { + const enabled = await hasWasmOpt(); + if (!enabled) { + console.log("wasm-opt not found; skipping WASM optimization"); + return; + } + + const entries = await fs.readdir(pkgDir, { withFileTypes: true }); + const wasmFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".wasm")).map((e) => path.join(pkgDir, e.name)); + + if (wasmFiles.length === 0) { + console.log("No .wasm file found in pkg; skipping WASM optimization"); + return; + } + + for (const wasmPath of wasmFiles) { + console.log(`Optimizing WASM with wasm-opt -Oz: ${wasmPath}`); + await optimizeWasmInPlace(wasmPath); + } +} + +await maybeOptimizeWasm(srcPkg); + +await fs.mkdir(dstWasmDir, { recursive: true }); +await fs.rm(dstPkg, { recursive: true, force: true }); + +// Node.js >=16 supports fs.cp; Node.js >=18 is expected on this repo's toolchain. +await fs.cp(srcPkg, dstPkg, { recursive: true }); + +console.log(`Synced WASM pkg: ${srcPkg} -> ${dstPkg}`); diff --git a/ui/src/AccessGrant.tsx b/ui/src/AccessGrant.tsx index 53d89cc4f4..175c2d3b3a 100644 --- a/ui/src/AccessGrant.tsx +++ b/ui/src/AccessGrant.tsx @@ -6,12 +6,13 @@ import { getNoTTLVRequest, postNoTTLVRequest } from "./utils"; interface AccessGrantFormData { user_id: string; unique_identifier: string; - operation_types: Array<"create" | "get" | "encrypt" | "decrypt" | "import" | "revoke" | "locate" | "rekey" | "destroy">; + operation_types: Array<"create" | "get" | "getattributes" | "encrypt" | "decrypt" | "import" | "revoke" | "locate" | "rekey" | "destroy">; grant_create_access_right: boolean; } const KMIP_OPERATIONS = [ { label: "Get", value: "get" }, + { label: "GetAttributes", value: "getattributes" }, { label: "Encrypt", value: "encrypt" }, { label: "Decrypt", value: "decrypt" }, { label: "Revoke", value: "revoke" }, diff --git a/ui/src/AccessRevoke.tsx b/ui/src/AccessRevoke.tsx index 865f546bd4..3ef7c215d4 100644 --- a/ui/src/AccessRevoke.tsx +++ b/ui/src/AccessRevoke.tsx @@ -6,12 +6,13 @@ import { getNoTTLVRequest, postNoTTLVRequest } from "./utils"; interface AccessRevokeFormData { user_id: string; unique_identifier: string; - operation_types: Array<"create" | "get" | "encrypt" | "decrypt" | "import" | "revoke" | "locate" | "rekey" | "destroy">; + operation_types: Array<"create" | "get" | "getattributes" | "encrypt" | "decrypt" | "import" | "revoke" | "locate" | "rekey" | "destroy">; revoke_create_access_right: boolean; } const KMIP_OPERATIONS = [ { label: "Get", value: "get" }, + { label: "GetAttributes", value: "getattributes" }, { label: "Encrypt", value: "encrypt" }, { label: "Decrypt", value: "decrypt" }, { label: "Revoke", value: "revoke" }, diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 4db6ecd0e9..9e3453b7f0 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -1,10 +1,38 @@ -import tailwindcss from '@tailwindcss/vite' -import react from '@vitejs/plugin-react-swc' -import { defineConfig } from 'vite' - +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ - base: '/ui', - plugins: [react(), tailwindcss(),], -}) + base: "/ui", + plugins: [react(), tailwindcss()], + build: { + // The UI bundles include Ant Design; keep chunking but avoid noisy warnings when + // a single library chunk is marginally above 500kB. + chunkSizeWarningLimit: 550, + rollupOptions: { + output: { + manualChunks(id) { + // Split the local WASM client glue code into its own chunk. + // (The .wasm binary itself is emitted as a separate asset.) + if (id.includes("/src/wasm/pkg/") || id.includes("\\src\\wasm\\pkg\\")) { + return "wasm-client"; + } + + if (id.includes("node_modules")) { + // Split Ant Design into multiple chunks to keep each output below the warning threshold. + if (id.includes("antd/es/table") || id.includes("antd/lib/table")) return "antd-table"; + if (id.includes("antd/es/modal") || id.includes("antd/lib/modal")) return "antd-modal"; + if (id.includes("antd")) return "antd"; + if (id.includes("@ant-design")) return "ant-icons"; + if (id.includes("react-router")) return "react-router"; + if (id.includes("react-dom") || id.includes("react/")) return "react"; + return "vendor"; + } + + return undefined; + }, + }, + }, + }, +});