From 4bbb45c546120988caeb2f9d7ae05e54617f19df Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 18 Feb 2026 05:58:27 +0100 Subject: [PATCH 1/3] fix: non-FIPS deterministic build --- .github/scripts/common.sh | 4 +- .github/scripts/nix.sh | 28 +- .github/scripts/smoke_test_dmg.sh | 27 +- .github/scripts/test_hsm_utimaco.sh | 8 +- .github/scripts/test_otel_export.sh | 1 - .github/scripts/test_pykmip.sh | 9 +- .github/workflows/packaging.yml | 6 +- .github/workflows/pr.yml | 2 - CHANGELOG.md | 30 ++ 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 + default.nix | 98 +++++-- nix/README.md | 164 ++++++----- nix/docker.nix | 19 +- .../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/kms-server.nix | 274 +++++++++++------- nix/openssl.nix | 256 +++++++++++----- nix/scripts/generate_sbom.sh | 8 +- nix/scripts/package_common.sh | 110 +++---- nix/scripts/package_dmg.sh | 39 ++- nix/ui.nix | 39 ++- shell.nix | 62 ++-- 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 ++- 41 files changed, 1027 insertions(+), 531 deletions(-) create mode 100644 crate/server/src/openssl_providers.rs create mode 100644 ui/scripts/sync-wasm.mjs diff --git a/.github/scripts/common.sh b/.github/scripts/common.sh index 1257eab775..5551ec29bb 100644 --- a/.github/scripts/common.sh +++ b/.github/scripts/common.sh @@ -136,8 +136,8 @@ ensure_macos_frameworks_ldflags() { # Unified nixpkgs pin (used by all scripts) # Keep a single source of truth for the pinned nixpkgs URL. -# Pin nixpkgs for a stable toolchain; Linux builds target GLIBC <= 2.34. -export PIN_URL="https://github.com/NixOS/nixpkgs/archive/24.11.tar.gz" +# IMPORTANT: Use an immutable commit tarball to ensure builds are deterministic across machines. +export PIN_URL="https://github.com/NixOS/nixpkgs/archive/8b27c1239e5c421a2bbc2c65d52e4a6fbf2ff296.tar.gz" # Backward-compatible alias used by some scripts export PINNED_NIXPKGS_URL="$PIN_URL" diff --git a/.github/scripts/nix.sh b/.github/scripts/nix.sh index 76fc36a19f..3052d92137 100755 --- a/.github/scripts/nix.sh +++ b/.github/scripts/nix.sh @@ -58,9 +58,6 @@ usage() { -l, --link OpenSSL linkage type (default: static) static: statically link OpenSSL 3.6.0 dynamic: dynamically link system OpenSSL - --enforce-deterministic-hash - When true, enforce expected hashes (fail on mismatch). - When false (default), relax expected-hash enforcement. For testing, also supports environment variables: REDIS_HOST, REDIS_PORT @@ -163,7 +160,6 @@ parse_global_options() { PROFILE="debug" VARIANT="fips" LINK="static" - ENFORCE_DETERMINISTIC_HASH="false" # Parse global options before the subcommand while [ $# -gt 0 ]; do @@ -182,10 +178,6 @@ parse_global_options() { LINK_EXPLICIT=1 shift 2 || true ;; - --enforce-deterministic-hash | --enforce_deterministic_hash) - ENFORCE_DETERMINISTIC_HASH="${2:-}" - shift 2 || true - ;; docker | test | package | sbom | update-hashes) COMMAND="$1" shift @@ -208,17 +200,7 @@ parse_global_options() { # Validate command argument [ -z "${COMMAND:-}" ] && usage - # Normalize boolean-ish inputs - case "${ENFORCE_DETERMINISTIC_HASH}" in - true | TRUE | 1) ENFORCE_DETERMINISTIC_HASH="true" ;; - false | FALSE | 0 | "") ENFORCE_DETERMINISTIC_HASH="false" ;; - *) - echo "Error: --enforce-deterministic-hash must be true/false" >&2 - exit 1 - ;; - esac - - export PROFILE VARIANT LINK ENFORCE_DETERMINISTIC_HASH + export PROFILE VARIANT LINK REMAINING_ARGS=("$@") } @@ -695,7 +677,7 @@ package_command() { echo "Note: Building DMG via nix-shell to allow macOS system tools (cargo-packager path)." # shellcheck disable=SC2086 nix-shell -I "nixpkgs=${PIN_URL}" $KEEP_VARS --argstr variant "$VARIANT" "$REPO_ROOT/shell.nix" \ - --run "ENFORCE_DETERMINISTIC_HASH='${ENFORCE_DETERMINISTIC_HASH}' bash '$SCRIPT' --variant '$VARIANT' --link '$LINK' --enforce-deterministic-hash '${ENFORCE_DETERMINISTIC_HASH}'" + --run "bash '$SCRIPT' --variant '$VARIANT' --link '$LINK'" OUT_DIR="$REPO_ROOT/result-dmg-$VARIANT-$LINK" dmg_file=$(find "$OUT_DIR" -maxdepth 1 -type f -name '*.dmg' | head -n1 || true) if [ -n "${dmg_file:-}" ] && [ -f "$dmg_file" ]; then @@ -750,7 +732,7 @@ package_command() { echo "Missing $SCRIPT_LINUX" >&2 exit 1 } - nix-shell -I "nixpkgs=${NIXPKGS_ARG}" -p curl --run "ENFORCE_DETERMINISTIC_HASH='${ENFORCE_DETERMINISTIC_HASH}' bash '$SCRIPT_LINUX' --variant '$BUILD_VARIANT' --link '$BUILD_LINK' --enforce-deterministic-hash '${ENFORCE_DETERMINISTIC_HASH}'" + nix-shell -I "nixpkgs=${NIXPKGS_ARG}" -p curl --run "bash '$SCRIPT_LINUX' --variant '$BUILD_VARIANT' --link '$BUILD_LINK'" REAL_OUT="$REPO_ROOT/result-deb-$BUILD_VARIANT-$BUILD_LINK" echo "Built deb ($BUILD_VARIANT-$BUILD_LINK): $REAL_OUT" @@ -783,7 +765,7 @@ package_command() { echo "Missing $SCRIPT_LINUX" >&2 exit 1 } - nix-shell -I "nixpkgs=${NIXPKGS_ARG}" -p curl --run "ENFORCE_DETERMINISTIC_HASH='${ENFORCE_DETERMINISTIC_HASH}' bash '$SCRIPT_LINUX' --variant '$BUILD_VARIANT' --link '$BUILD_LINK' --enforce-deterministic-hash '${ENFORCE_DETERMINISTIC_HASH}'" + nix-shell -I "nixpkgs=${NIXPKGS_ARG}" -p curl --run "bash '$SCRIPT_LINUX' --variant '$BUILD_VARIANT' --link '$BUILD_LINK'" REAL_OUT="$REPO_ROOT/result-rpm-$BUILD_VARIANT-$BUILD_LINK" echo "Built rpm ($BUILD_VARIANT-$BUILD_LINK): $REAL_OUT" @@ -822,7 +804,7 @@ package_command() { ATTR="kms-server-${BUILD_VARIANT}-dmg" OUT_LINK="$REPO_ROOT/result-dmg-$BUILD_VARIANT-$BUILD_LINK" fi - nix-build -I "nixpkgs=${NIXPKGS_ARG}" --arg enforceDeterministicHash "$ENFORCE_DETERMINISTIC_HASH" "$REPO_ROOT/default.nix" -A "$ATTR" -o "$OUT_LINK" + nix-build -I "nixpkgs=${NIXPKGS_ARG}" "$REPO_ROOT/default.nix" -A "$ATTR" -o "$OUT_LINK" REAL_OUT=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") echo "Built dmg ($BUILD_VARIANT-$BUILD_LINK): $REAL_OUT" diff --git a/.github/scripts/smoke_test_dmg.sh b/.github/scripts/smoke_test_dmg.sh index fbb2dcbd09..c4388e88f6 100644 --- a/.github/scripts/smoke_test_dmg.sh +++ b/.github/scripts/smoke_test_dmg.sh @@ -161,10 +161,26 @@ if [ "$IS_FIPS" = true ]; then ENV_OPENSSL_MODULES="$CHECK_DIR/usr/local/cosmian/lib/ossl-modules" fi +# For non-FIPS builds, set OPENSSL_MODULES to point to bundled provider modules +# so the legacy provider can be loaded during smoke test execution. +if [ "$IS_FIPS" != true ]; then + NON_FIPS_OSSL_MODULES="$CHECK_DIR/usr/local/cosmian/lib/ossl-modules" + if [ -d "$NON_FIPS_OSSL_MODULES" ]; then + ENV_OPENSSL_MODULES="$NON_FIPS_OSSL_MODULES" + fi + NON_FIPS_OSSL_CONF="$CHECK_DIR/usr/local/cosmian/lib/ssl/openssl.cnf" + if [ -f "$NON_FIPS_OSSL_CONF" ]; then + ENV_OPENSSL_CONF="$NON_FIPS_OSSL_CONF" + fi +fi + # Use `env` to set variables for the run CMD=("$BINARY_PATH" --version) -if [ "$IS_FIPS" = true ]; then - VERSION_OUTPUT=$(env OPENSSL_CONF="$ENV_OPENSSL_CONF" OPENSSL_MODULES="$ENV_OPENSSL_MODULES" "${CMD[@]}" 2>&1 || true) +if [ -n "$ENV_OPENSSL_CONF" ] || [ -n "$ENV_OPENSSL_MODULES" ]; then + ENV_ARGS=() + [ -n "$ENV_OPENSSL_CONF" ] && ENV_ARGS+=(OPENSSL_CONF="$ENV_OPENSSL_CONF") + [ -n "$ENV_OPENSSL_MODULES" ] && ENV_ARGS+=(OPENSSL_MODULES="$ENV_OPENSSL_MODULES") + VERSION_OUTPUT=$(env "${ENV_ARGS[@]}" "${CMD[@]}" 2>&1 || true) else VERSION_OUTPUT=$("${CMD[@]}" 2>&1 || true) fi @@ -183,8 +199,11 @@ info "\xe2\x9c\x93 Binary executed successfully" # - FIPS dynamic builds bundle 3.1.2 runtime libs to match the FIPS provider EXPECTED_VER="3.6.0" info "Verifying OpenSSL runtime version (expected ${EXPECTED_VER})…" -if [ "$IS_FIPS" = true ]; then - INFO_CMD=(env OPENSSL_CONF="$ENV_OPENSSL_CONF" OPENSSL_MODULES="$ENV_OPENSSL_MODULES" "$BINARY_PATH" --info) +if [ -n "$ENV_OPENSSL_CONF" ] || [ -n "$ENV_OPENSSL_MODULES" ]; then + INFO_CMD=(env) + [ -n "$ENV_OPENSSL_CONF" ] && INFO_CMD+=(OPENSSL_CONF="$ENV_OPENSSL_CONF") + [ -n "$ENV_OPENSSL_MODULES" ] && INFO_CMD+=(OPENSSL_MODULES="$ENV_OPENSSL_MODULES") + INFO_CMD+=("$BINARY_PATH" --info) else INFO_CMD=("$BINARY_PATH" --info) fi 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_otel_export.sh b/.github/scripts/test_otel_export.sh index 0e479e1cfb..f5fd101c2e 100755 --- a/.github/scripts/test_otel_export.sh +++ b/.github/scripts/test_otel_export.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -euo pipefail - SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) source "$SCRIPT_DIR/common.sh" 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/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 8891dd4d6b..02dc422473 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -8,10 +8,6 @@ on: required: true type: string default: 1.90.0 - enforceDeterministicHash: - required: false - type: boolean - default: false jobs: windows-package: @@ -57,7 +53,7 @@ jobs: - name: Package with GPG signature run: | - bash .github/scripts/nix.sh --profile release --variant ${{ matrix.features }} --link ${{ matrix.link }} --enforce-deterministic-hash ${{ inputs.enforceDeterministicHash }} package + bash .github/scripts/nix.sh --profile release --variant ${{ matrix.features }} --link ${{ matrix.link }} package env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 78341061af..77db0813a5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,5 +14,3 @@ jobs: secrets: inherit with: toolchain: 1.90.0 - enforceDeterministicHash: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.head_ref, - 'release/') || startsWith(github.base_ref, 'release/') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8ceffd3e..6973793db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ 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 + +- Non-FIPS Nix Linux builds are now bit-for-bit reproducible (`nix-build --check` passes for all four Linux variants: FIPS/non-FIPS Γ— static/dynamic OpenSSL): + - Removed `${toString ../.}` from RUSTFLAGS `-C remap-path-prefix` β€” it embedded the machine-specific workspace path into the derivation, causing cross-machine hash divergence. + - Added `-C strip=symbols` and `-C symbol-mangling-version=v0` to strip residual host-path artefacts from symbol tables. + - Scrub the Nix-store path from OpenSSL's `buildinf.h` at build time so the OpenSSL derivation hash is identical across machines. +- Pin all `builtins.fetchTarball` calls in `default.nix` with explicit `sha256` hashes (nixpkgs 24.11, rust-overlay, nixpkgs 22.05) β€” eliminates Nix-version-sensitive evaluation impurity and removes the `NIXPKGS_GLIBC_234_URL` environment variable override. +- Non-FIPS Docker image now ships OpenSSL 3.6.0 provider modules (`legacy.so`, `openssl.cnf`) and sets `OPENSSL_CONF`/`OPENSSL_MODULES` environment variables, matching the FIPS image layout. +- macOS packaging fixes in `nix/scripts/package_dmg.sh` and related CI scripts. ## [5.16.0] - 2026-02-04 diff --git a/Cargo.lock b/Cargo.lock index 5ba406d414..5a8fbc4001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1327,6 +1327,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/default.nix b/default.nix index a0c6304bef..9a43530480 100644 --- a/default.nix +++ b/default.nix @@ -4,14 +4,12 @@ pkgs ? let nixpkgsSrc = builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/24.11.tar.gz"; + # Use an immutable commit tarball so builds are deterministic across machines. + url = "https://github.com/NixOS/nixpkgs/archive/8b27c1239e5c421a2bbc2c65d52e4a6fbf2ff296.tar.gz"; + sha256 = "sha256-CqCX4JG7UiHvkrBTpYC3wcEurvbtTADLbo3Ns2CEoL8="; }; in import nixpkgsSrc { config.allowUnfree = true; }, - # Allow callers (e.g., Docker) to toggle deterministic hash enforcement. - # Default is relaxed (false) so builds don't fail when hashes drift; CI/scripts - # can enable it explicitly when needed. - enforceDeterministicHash ? false, }: let @@ -72,12 +70,16 @@ let # Reuse the same pinned nixpkgs for internal imports/overlays # Reuse the same pinned nixpkgs; Linux builds target glibc 2.34 compatibility. nixpkgsSrc = builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/24.11.tar.gz"; + # Use an immutable commit tarball so builds are deterministic across machines. + url = "https://github.com/NixOS/nixpkgs/archive/8b27c1239e5c421a2bbc2c65d52e4a6fbf2ff296.tar.gz"; + sha256 = "sha256-CqCX4JG7UiHvkrBTpYC3wcEurvbtTADLbo3Ns2CEoL8="; }; # Bring a modern Rust toolchain (1.90.0) via oxalica/rust-overlay for Cargo edition2024 support rustOverlay = import ( builtins.fetchTarball { - url = "https://github.com/oxalica/rust-overlay/archive/refs/heads/master.tar.gz"; + # Pin rust-overlay to an immutable commit (master is moving). + url = "https://github.com/oxalica/rust-overlay/archive/23dd7fa91602a68bd04847ac41bc10af1e6e2fd2.tar.gz"; + sha256 = "sha256-KvmjUeA7uODwzbcQoN/B8DCZIbhT/Q/uErF1BBMcYnw="; } ); pkgsWithRust = import nixpkgsSrc { @@ -93,24 +95,15 @@ let targets = [ "wasm32-unknown-unknown" ]; }; - # For Linux, we need GLIBC <= 2.34 to support Rocky Linux 9. - # Import nixpkgs 22.05 to get its stdenv (glibc 2.34) while still using a modern - # Rust toolchain (1.90.0) from rust-overlay. + # For Linux, pin nixpkgs 22.05 (glibc 2.34) to get its stdenv while using a modern + # Rust toolchain (1.90.0) from rust-overlay. Rocky Linux 9 compatibility requires GLIBC <= 2.34. + # Hardcoded URL+hash for full determinism β€” override via `--arg pkgs234 ...` if needed. pkgs234 = if pkgs.stdenv.isLinux then - ( - let - nixpkgs2205 = builtins.getEnv "NIXPKGS_GLIBC_234_URL"; - in - import (builtins.fetchTarball { - url = - if nixpkgs2205 != "" then - nixpkgs2205 - else - # Pin to 22.05 stable tag tarball (glibc 2.34 for Rocky Linux 9 compatibility) - "https://github.com/NixOS/nixpkgs/archive/nixos-22.05.tar.gz"; - }) { config.allowUnfree = true; } - ) + import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/380be19fbd2d9079f677978361792cb25e8a3635.tar.gz"; + sha256 = "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ="; + }) { config.allowUnfree = true; } else pkgs; @@ -135,6 +128,18 @@ let # Default to static for backward compatibility openssl312 = openssl312-static; + # Build OpenSSL 3.6.0 with legacy provider for non-FIPS builds + # Common parameters for both static and dynamic builds + openssl36Args = { + 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"; + }; + openssl36-static = pkgs234.callPackage ./nix/openssl.nix (openssl36Args // { static = true; }); + openssl36-dynamic = pkgs234.callPackage ./nix/openssl.nix (openssl36Args // { static = false; }); + # Tool: cargo-generate-rpm (not available in some nixpkgs pins). Build it from crates.io. # Build cargo-generate-rpm with the same modern Rust toolchain (Cargo 1.90) # to support lockfile v4 and avoid -Znext-lockfile-bump issues @@ -191,13 +196,13 @@ let ui-fips = pkgs.callPackage ./nix/ui.nix { features = [ ]; version = kmsVersion; - inherit rustToolchain enforceDeterministicHash; + inherit rustToolchain; }; ui-non-fips = pkgs.callPackage ./nix/ui.nix { features = [ "non-fips" ]; version = kmsVersion; - inherit rustToolchain enforceDeterministicHash; + inherit rustToolchain; }; # DRY helper to build servers for both variants and both linkage modes @@ -206,10 +211,10 @@ let features, ui, static ? true, - enforceDeterministicHash ? true, }: pkgs.callPackage ./nix/kms-server.nix { openssl312 = if static then openssl312-static else openssl312-dynamic; + openssl36 = if static then openssl36-static else openssl36-dynamic; inherit pkgs234; rustPlatform = rustPlatform190; version = kmsVersion; @@ -217,7 +222,6 @@ let features ui static - enforceDeterministicHash ; }; @@ -226,14 +230,12 @@ let features = [ ]; ui = ui-fips; static = true; - inherit enforceDeterministicHash; }; kms-server-non-fips-static-openssl = mkKmsServer { features = [ "non-fips" ]; ui = ui-non-fips; static = true; - enforceDeterministicHash = false; }; # Build KMS server with dynamic OpenSSL linking @@ -241,14 +243,12 @@ let features = [ ]; ui = ui-fips; static = false; - enforceDeterministicHash = false; }; kms-server-non-fips-dynamic-openssl = mkKmsServer { features = [ "non-fips" ]; ui = ui-non-fips; static = false; - enforceDeterministicHash = false; }; # Docker images using dockerTools (minimal images) @@ -263,7 +263,8 @@ let kmsServer = kms-server-non-fips-static-openssl; variant = "non-fips"; version = kmsVersion; - # non-FIPS image doesn't require a specific OpenSSL derivation for provider config + # Provide OpenSSL 3.6.0 so the Docker image ships legacy provider + non-FIPS openssl.cnf + opensslDrv = openssl36-static; }; in @@ -288,6 +289,9 @@ rec { # Export OpenSSL 3.1.2 FIPS derivations for tooling (packaging script) inherit openssl312 openssl312-static openssl312-dynamic; + # Export OpenSSL 3.6.0 derivations (with legacy provider for non-FIPS) + inherit openssl36-static openssl36-dynamic; + # Export cargo-packager and cargo-generate-rpm tools for scripts and dev shell inherit cargoPackagerTool cargoGenerateRpmTool; @@ -396,4 +400,34 @@ rec { printf '%s\n' "$hash" >"$out/${name}" ''; + # Expected hash for OpenSSL legacy provider module + expected-hash-openssl-legacy-provider = + let + sys = pkgs.stdenv.hostPlatform.system; + parts = pkgs.lib.splitString "-" sys; + arch = builtins.elemAt parts 0; + os = builtins.elemAt parts 1; + name = "openssl-legacy-provider.3.6.0.${arch}.${os}.sha256"; + soExt = if pkgs.stdenv.isDarwin then "dylib" else "so"; + in + pkgs.runCommand "expected-hash-openssl-legacy-provider" + { + buildInputs = [ + pkgs.openssl + pkgs.coreutils + ]; + } + '' + set -euo pipefail + mkdir -p "$out" + legacy='${openssl36-static}/lib/ossl-modules/legacy.${soExt}' + if [ -f "$legacy" ]; then + hash="$(${pkgs.openssl}/bin/openssl dgst -sha256 -r "$legacy" | awk '{print $1}')" + printf '%s\n' "$hash" >"$out/${name}" + else + echo "Legacy provider not found at $legacy" >&2 + exit 1 + fi + ''; + } diff --git a/nix/README.md b/nix/README.md index 462543f710..4d2434c312 100644 --- a/nix/README.md +++ b/nix/README.md @@ -22,7 +22,7 @@ This directory contains the reproducible Nix derivations and helper scripts used β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Cargo β”‚ β”‚ OpenSSL β”‚ β”‚ Rust β”‚ - β”‚ Hash β”‚ β”‚ 3.1.2 β”‚ β”‚ 1.90.0 β”‚ + β”‚ Hash β”‚ β”‚ 3.6.0 β”‚ β”‚ 1.90.0 β”‚ β”‚ Verify β”‚ β”‚ Build β”‚ β”‚Toolchain β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ @@ -51,8 +51,8 @@ This directory contains the reproducible Nix derivations and helper scripts used β”‚ FIPS β”‚ β”‚ non-FIPS β”‚ β”‚ Variant β”‚ β”‚ Variant β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ Bit-for-bit β”‚ β”‚ Hash tracked β”‚ - β”‚reproducible β”‚ β”‚(consistency) β”‚ + β”‚ Bit-for-bit β”‚ β”‚ Bit-for-bit β”‚ + β”‚reproducible β”‚ β”‚reproducible β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -80,17 +80,17 @@ This directory contains the reproducible Nix derivations and helper scripts used - [Research \& Academia](#research--academia) - [Government \& High-Assurance](#government--high-assurance) - [Why Nix Matters for Cosmian KMS](#why-nix-matters-for-cosmian-kms) - - [Reproducible FIPS Builds](#reproducible-fips-builds) + - [Reproducible Builds](#reproducible-builds) - [Dependency Transparency](#dependency-transparency) - [Offline Air-Gapped Builds](#offline-air-gapped-builds) - [Build reproducibility foundations](#build-reproducibility-foundations) - - [How reproducible builds work (FIPS only)](#how-reproducible-builds-work-fips-only) + - [How reproducible builds work](#how-reproducible-builds-work) - [Reproducibility Architecture Diagram](#reproducibility-architecture-diagram) - [Build hash inventory](#build-hash-inventory) - [Hash verification flow](#hash-verification-flow) - [Hash Verification Details](#hash-verification-details) - [Native hash verification (installCheckPhase)](#native-hash-verification-installcheckphase) - - [Proving determinism locally (FIPS builds only)](#proving-determinism-locally-fips-builds-only) + - [Proving determinism locally](#proving-determinism-locally) - [Unified \& idempotent packaging](#unified--idempotent-packaging) - [Offline packaging flow](#offline-packaging-flow) - [Offline Build Visual Flow](#offline-build-visual-flow) @@ -241,20 +241,26 @@ This purely functional approach means: ### Why Nix Matters for Cosmian KMS -#### Reproducible FIPS Builds +#### Reproducible Builds + +Both FIPS and non-FIPS Linux builds are **bit-for-bit deterministic**: ```bash # Developer build on laptop (Linux x86_64) nix-build -A kms-server-fips-static-openssl -o result-server-fips -# SHA256: abc123... +# SHA256: 528e0f20... # CI build on GitHub Actions (same platform) nix-build -A kms-server-fips-static-openssl -o result-server-fips -# SHA256: abc123... βœ… IDENTICAL +# SHA256: 528e0f20... βœ… IDENTICAL + +# Non-FIPS builds are also deterministic +nix-build -A kms-server-non-fips-static-openssl -o result-server-non-fips +# SHA256: a921942f... βœ… REPRODUCIBLE # Security team rebuild 6 months later (same commit) nix-build -A kms-server-fips-static-openssl -o result-server-fips -# SHA256: abc123... βœ… STILL IDENTICAL +# SHA256: 528e0f20... βœ… STILL IDENTICAL ``` This **bit-for-bit reproducibility** is essential for: @@ -315,7 +321,7 @@ Critical for: Goals: -- **Bit-for-bit deterministic FIPS builds** on Linux (non-FIPS builds are consistent but not fully deterministic) +- **Bit-for-bit deterministic builds** on Linux (both FIPS and non-FIPS) - Native hash verification inside the Nix derivation (installCheckPhase) - Fully offline packaging after first prewarm - Idempotent repeated packaging (no rebuild/download) via reuse & NO_PREWARM @@ -326,34 +332,35 @@ Goals: ## Build reproducibility foundations -### How reproducible builds work (FIPS only) +### How reproducible builds work -**IMPORTANT**: Only FIPS builds on Linux achieve bit-for-bit deterministic reproducibility. Non-FIPS builds use hash verification for consistency tracking but may not be fully reproducible across different build environments. +All Linux builds (FIPS and non-FIPS) achieve bit-for-bit deterministic reproducibility. -`nix/kms-server.nix` builds FIPS binaries inside a hermetic, pinned environment with controlled inputs: +`nix/kms-server.nix` builds binaries inside a hermetic, pinned environment with controlled inputs: 1. **Pinned nixpkgs (24.11)**: Frozen package set prevents upstream drift (Linux builds target glibc 2.34) 2. **Source cleaning**: `cleanSourceWith` removes non-input artifacts (`result-*`, reports, caches) 3. **Locked dependencies**: Cargo dependency graph frozen via `cargoHash` (reproducible vendoring) -4. **Deterministic compilation flags**: Rust codegen flags minimize non-determinism (FIPS builds): +4. **Deterministic compilation flags**: Rust codegen flags eliminate non-determinism: - `-Cdebuginfo=0` β€” No debug symbols (timestamps, paths) - `-Ccodegen-units=1` β€” Single codegen unit (deterministic order) - `-Cincremental=false` β€” No incremental compilation cache - `-C link-arg=-Wl,--build-id=none` β€” No build-id section + - `-C strip=symbols` β€” Strip all symbols + - `-C symbol-mangling-version=v0` β€” Stable symbol mangling - `SOURCE_DATE_EPOCH` β€” Normalized embedded timestamps -5. **Pinned OpenSSL 3.6.0 (runtime) + 3.1.2 (FIPS provider)**: Local tarball or fetched by SRI hash (FIPS 140-3 certified) +5. **Pinned OpenSSL 3.6.0 (runtime) + 3.1.2 (FIPS provider)**: Fetched by SRI hash (FIPS 140-3 certified) - Note: OpenSSL 3.1.2 is kept for the FIPS provider. 6. **Sanitized binaries**: RPATH removed, interpreter fixed to avoid volatile store paths +7. **No host-path leakage**: Build uses only `/build` and `/tmp` remap prefixes (no workspace paths in derivation) -**Result for FIPS builds**: Identical inputs β‡’ identical binary hash. Hash drift always means an intentional or accidental input change. - -**Result for non-FIPS builds**: Builds are tracked with expected hashes for consistency, but the binaries may vary across different build environments due to non-deterministic compilation of non-FIPS cryptographic components. +**Result**: Identical inputs β‡’ identical binary hash. Hash drift always means an intentional or accidental input change. ### Reproducibility Architecture Diagram ```text β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Deterministic Build Architecture (FIPS) β”‚ +β”‚ Deterministic Build Architecture β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ INPUT LAYER (All Cryptographically Pinned) @@ -383,10 +390,11 @@ INPUT LAYER (All Cryptographically Pinned) β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ OpenSSL 3.1.2 Source β”‚ -β”‚ β€’ Hash: sha256-def456... (openssl-3.1.2.tar.gz) β”‚ -β”‚ β€’ FIPS 140-3 certified source code β”‚ -β”‚ β€’ Local tarball fallback (resources/tarballs/) β”‚ +β”‚ OpenSSL 3.6.0 + 3.1.2 Source β”‚ +β”‚ β€’ OpenSSL 3.6.0: runtime library (statically linked) β”‚ +β”‚ β€’ OpenSSL 3.1.2: FIPS provider (shipped separately) β”‚ +β”‚ β€’ Both verified by SRI hash β”‚ +β”‚ β€’ FIPS 140-3 certified source code β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό @@ -412,7 +420,7 @@ BUILD LAYER (Hermetic Execution) β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Deterministic Compilation (FIPS Only) β”‚ +β”‚ Deterministic Compilation β”‚ β”‚ β”‚ β”‚ Flags preventing non-determinism: β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ @@ -420,17 +428,18 @@ BUILD LAYER (Hermetic Execution) β”‚ β”‚ -Ccodegen-units=1 Single codegen (deterministic order)β”‚ β”‚ β”‚ β”‚ -Cincremental=false No incremental cache β”‚ β”‚ β”‚ β”‚ -Clink-arg=-Wl,--build-id=none No build timestamp β”‚ β”‚ +β”‚ β”‚ -Cstrip=symbols Strip all symbols β”‚ β”‚ +β”‚ β”‚ -Csymbol-mangling-version=v0 Stable mangling β”‚ β”‚ β”‚ β”‚ SOURCE_DATE_EPOCH=1 Normalized embedded times β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ -β”‚ Non-FIPS builds: Flags relaxed for performance β”‚ -β”‚ (may introduce non-determinism) β”‚ +β”‚ Applied to all Linux builds (FIPS and non-FIPS) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Static Linking β”‚ -β”‚ β€’ OpenSSL 3.1.2 statically linked (no .so dependency) β”‚ +β”‚ β€’ OpenSSL 3.6.0 statically linked (no .so dependency) β”‚ β”‚ β€’ GLIBC dynamically linked (version ≀ 2.34 for Rocky Linux 9 compatibility) β”‚ β”‚ β€’ No RPATH (would contain /nix/store paths) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ @@ -455,15 +464,15 @@ OUTPUT LAYER (Hash Verification) β”‚ Computed: sha256($out/bin/cosmian_kms) β”‚ β”‚ Expected: nix/expected-hashes/....sha256 β”‚ β”‚ β”‚ -β”‚ FIPS on Linux: β”‚ +β”‚ Linux (FIPS and non-FIPS): β”‚ β”‚ βœ… Hashes MUST match (bit-for-bit deterministic) β”‚ β”‚ ❌ Mismatch = BUILD FAILS (potential tampering/drift) β”‚ β”‚ β”‚ -β”‚ Non-FIPS or macOS: β”‚ +β”‚ macOS: β”‚ β”‚ ⚠️ Hashes tracked for consistency (not guaranteed reproducible) β”‚ β”‚ β”‚ β”‚ Additional checks: β”‚ -β”‚ β€’ OpenSSL version exactly 3.1.2 β”‚ +β”‚ β€’ OpenSSL 3.6.0 statically linked (strings check) β”‚ β”‚ β€’ ldd shows no libssl.so (static linkage) β”‚ β”‚ β€’ GLIBC symbols ≀ 2.34 β”‚ β”‚ β€’ FIPS mode operational (if FIPS variant) β”‚ @@ -475,8 +484,8 @@ OUTPUT LAYER (Hash Verification) β”‚ /nix/store/-cosmian-kms-server/bin/cosmian_kms β”‚ β”‚ β”‚ β”‚ Properties: β”‚ -β”‚ β€’ Hash-verified (FIPS: bit-for-bit reproducible) β”‚ -β”‚ β€’ Statically linked OpenSSL β”‚ +β”‚ β€’ Hash-verified (bit-for-bit reproducible on Linux) β”‚ +β”‚ β€’ OpenSSL 3.6.0 statically linked β”‚ β”‚ β€’ Portable across Linux distributions (GLIBC β‰₯ 2.34, Rocky Linux 9+) β”‚ β”‚ β€’ No /nix/store runtime dependencies β”‚ β”‚ β€’ Ready for packaging (DEB/RPM/DMG) β”‚ @@ -493,29 +502,30 @@ REPRODUCIBILITY GUARANTEES β”‚ β”‚ Same inputs β†’ IDENTICAL binary hash β”‚ β”‚ β”‚ Cryptographically verifiable β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Linux ARM64 FIPS β”‚ βœ… Bit-for-bit deterministic β”‚ -β”‚ β”‚ (cross-compilation from x86_64) β”‚ +β”‚ Linux x86_64 β”‚ βœ… Bit-for-bit deterministic β”‚ +β”‚ non-FIPS β”‚ Same inputs β†’ IDENTICAL binary hash β”‚ +β”‚ β”‚ Cryptographically verifiable β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Linux x86_64 β”‚ ⚠️ Hash tracked (consistency monitoring) β”‚ -β”‚ non-FIPS β”‚ May vary across environments β”‚ -β”‚ β”‚ Not guaranteed reproducible β”‚ +β”‚ Linux ARM64 β”‚ βœ… Bit-for-bit deterministic β”‚ +β”‚ (any variant) β”‚ (cross-compilation from x86_64) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ macOS ARM64 β”‚ ⚠️ Hash tracked (consistency monitoring) β”‚ β”‚ (any variant) β”‚ macOS toolchain introduces variance β”‚ β”‚ β”‚ Not bit-for-bit reproducible β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -Why FIPS builds are reproducible: +Why Linux builds are reproducible (FIPS and non-FIPS): 1. Deterministic compilation flags (no debug info, single codegen unit) - 2. Normalized timestamps (SOURCE_DATE_EPOCH) - 3. No build-id section in binary - 4. Cleaned source tree (no artifacts) - 5. All inputs cryptographically pinned + 2. Symbol stripping and stable mangling (-Cstrip=symbols, -Csymbol-mangling-version=v0) + 3. Normalized timestamps (SOURCE_DATE_EPOCH) + 4. No build-id section in binary + 5. Cleaned source tree (no artifacts) + 6. All inputs cryptographically pinned (nixpkgs, rust-overlay, OpenSSL tarballs) + 7. No host-path leakage in derivation inputs -Why non-FIPS builds may vary: - 1. Relaxed compilation flags (performance optimization) - 2. Non-deterministic cryptographic backend components - 3. Platform-specific optimizations +Why macOS builds may vary: + 1. macOS toolchain introduces non-deterministic artifacts + 2. Platform-specific optimizations Use case: FIPS for compliance/audits, non-FIPS for general deployment ``` @@ -528,17 +538,17 @@ Cargo/UI vendor hashes are committed in the repository and verified during build | --------------------- | ------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------ | | **Cargo vendor** | Reproducible Rust dependencies | `nix/kms-server.nix` | `sha256-NAy4vNoW7nkqJF263FkkEvAh1bMMDJkL0poxBzXFOO8=` | | **OpenSSL sources** | OpenSSL 3.6.0 (runtime) + OpenSSL 3.1.2 (FIPS provider) | `nix/kms-server.nix` + `nix/openssl.nix` | `sha256-tqX0S362nj+jXb8VUkQFtEg3pIHUPYHa3d4/8h/LuOk=` | -| **Binary (FIPS)** | Deterministic FIPS server executable | `nix/expected-hashes/cosmian-kms-server.fips.static-openssl.x86_64.linux.sha256` | `90eb9f3bd0d58c521ea68dfa205bdcc6c34b4064198c9fbb51f4d753df16e1f1` | -| **Binary (non-FIPS)** | Non-FIPS server (tracked hash, not fully deterministic) | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.x86_64.linux.sha256` | `2eb034667cde901bb85b195d58b48a32ff4028f785bd977acdb689ea42268f1b` | +| **Binary (FIPS)** | Deterministic FIPS server executable | `nix/expected-hashes/cosmian-kms-server.fips.static-openssl.x86_64.linux.sha256` | `528e0f2019769afb8016bb822f640b2b8b5c5711a0e13f59062c84f9b772bed6` | +| **Binary (non-FIPS)** | Deterministic non-FIPS server executable | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.x86_64.linux.sha256` | `a921942fd81bedca3438789be5580bde794d5569ce3e955f692d44391f99ff02` | Platform-specific binary hashes: | Platform | Variant | Hash File | Enforced At | Deterministic? | | -------------- | -------- | ------------------------------------------------------------ | -------------------- | ------------------------------ | | x86_64-linux | FIPS | `nix/expected-hashes/cosmian-kms-server.fips.static-openssl.x86_64.linux.sha256` | `installCheckPhase` | βœ… Yes (bit-for-bit) | -| x86_64-linux | non-FIPS | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.x86_64.linux.sha256` | `installCheckPhase` | ⚠️ No (tracked for consistency) | +| x86_64-linux | non-FIPS | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.x86_64.linux.sha256` | `installCheckPhase` | βœ… Yes (bit-for-bit) | | aarch64-linux | FIPS | `nix/expected-hashes/cosmian-kms-server.fips.static-openssl.aarch64.linux.sha256` | `installCheckPhase` | βœ… Yes (bit-for-bit) | -| aarch64-linux | non-FIPS | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.aarch64.linux.sha256` | `installCheckPhase` | ⚠️ No (tracked for consistency) | +| aarch64-linux | non-FIPS | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.aarch64.linux.sha256` | `installCheckPhase` | βœ… Yes (bit-for-bit) | | aarch64-darwin | FIPS | `nix/expected-hashes/cosmian-kms-server.fips.static-openssl.aarch64.darwin.sha256` | Not enforced (macOS) | ⚠️ No (macOS builds) | | aarch64-darwin | non-FIPS | `nix/expected-hashes/cosmian-kms-server.non-fips.static-openssl.aarch64.darwin.sha256` | Not enforced (macOS) | ⚠️ No (macOS builds) | @@ -547,7 +557,7 @@ Platform-specific binary hashes: - The Cargo vendor hash may differ between macOS and Linux due to platform-specific dependencies - OpenSSL and binary hashes are platform-specific by design - Expected-binary-hash enforcement is opt-in (via `enforceDeterministicHash`) and only runs on Linux -- **Only FIPS builds on Linux are bit-for-bit deterministic**; non-FIPS hashes are tracked for build consistency but not reproducibility guarantees +- **All Linux builds (FIPS and non-FIPS) are bit-for-bit deterministic**; macOS hashes are tracked for consistency but not reproducibility guarantees ### Hash verification flow @@ -581,13 +591,14 @@ During the build process, Nix enforces all hashes at multiple stages: β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Step 4: Compilation (deterministic for FIPS only) β”‚ +β”‚ Step 4: Compilation (deterministic) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β€’ Flags: -Cdebuginfo=0 -Ccodegen-units=1 -Cincremental=false β”‚ -β”‚ β€’ Static OpenSSL linkage (no dynamic deps) β”‚ +β”‚ β€’ Additional: -Cstrip=symbols -Csymbol-mangling-version=v0 β”‚ +β”‚ β€’ Static OpenSSL 3.6.0 linkage (no dynamic deps) β”‚ β”‚ β€’ SOURCE_DATE_EPOCH for normalized timestamps β”‚ β”‚ β€’ Build cosmian_kms binary β”‚ -β”‚ β€’ Note: Non-FIPS builds may have non-deterministic artifacts β”‚ +β”‚ β€’ Same flags applied to FIPS and non-FIPS β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” @@ -602,7 +613,7 @@ During the build process, Nix enforces all hashes at multiple stages: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 6: Runtime Validation β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β€’ Assert: OpenSSL version = 3.1.2 β”‚ +β”‚ β€’ Assert: OpenSSL 3.6.0 statically linked (strings check) β”‚ β”‚ β€’ Assert: Static linkage (no libssl.so) β”‚ β”‚ β€’ Assert: GLIBC symbols ≀ 2.34 β”‚ β”‚ β€’ Assert: FIPS mode if variant=fips β”‚ @@ -614,8 +625,7 @@ During the build process, Nix enforces all hashes at multiple stages: β”‚ Output: Hash-Verified Binary β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ result-server-/bin/cosmian_kms β”‚ -β”‚ β€’ FIPS: Deterministically reproducible (bit-for-bit) β”‚ -β”‚ β€’ Non-FIPS: Hash verified for consistency (not reproducible) β”‚ +β”‚ β€’ Deterministically reproducible (bit-for-bit) on Linux β”‚ β”‚ β€’ Ready for packaging (DEB/RPM/DMG) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -638,9 +648,9 @@ Layer 1: Cargo Dependencies β–Ό Layer 2: System Dependencies β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ OpenSSL 3.1.2 tarball hash β”‚ -β”‚ β”œβ”€ Cryptographic verification of openssl-3.1.2.tar.gz β”‚ -β”‚ β”œβ”€ FIPS 140-3 certified source code β”‚ +β”‚ OpenSSL 3.6.0 (runtime) + 3.1.2 (FIPS provider) tarball hashes β”‚ +β”‚ β”œβ”€ Cryptographic verification of openssl source tarballs β”‚ +β”‚ β”œβ”€ FIPS 140-3 certified source code (3.1.2 provider) β”‚ β”‚ └─ Protection: Supply chain attack on OpenSSL = immediate detection β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ @@ -648,9 +658,9 @@ Layer 2: System Dependencies Layer 3: Final Binary β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Binary hash in expected-hashes/..sha256 β”‚ -β”‚ β”œβ”€ FIPS: Bit-for-bit reproducible (Linux) β”‚ +β”‚ β”œβ”€ Linux (FIPS and non-FIPS): Bit-for-bit reproducible β”‚ β”‚ β”‚ β†’ Same source + same Nix = IDENTICAL binary β”‚ -β”‚ β”œβ”€ Non-FIPS: Hash tracking for consistency β”‚ +β”‚ β”œβ”€ macOS: Hash tracking for consistency β”‚ β”‚ β”‚ β†’ Detects unexpected changes, not guaranteed reproducible β”‚ β”‚ └─ Protection: Any tampering in build process = hash mismatch β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ @@ -705,10 +715,6 @@ Tip: for a quick end-to-end check after updates, use `bash .github/scripts/nix.s Hash enforcement is configurable: some expected-hash checks are only enforced when `enforceDeterministicHash`/`--enforce-deterministic-hash true` is enabled. -**Note on non-FIPS hashes**: Non-FIPS builds are tracked with expected hashes for consistency monitoring, -but these hashes may change across different build environments even with identical source code. Hash changes -should still be reviewed, but they don't necessarily indicate a source change for non-FIPS builds. - ## Native hash verification (installCheckPhase) During `installCheckPhase` we: @@ -734,9 +740,9 @@ nix-build -A kms-server-fips-static-openssl -o result-server-fips The `update-hashes` command is integrated into the main `nix.sh` script for convenience. -## Proving determinism locally (FIPS builds only) +## Proving determinism locally -**IMPORTANT**: Only FIPS builds on Linux are bit-for-bit deterministic. Non-FIPS builds may produce different hashes even with identical inputs. +Both FIPS and non-FIPS Linux builds are bit-for-bit deterministic. ```bash # Two identical FIPS builds - hashes MUST match @@ -745,16 +751,18 @@ nix-build -A kms-server-fips-static-openssl -o result-server-fips-2 sha256sum result-server-fips/bin/cosmian_kms result-server-fips-2/bin/cosmian_kms # Expected: Identical SHA-256 hashes -# Non-FIPS builds - hashes MAY differ across builds +# Non-FIPS builds are also deterministic - hashes MUST match nix-build -A kms-server-non-fips-static-openssl -o result-server-non-fips nix-build -A kms-server-non-fips-static-openssl -o result-server-non-fips-2 sha256sum result-server-non-fips/bin/cosmian_kms result-server-non-fips-2/bin/cosmian_kms -# Warning: Hashes may not match even with identical source -``` +# Expected: Identical SHA-256 hashes -For FIPS builds, hashes must match. To test failure path: edit one character in the expected hash file and rebuild; build must fail. Restore correct hash; build succeeds. +# You can also use nix-build --check for a quick verification +nix-build -A kms-server-fips-static-openssl --no-out-link --check +nix-build -A kms-server-non-fips-static-openssl --no-out-link --check +``` -For non-FIPS builds, hash verification ensures the binary hasn't unexpectedly changed from the last known good build, but reproducibility across different machines or environments is not guaranteed. +To test the failure path: edit one character in the expected hash file and rebuild; build must fail. Restore correct hash; build succeeds. ## Unified & idempotent packaging @@ -1077,7 +1085,7 @@ nix-build -A rustToolchain -o result-rust export PATH="$(readlink -f result-rust)/bin:$PATH" ``` -Benefits: consistent versions, no rustup downloads, contributes to build reproducibility (FIPS) and consistency (non-FIPS). +Benefits: consistent versions, no rustup downloads, contributes to build reproducibility. ## Notes @@ -1158,7 +1166,7 @@ This section documents the low-level helper scripts in `nix/scripts/` for buildi β”‚ compilation β”‚ β”‚ β€’ package_rpmβ”‚ β”‚ β€’ update_ β”‚ β”‚ β”‚ β”‚ β€’ package_dmgβ”‚ β”‚ hashes β”‚ β”‚ Static link β”‚ β”‚ β”‚ β”‚ β€’ generate_ β”‚ - β”‚ OpenSSL 3.1.2β”‚ β”‚ Common logic:β”‚ β”‚ sbom β”‚ + β”‚ OpenSSL 3.6.0β”‚ β”‚ Common logic:β”‚ β”‚ sbom β”‚ β”‚ β”‚ β”‚ package_ β”‚ β”‚ β€’ signing_keyβ”‚ β”‚ Validates: β”‚ β”‚ common.sh β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ Hash β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ @@ -1344,7 +1352,7 @@ Entry: bash nix/scripts/package_.sh --variant β”‚ --info β”‚ β”‚ 3. Verify: β”‚ β”‚ β€’ Version matches β”‚ - β”‚ β€’ OpenSSL = 3.1.2 β”‚ + β”‚ β€’ OpenSSL = 3.6.0 β”‚ β”‚ β€’ Binary runs β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ @@ -1651,7 +1659,7 @@ Understanding specific techniques used in this project: | **Hash verification** | [Native hash verification](#native-hash-verification-installcheckphase) | [Nix Manual: Fixed-output derivations](https://nixos.org/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash) | | **Offline builds** | [Offline packaging flow](#offline-packaging-flow) | [Nixpkgs: Offline evaluation](https://nixos.org/manual/nixpkgs/stable/#sec-offline-mode) | | **Static linking** | `nix/openssl.nix` | [Static binaries in Nix](https://nixos.wiki/wiki/Static_binaries) | -| **FIPS compliance** | [Proving determinism locally](#proving-determinism-locally-fips-builds-only) | [OpenSSL FIPS 140-3](https://www.openssl.org/docs/fips.html) | +| **FIPS compliance** | [Proving determinism locally](#proving-determinism-locally) | [OpenSSL FIPS 140-3](https://www.openssl.org/docs/fips.html) | ### Community Resources diff --git a/nix/docker.nix b/nix/docker.nix index b0e95d81d7..17c64fa071 100644 --- a/nix/docker.nix +++ b/nix/docker.nix @@ -326,24 +326,15 @@ pkgs.dockerTools.buildLayeredImage { # Environment variables # Ensure OpenSSL uses the packaged configuration and provider modules. - # - FIPS: use the dev/test FIPS config and packaged modules - # - non-FIPS: use the original OpenSSL config (default provider) + # Both FIPS and non-FIPS variants need OPENSSL_CONF and OPENSSL_MODULES + # to locate the correct openssl.cnf and provider modules (fips.so, legacy.so, etc.). Env = [ "PATH=/usr/local/bin:/bin:${runtimeEnv}/bin:${pkgs.busybox}/bin" "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" "TZDIR=${pkgs.tzdata}/share/zoneinfo" - ] - ++ ( - if variant == "fips" then - [ - # Reuse the original OpenSSL config shipped with the server derivation - # which includes the FIPS module configuration. - "OPENSSL_CONF=/usr/local/cosmian/lib/ssl/openssl.cnf" - "OPENSSL_MODULES=/usr/local/cosmian/lib/ossl-modules" - ] - else - [ ] - ); + "OPENSSL_CONF=/usr/local/cosmian/lib/ssl/openssl.cnf" + "OPENSSL_MODULES=/usr/local/cosmian/lib/ossl-modules" + ]; # Set working directory WorkingDir = "/var/lib/cosmian-kms"; diff --git a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 index ef949378ab..642cf2a923 100644 --- a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 @@ -1 +1 @@ -sha256-5g6xpd2j+GrT7kCTmAMGY41aFbKy9Br1C2LSDlEA9AM= +sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= diff --git a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 index ef949378ab..642cf2a923 100644 --- a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 @@ -1 +1 @@ -sha256-5g6xpd2j+GrT7kCTmAMGY41aFbKy9Br1C2LSDlEA9AM= +sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= diff --git a/nix/expected-hashes/server.vendor.static.darwin.sha256 b/nix/expected-hashes/server.vendor.static.darwin.sha256 index 47bd75a6ec..0767f4bc15 100644 --- a/nix/expected-hashes/server.vendor.static.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.static.darwin.sha256 @@ -1 +1 @@ -sha256-u9mnysm5S8TVPMk7JV6EvAZURF3XHHfn9BUgq5dLyc8= \ No newline at end of file +sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= diff --git a/nix/expected-hashes/server.vendor.static.linux.sha256 b/nix/expected-hashes/server.vendor.static.linux.sha256 index 6f01af824a..0767f4bc15 100644 --- a/nix/expected-hashes/server.vendor.static.linux.sha256 +++ b/nix/expected-hashes/server.vendor.static.linux.sha256 @@ -1 +1 @@ -sha256-u9mnysm5S8TVPMk7JV6EvAZURF3XHHfn9BUgq5dLyc8= +sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index 411fb6397d..2aacce5131 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1 @@ -sha256-Ag/Pwh3Cc7g3PlgWurmwFvTBb6VgqEPMDEJzhg7WHIM= +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 b7b119df1d..9a7544f89d 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-JcVw7igxAdQwp5Tmwc4FQPT1Ox8eZNwWdKXKcroKLJo= +sha256-vVEMcIsU6lVfVrRTtHzBRzOHNX5+3D41CdjFSOPd9oA= diff --git a/nix/kms-server.nix b/nix/kms-server.nix index 94ee18b815..80645e4909 100644 --- a/nix/kms-server.nix +++ b/nix/kms-server.nix @@ -14,11 +14,6 @@ ui ? null, # Pre-built UI derivation providing dist/ # Linkage mode: true for static OpenSSL, false for dynamic OpenSSL static ? true, - # Allow callers (e.g., Docker image build) to bypass deterministic hash - # enforcement when the container build environment cannot yet reproduce - # the committed expected hashes. Default remains strict (true) for - # packaging and CI flows. - enforceDeterministicHash ? false, }: let @@ -66,37 +61,37 @@ let # Expected deterministic sha256 of the final installed binary (cosmian_kms) # Naming convention (matches repository files): # cosmian-kms-server.....sha256 - expectedHashPath = - _unused: - let - sys = pkgs.stdenv.hostPlatform.system; # e.g., x86_64-linux - parts = lib.splitString "-" sys; - arch = builtins.elemAt parts 0; - os = builtins.elemAt parts 1; - # Match binary expected-hash file naming: static => static-openssl, dynamic => dynamic-openssl - impl = if static then "static-openssl" else "dynamic-openssl"; - file1 = ./expected-hashes + "/cosmian-kms-server.${baseVariant}.${impl}.${arch}.${os}.sha256"; - in - if builtins.pathExists file1 then file1 else null; - # Compute the actual hash file path for writing during build + # Pre-compute all platform-specific expected hash file paths at Nix evaluation time. + # If the file exists and contains a non-zero hash, it will be embedded in the + # installCheckPhase shell script for mandatory comparison. + linkTag = if static then "static-openssl" else "dynamic-openssl"; + expectedHashDir = ./expected-hashes; - # Only compute and validate expected hash path if enforcement is enabled - expectedHashPathVariant = if enforceDeterministicHash then expectedHashPath variant else null; - hasExpectedHashFile = expectedHashPathVariant != null; - # Only read the hash file if enforcement is enabled to avoid errors when file doesn't exist - expectedHashRaw = - if enforceDeterministicHash && expectedHashPathVariant != null then - builtins.readFile expectedHashPathVariant - else - ""; - sanitizeHash = - s: + # Helper: read & trim a hash file, returning null when absent or placeholder (all zeros). + readHashFile = + name: let - noWS = lib.replaceStrings [ "\n" "\r" " " "\t" ] [ "" "" "" "" ] s; + path = expectedHashDir + "/${name}"; in - lib.strings.removeSuffix "\n" noWS; - expectedHash = sanitizeHash expectedHashRaw; + if builtins.pathExists path then + let + raw = builtins.readFile path; + trimmed = lib.replaceStrings [ "\n" "\r" " " "\t" ] [ "" "" "" "" ] raw; + isPlaceholder = builtins.match "^0+$" trimmed != null; + in + if trimmed != "" && !isPlaceholder then trimmed else null + else + null; + + # Pre-read expected hashes for every arch+os combination this derivation supports. + # Only the matching platform will actually use its value at build time. + expectedHash_x86_64_linux = readHashFile "cosmian-kms-server.${baseVariant}.${linkTag}.x86_64.linux.sha256"; + expectedHash_aarch64_linux = readHashFile "cosmian-kms-server.${baseVariant}.${linkTag}.aarch64.linux.sha256"; + expectedHash_x86_64_darwin = readHashFile "cosmian-kms-server.${baseVariant}.${linkTag}.x86_64.darwin.sha256"; + expectedHash_arm64_darwin = readHashFile "cosmian-kms-server.${baseVariant}.${linkTag}.arm64.darwin.sha256"; + + # Compute the actual hash file path for writing during build # Force rebuild marker - increment to invalidate cache when only Nix expressions change rebuildMarker = "1"; @@ -200,35 +195,44 @@ let echo "ERROR: GLIBC $MAX_VER > 2.34"; exit 1; } - # Deterministic hash check - ${lib.optionalString enforceDeterministicHash '' - if [ "${if hasExpectedHashFile then "1" else "0"}" = "1" ]; then - [ -n "${expectedHash}" ] || { echo "ERROR: Expected hash file is empty" >&2; exit 1; } - ACTUAL=$(sha256sum "$BIN" | awk '{print $1}') - [ "$ACTUAL" = "${expectedHash}" ] || { - echo "ERROR: Hash mismatch. Expected ${expectedHash}, got $ACTUAL" >&2; exit 1; - } - echo "Hash OK: $ACTUAL" - else - echo "WARNING: Expected hash file missing; skipping deterministic hash check" - fi - ''} - - # Always write actual hash to output for reference/updates + # Compute actual binary hash ACTUAL=$(sha256sum "$BIN" | awk '{print $1}') echo "$ACTUAL" > "$out/bin/cosmian_kms.sha256" echo "Binary hash: $ACTUAL (saved to $out/bin/cosmian_kms.sha256)" - # Write the expected hash filename for easy copying + # Determine expected hash (resolved at Nix evaluation time from nix/expected-hashes/) ARCH_LINUX="$(uname -m)" case "$ARCH_LINUX" in x86_64) ARCH_TAG="x86_64" ;; aarch64|arm64) ARCH_TAG="aarch64" ;; *) ARCH_TAG="$ARCH_LINUX" ;; esac - HASH_FILENAME="cosmian-kms-server.${baseVariant}.${ - if static then "static-openssl" else "dynamic-openssl" - }.$ARCH_TAG.linux.sha256" + HASH_FILENAME="cosmian-kms-server.${baseVariant}.${linkTag}.$ARCH_TAG.linux.sha256" + + # Pick the expected hash for the current architecture + EXPECTED="" + case "$ARCH_LINUX" in + x86_64) EXPECTED="${toString expectedHash_x86_64_linux}" ;; + aarch64) EXPECTED="${toString expectedHash_aarch64_linux}" ;; + esac + + if [ -n "$EXPECTED" ]; then + if [ "$ACTUAL" = "$EXPECTED" ]; then + echo "Deterministic hash check PASSED: $ACTUAL" + else + echo "ERROR: Deterministic hash MISMATCH!" + echo " Expected: $EXPECTED" + echo " Actual: $ACTUAL" + echo " File: nix/expected-hashes/$HASH_FILENAME" + echo "" + echo "If this is an intentional change, update the expected hash:" + echo " echo '$ACTUAL' > nix/expected-hashes/$HASH_FILENAME" + exit 1 + fi + else + echo "NOTE: No expected hash file for $HASH_FILENAME β€” skipping enforcement (bootstrap mode)" + fi + echo "$ACTUAL" > "$out/bin/$HASH_FILENAME" echo "Expected hash file saved to: $out/bin/$HASH_FILENAME" echo "To update repository, copy this file to: nix/expected-hashes/$HASH_FILENAME" @@ -248,16 +252,39 @@ let echo "WARNING: Binary has Nix store dylib references" fi - # Always write actual hash to output for reference/updates + # Compute actual binary hash ACTUAL=$(sha256sum "$BIN" | awk '{print $1}') echo "$ACTUAL" > "$out/bin/cosmian_kms.sha256" echo "Binary hash: $ACTUAL (saved to $out/bin/cosmian_kms.sha256)" - # Write the expected hash filename for easy copying + # Determine expected hash (resolved at Nix evaluation time from nix/expected-hashes/) ARCH="$(uname -m)" - HASH_FILENAME="cosmian-kms-server.${baseVariant}.${ - if static then "static-openssl" else "dynamic-openssl" - }.$ARCH.darwin.sha256" + HASH_FILENAME="cosmian-kms-server.${baseVariant}.${linkTag}.$ARCH.darwin.sha256" + + # Pick the expected hash for the current architecture + EXPECTED="" + case "$ARCH" in + x86_64) EXPECTED="${toString expectedHash_x86_64_darwin}" ;; + arm64|aarch64) EXPECTED="${toString expectedHash_arm64_darwin}" ;; + esac + + if [ -n "$EXPECTED" ]; then + if [ "$ACTUAL" = "$EXPECTED" ]; then + echo "Deterministic hash check PASSED: $ACTUAL" + else + echo "ERROR: Deterministic hash MISMATCH!" + echo " Expected: $EXPECTED" + echo " Actual: $ACTUAL" + echo " File: nix/expected-hashes/$HASH_FILENAME" + echo "" + echo "If this is an intentional change, update the expected hash:" + echo " echo '$ACTUAL' > nix/expected-hashes/$HASH_FILENAME" + exit 1 + fi + else + echo "NOTE: No expected hash file for $HASH_FILENAME β€” skipping enforcement (bootstrap mode)" + fi + echo "$ACTUAL" > "$out/bin/$HASH_FILENAME" echo "Expected hash file saved to: $out/bin/$HASH_FILENAME" echo "To update repository, copy this file to: nix/expected-hashes/$HASH_FILENAME" @@ -289,7 +316,6 @@ rustPlatform.buildRustPackage rec { # Disable cargo-auditable wrapper; it doesn't understand edition=2024 yet auditable = false; # Run tests only for static builds (self-contained OpenSSL); dynamic builds may lack runtime libssl in sandbox - doCheck = static; # Provide the whole workspace but filtered; build only the server crate. src = filteredSrc; @@ -313,17 +339,10 @@ rustPlatform.buildRustPackage rec { raw = builtins.readFile vendorFile; trimmed = lib.replaceStrings [ "\n" "\r" " " "\t" ] [ "" "" "" "" ] raw; in - if enforceDeterministicHash then - ( - assert trimmed != placeholder && trimmed != ""; - trimmed - ) - else - trimmed - else if enforceDeterministicHash then - builtins.throw ("Expected server vendor cargo hash file not found: " + vendorFile) + assert trimmed != placeholder && trimmed != ""; + trimmed else - placeholder; + builtins.throw ("Expected server vendor cargo hash file not found: " + vendorFile); cargoSha256 = cargoHash; # Use release profile by default @@ -339,6 +358,7 @@ rustPlatform.buildRustPackage rec { ] ++ lib.optionals pkgs.stdenv.isLinux [ binutils # provides readelf and ldd used during installCheckPhase + patchelf ] ++ lib.optionals pkgs.stdenv.isDarwin [ darwin.cctools # provides otool used during installCheckPhase @@ -372,9 +392,18 @@ rustPlatform.buildRustPackage rec { echo "== cargo build cosmian_kms_server (release) ==" cargo build --release -p cosmian_kms_server --no-default-features \ ${lib.optionalString (features != [ ]) "--features ${lib.concatStringsSep "," features}"} + # Note: NOT running postBuild hook to avoid test execution + ''; + + installPhase = '' + runHook preInstall + mkdir -p "$out/bin" + # Copy the server binary + install -m755 target/release/cosmian_kms "$out/bin/cosmian_kms" + # Ensure the final artifact uses the system dynamic linker (not the Nix store one). + # Do this as a deterministic post-link patch rather than an impure re-link. if [ "$(uname)" = "Linux" ]; then - # Determine system dynamic linker path by architecture (avoid Nix-side interpolation on Darwin) DL="" ARCH="$(uname -m)" if [ "$ARCH" = "x86_64" ]; then @@ -383,25 +412,9 @@ rustPlatform.buildRustPackage rec { DL="/lib/ld-linux-aarch64.so.1" fi if [ -n "$DL" ]; then - echo "== Re-linking final binary with system dynamic linker: $DL ==" - export NIX_ENFORCE_PURITY=0 - export NIX_DONT_SET_RPATH=1 - export NIX_LDFLAGS="" - export NIX_CFLAGS_LINK="" - # Re-link the final binary (no rebuild of deps/build-scripts) - cargo rustc --release -p cosmian_kms_server --bin cosmian_kms \ - ${lib.optionalString (features != [ ]) "--features ${lib.concatStringsSep "," features}"} \ - -- -C link-arg=-Wl,--dynamic-linker,$DL + patchelf --set-interpreter "$DL" "$out/bin/cosmian_kms" fi fi - # Note: NOT running postBuild hook to avoid test execution - ''; - - installPhase = '' - runHook preInstall - mkdir -p "$out/bin" - # Copy the re-linked server binary - install -m755 target/release/cosmian_kms "$out/bin/cosmian_kms" runHook postInstall ''; @@ -419,6 +432,23 @@ rustPlatform.buildRustPackage rec { cp -r "${openssl312_}/usr/local/cosmian/lib/ssl" "$out/usr/local/cosmian/lib/" ''} + ${lib.optionalString (!isFips && static) '' + # Non-FIPS static: ship OpenSSL 3.6.0 provider modules (legacy, default) + # and a non-FIPS openssl.cnf that activates default+legacy (not fips) providers. + # This is needed for PKCS#12 parsing and other legacy algorithms at runtime. + mkdir -p "$out/usr/local/cosmian/lib/ossl-modules" + mkdir -p "$out/usr/local/cosmian/lib/ssl" + if [ -d "${openssl36_}/usr/local/cosmian/lib/ossl-modules" ]; then + cp -r "${openssl36_}/usr/local/cosmian/lib/ossl-modules/"* "$out/usr/local/cosmian/lib/ossl-modules/" 2>/dev/null || true + elif [ -d "${openssl36_}/lib/ossl-modules" ]; then + cp -r "${openssl36_}/lib/ossl-modules/"* "$out/usr/local/cosmian/lib/ossl-modules/" 2>/dev/null || true + fi + # Ship non-FIPS openssl.cnf (generated by openssl.nix with enableLegacy) + if [ -f "${openssl36_}/usr/local/cosmian/lib/ssl/openssl.cnf" ]; then + cp "${openssl36_}/usr/local/cosmian/lib/ssl/openssl.cnf" "$out/usr/local/cosmian/lib/ssl/" + fi + ''} + ${lib.optionalString (!static) '' # Dynamic linkage variant: ship libssl and libcrypto mkdir -p "$out/usr/local/cosmian/lib" @@ -509,8 +539,10 @@ rustPlatform.buildRustPackage rec { "/build=/cosmian-src" "--remap-path-prefix" "/tmp=/cosmian-src" - "--remap-path-prefix" - "${toString ../.}=/cosmian-src" + ]; + # Additional flags for determinism + determinism = lib.concatStringsSep " " [ + "-C symbol-mangling-version=v0" ]; linuxOnly = lib.concatStringsSep " " ( [ @@ -526,32 +558,64 @@ rustPlatform.buildRustPackage rec { !static && pkgs.stdenv.isLinux ) "-C link-arg=-Wl,-rpath,/usr/local/cosmian/lib"; in - if pkgs.stdenv.isLinux then remap + " " + linuxOnly + " " + dynamicOnly else remap; + if pkgs.stdenv.isLinux then + remap + " " + determinism + " " + linuxOnly + " " + dynamicOnly + else + remap + " " + determinism; NIX_DONT_SET_RPATH = lib.optionalString pkgs.stdenv.isLinux "1"; - NIX_LDFLAGS = lib.optionalString pkgs.stdenv.isLinux ""; - NIX_CFLAGS_LINK = lib.optionalString pkgs.stdenv.isLinux ""; - NIX_ENFORCE_PURITY = lib.optionalString pkgs.stdenv.isLinux "0"; + NIX_ENFORCE_PURITY = lib.optionalString pkgs.stdenv.isLinux "1"; dontCargoCheck = true; - dontCheck = !static; + # Run tests only for static builds (self-contained OpenSSL); dynamic builds + # lack runtime libssl in the Nix sandbox. Use doCheck (not dontCheck) for + # reliable behaviour across nixpkgs versions. + doCheck = static; dontUseCargoParallelTests = true; doInstallCheck = true; # Always run install checks to generate/verify hashes dontInstallCheck = false; cargoCheckHook = ""; cargoNextestHook = ""; - checkPhase = '' - runHook preCheck - echo "== cargo test cosmian_kms_server (release) ==" - export RUST_BACKTRACE=1 - export OPENSSL_DIR="${openssl312}" - export OPENSSL_LIB_DIR="${openssl312}/lib" - export OPENSSL_INCLUDE_DIR="${openssl312}/include" - export OPENSSL_NO_VENDOR=1 - - cargo test --release -p cosmian_kms_server --no-default-features \ - ${lib.optionalString (features != [ ]) "--features ${lib.concatStringsSep "," features}"} + checkPhase = + if static then + '' + runHook preCheck + echo "== cargo test cosmian_kms_server (release) ==" + export RUST_BACKTRACE=1 + '' + + ( + if isFips then + '' + # FIPS: tests use the 3.1.2 provider + export OPENSSL_DIR="${openssl312_}" + export OPENSSL_LIB_DIR="${openssl312_}/lib" + export OPENSSL_INCLUDE_DIR="${openssl312_}/include" + export OPENSSL_CONF="${openssl312_}/ssl/openssl.cnf" + export OPENSSL_MODULES="${openssl312_}/lib/ossl-modules" + '' + else + '' + # Non-FIPS: the binary needs the legacy provider at runtime. + # Point OPENSSL_CONF/MODULES to the Nix-store copy so legacy.so + # is found (compiled-in OPENSSLDIR=/usr/local/cosmian/… doesn't + # exist in the sandbox). + export OPENSSL_DIR="${openssl36_}" + export OPENSSL_LIB_DIR="${openssl36_}/lib" + export OPENSSL_INCLUDE_DIR="${openssl36_}/include" + export OPENSSL_CONF="${openssl36_}/ssl/openssl.cnf" + export OPENSSL_MODULES="${openssl36_}/lib/ossl-modules" + '' + ) + + '' + export OPENSSL_NO_VENDOR=1 - runHook postCheck - ''; + cargo test --release -p cosmian_kms_server --no-default-features \ + ${lib.optionalString (features != [ ]) "--features ${lib.concatStringsSep "," features}"} + + runHook postCheck + '' + else + '' + echo "== Skipping cargo test for dynamic build (libssl.so.3 unavailable in Nix sandbox) ==" + ''; configurePhase = '' export CARGO_HOME="$(pwd)/.cargo-home" ''; diff --git a/nix/openssl.nix b/nix/openssl.nix index ca439ad62c..0d97dbe6cc 100644 --- a/nix/openssl.nix +++ b/nix/openssl.nix @@ -114,6 +114,17 @@ stdenv.mkDerivation rec { # Configure with production openssldir path for portability # This hardcodes /usr/local/cosmian/lib/ssl into the library, making binaries portable # During build, we'll create this directory structure in $out for FIPS module generation + # + # Use fixed (non-Nix-store) paths for enginesdir and modulesdir so that + # these compiled-in strings are identical across machines, regardless of the + # OpenSSL derivation's Nix store hash. For static builds these directories + # are never opened at runtime; for dynamic/FIPS builds the provider is + # loaded from /usr/local/cosmian/lib/ossl-modules via openssl.cnf. + # + # Note: OpenSSL 3.x Configure does NOT support --enginesdir / --modulesdir + # as separate flags (they are misinterpreted as CFLAGS). Instead, we + # override the Makefile variables ENGINESDIR and MODULESDIR after Configure + # so the -D defines compiled into libcrypto use fixed paths. perl ./Configure \ ${if static then "no-shared" else "shared"} \ no-zlib \ @@ -124,12 +135,36 @@ stdenv.mkDerivation rec { --libdir=lib \ ${target} + # Override compiled-in ENGINESDIR and MODULESDIR in the generated Makefile + # to avoid embedding Nix store paths. The Makefile derives these from + # --prefix, so without this patch they would contain $out (/nix/store/...). + echo "Patching Makefile to use fixed ENGINESDIR / MODULESDIR..." + sed -i 's|^ENGINESDIR=.*|ENGINESDIR=/usr/local/cosmian/lib/engines-3|' Makefile + sed -i 's|^MODULESDIR=.*|MODULESDIR=/usr/local/cosmian/lib/ossl-modules|' Makefile + echo "ENGINESDIR=$(grep '^ENGINESDIR=' Makefile)" + echo "MODULESDIR=$(grep '^MODULESDIR=' Makefile)" + ''; buildPhase = '' runHook preBuild + + # Apply deterministic timestamp for reproducible builds + export SOURCE_DATE_EPOCH=1 + export ZERO_AR_DATE=1 + echo "Building OpenSSL ${version}..." make depend > /dev/null 2>&1 + + # Scrub Nix store paths from buildinf.h to ensure deterministic builds. + # OpenSSL generates this file during `make depend`, embedding the CC path and flags. + # The compiler wrapper may contain /nix/store/... paths that vary between machines. + if [ -f "crypto/buildinf.h" ]; then + echo "Scrubbing Nix store paths from crypto/buildinf.h..." + sed -i 's|/nix/store/[a-z0-9]\{32\}-[^/"]*|/usr/bin|g' crypto/buildinf.h + echo "buildinf.h after scrub:" + cat crypto/buildinf.h + fi # Determine job count as (cores - 1), minimum 1 if command -v nproc >/dev/null 2>&1; then CORES=$(nproc) @@ -148,6 +183,11 @@ stdenv.mkDerivation rec { installPhase = '' runHook preInstall + + # Apply deterministic timestamp for reproducible builds + export SOURCE_DATE_EPOCH=1 + export ZERO_AR_DATE=1 + echo "Installing OpenSSL ${version} to target paths..." # Determine job count as (cores - 1), minimum 1 if command -v nproc >/dev/null 2>&1; then @@ -162,8 +202,11 @@ stdenv.mkDerivation rec { JOBS=$(( CORES > 1 ? CORES - 1 : 1 )) # Install OpenSSL binaries and libraries only (not ssldirs - we'll handle that manually) + # Override ENGINESDIR and MODULESDIR back to $out paths for the install step, + # since the Makefile values were patched to fixed /usr/local/cosmian paths + # (needed for deterministic compile-time defines, but install must write to $out). echo "Running make install_sw..." - if ! make -j"$JOBS" install_sw; then + if ! make -j"$JOBS" install_sw ENGINESDIR=$out/lib/engines-3 MODULESDIR=$out/lib/ossl-modules; then echo "ERROR: make install_sw failed" exit 1 fi @@ -194,6 +237,9 @@ stdenv.mkDerivation rec { echo "Found legacy module at providers/legacy.${soExt}" cp "providers/legacy.${soExt}" "$out/usr/local/cosmian/lib/ossl-modules/" cp "providers/legacy.${soExt}" "$out/lib/ossl-modules/" + # Normalize timestamps for deterministic builds + touch --date=@1 "$out/usr/local/cosmian/lib/ossl-modules/legacy.${soExt}" + touch --date=@1 "$out/lib/ossl-modules/legacy.${soExt}" else echo "WARNING: legacy provider not found at providers/legacy.${soExt}" ls -la providers/ || true @@ -222,74 +268,148 @@ stdenv.mkDerivation rec { # Ensure dev copy exists cp "$out/usr/local/cosmian/lib/ssl/openssl.cnf" "$out/ssl/" - # Enable FIPS in both locations (original $out/ssl and target usr/local/cosmian/lib/ssl) - # This ensures FIPS works during both development/testing and production - # For production path, use the runtime path not the build path - for conf_dir in "$out/ssl" "$out/usr/local/cosmian/lib/ssl"; do - # Determine the appropriate include path based on the config directory - if [ "$conf_dir" = "$out/usr/local/cosmian/lib/ssl" ]; then - # Production path: use runtime location - include_path="/usr/local/cosmian/lib/ssl/fipsmodule.cnf" - else - # Dev/test path: use Nix store path for development - include_path="$conf_dir/fipsmodule.cnf" - fi - - # Use absolute path for .include to ensure it finds fipsmodule.cnf reliably - # OpenSSL 3.x supports absolute paths in .include directives - sed -i "s|^# \\.include fipsmodule\\.cnf|.include $include_path|g" "$conf_dir/openssl.cnf" - - # Uncomment the fips provider line - sed -i 's|^# fips = fips_sect|fips = fips_sect|g' "$conf_dir/openssl.cnf" - - # Ensure providers section is enabled and includes provider_sect - if ! grep -q "^providers[[:space:]]*=" "$conf_dir/openssl.cnf"; then - # Add providers = provider_sect under [openssl_init] - awk ' - BEGIN{in_init=0} - /^\[ *openssl_init *\]/{in_init=1; print; next} - in_init && /^[[:space:]]*#?[[:space:]]*providers[[:space:]]*=/{in_init=0} - in_init && NF==0{print "providers = provider_sect"; in_init=0} - {print} - ' "$conf_dir/openssl.cnf" > "$conf_dir/openssl.cnf.tmp" && mv "$conf_dir/openssl.cnf.tmp" "$conf_dir/openssl.cnf" - fi - - # Ensure provider_sect exists and references both fips and base - if ! grep -q "^\[ *provider_sect *\]" "$conf_dir/openssl.cnf"; then - { - echo ""; - echo "[ provider_sect ]"; - echo "fips = fips_sect"; - echo "base = base_sect"; - } >> "$conf_dir/openssl.cnf" + ${ + if enableLegacy then + # Non-FIPS build (enableLegacy=true): configure default + legacy + base providers. + # Do NOT reference fipsmodule.cnf or fips_sect β€” the FIPS provider may not be + # present at runtime in non-FIPS deployments. + '' + echo "Configuring openssl.cnf for non-FIPS mode (default + legacy + base providers)..." + for conf_dir in "$out/ssl" "$out/usr/local/cosmian/lib/ssl"; do + # Write a clean provider configuration for non-FIPS usage + # Remove any leftover FIPS references from the default openssl.cnf + sed -i '/^# \.include fipsmodule\.cnf/d' "$conf_dir/openssl.cnf" + sed -i '/^\.include.*fipsmodule\.cnf/d' "$conf_dir/openssl.cnf" + sed -i '/^# fips = fips_sect/d' "$conf_dir/openssl.cnf" + sed -i '/^fips = fips_sect/d' "$conf_dir/openssl.cnf" + + # Ensure providers section is enabled under [openssl_init] + if ! grep -q "^providers[[:space:]]*=" "$conf_dir/openssl.cnf"; then + awk ' + BEGIN{in_init=0} + /^\[ *openssl_init *\]/{in_init=1; print; next} + in_init && /^[[:space:]]*#?[[:space:]]*providers[[:space:]]*=/{in_init=0} + in_init && NF==0{print "providers = provider_sect"; in_init=0} + {print} + ' "$conf_dir/openssl.cnf" > "$conf_dir/openssl.cnf.tmp" && mv "$conf_dir/openssl.cnf.tmp" "$conf_dir/openssl.cnf" + fi + + # Ensure provider_sect references default + legacy + base + # The stock openssl.cnf may already have [provider_sect] with just `default = default_sect` + if grep -q "^\[ *provider_sect *\]" "$conf_dir/openssl.cnf"; then + # Add legacy and base references if missing + if ! awk 'f&&/^[[:space:]]*legacy[[:space:]]*=/{found=1} /^\[/{f=($0 ~ /provider_sect/)} END{exit found?0:1}' "$conf_dir/openssl.cnf"; then + sed -i '/^default = default_sect/a legacy = legacy_sect' "$conf_dir/openssl.cnf" + fi + if ! awk 'f&&/^[[:space:]]*base[[:space:]]*=/{found=1} /^\[/{f=($0 ~ /provider_sect/)} END{exit found?0:1}' "$conf_dir/openssl.cnf"; then + sed -i '/^legacy = legacy_sect/a base = base_sect' "$conf_dir/openssl.cnf" + fi + else + { + echo "" + echo "[ provider_sect ]" + echo "default = default_sect" + echo "legacy = legacy_sect" + echo "base = base_sect" + } >> "$conf_dir/openssl.cnf" + fi + + # Activate default_sect β€” the stock config has `# activate = 1` commented out. + # When we explicitly list providers, default must be activated. + if grep -q "^\[ *default_sect *\]" "$conf_dir/openssl.cnf"; then + # Uncomment activate = 1 if it's commented + sed -i '/^\[ *default_sect *\]/,/^\[/ s/^# *activate *= *1/activate = 1/' "$conf_dir/openssl.cnf" + # If no activate line at all, add one + if ! awk 'f&&/^[[:space:]]*activate[[:space:]]*=/{found=1} /^\[/{f=($0 ~ /default_sect/)} END{exit found?0:1}' "$conf_dir/openssl.cnf"; then + sed -i '/^\[ *default_sect *\]/a activate = 1' "$conf_dir/openssl.cnf" + fi + else + echo "" >> "$conf_dir/openssl.cnf" + echo "[ default_sect ]" >> "$conf_dir/openssl.cnf" + echo "activate = 1" >> "$conf_dir/openssl.cnf" + fi + + # Add legacy_sect + if ! grep -q "^\[ *legacy_sect *\]" "$conf_dir/openssl.cnf"; then + echo "" >> "$conf_dir/openssl.cnf" + echo "[ legacy_sect ]" >> "$conf_dir/openssl.cnf" + echo "activate = 1" >> "$conf_dir/openssl.cnf" + fi + + # Add base_sect + if ! grep -q "^\[ *base_sect *\]" "$conf_dir/openssl.cnf"; then + echo "" >> "$conf_dir/openssl.cnf" + echo "[ base_sect ]" >> "$conf_dir/openssl.cnf" + echo "activate = 1" >> "$conf_dir/openssl.cnf" + fi + done + echo "Non-FIPS openssl.cnf configured with default + legacy + base providers" + '' else - # If provider_sect exists, ensure base reference is present - if ! awk 'f&&/^[[:space:]]*base[[:space:]]*=/{found=1} /^\[/{f=($0 ~ /provider_sect/)} END{exit found?0:1}' "$conf_dir/openssl.cnf"; then - awk ' - BEGIN{in_prov=0} - /^\[ *provider_sect *\]/{in_prov=1; print; next} - in_prov && NF==0{print "base = base_sect"; in_prov=0} - {print} - ' "$conf_dir/openssl.cnf" > "$conf_dir/openssl.cnf.tmp" && mv "$conf_dir/openssl.cnf.tmp" "$conf_dir/openssl.cnf" - fi - fi - - # Add base provider (for non-FIPS algorithms still needed) - # First check if base_sect already exists to avoid duplication - if ! grep -q "^base = base_sect" "$conf_dir/openssl.cnf"; then - sed -i '/^fips = fips_sect/a base = base_sect' "$conf_dir/openssl.cnf" - fi - - # Add base_sect configuration if not already present - if ! grep -q "^\[ base_sect \]" "$conf_dir/openssl.cnf"; then - echo "" >> "$conf_dir/openssl.cnf" - echo "[ base_sect ]" >> "$conf_dir/openssl.cnf" - echo "activate = 1" >> "$conf_dir/openssl.cnf" - fi - done - - echo "OpenSSL FIPS modules and config installed to $out/usr/local/cosmian/lib/" - echo "OpenSSL FIPS config also enabled in $out/ssl/ for development/testing" + # FIPS build (enableLegacy=false): configure fips + base providers with fipsmodule.cnf + '' + echo "Configuring openssl.cnf for FIPS mode (fips + base providers)..." + # Enable FIPS in both locations (original $out/ssl and target usr/local/cosmian/lib/ssl) + for conf_dir in "$out/ssl" "$out/usr/local/cosmian/lib/ssl"; do + # Determine the appropriate include path based on the config directory + if [ "$conf_dir" = "$out/usr/local/cosmian/lib/ssl" ]; then + include_path="/usr/local/cosmian/lib/ssl/fipsmodule.cnf" + else + include_path="$conf_dir/fipsmodule.cnf" + fi + + sed -i "s|^# \\.include fipsmodule\\.cnf|.include $include_path|g" "$conf_dir/openssl.cnf" + sed -i 's|^# fips = fips_sect|fips = fips_sect|g' "$conf_dir/openssl.cnf" + + # Ensure providers section is enabled + if ! grep -q "^providers[[:space:]]*=" "$conf_dir/openssl.cnf"; then + awk ' + BEGIN{in_init=0} + /^\[ *openssl_init *\]/{in_init=1; print; next} + in_init && /^[[:space:]]*#?[[:space:]]*providers[[:space:]]*=/{in_init=0} + in_init && NF==0{print "providers = provider_sect"; in_init=0} + {print} + ' "$conf_dir/openssl.cnf" > "$conf_dir/openssl.cnf.tmp" && mv "$conf_dir/openssl.cnf.tmp" "$conf_dir/openssl.cnf" + fi + + # Ensure provider_sect exists with fips + base + if ! grep -q "^\[ *provider_sect *\]" "$conf_dir/openssl.cnf"; then + { + echo "" + echo "[ provider_sect ]" + echo "fips = fips_sect" + echo "base = base_sect" + } >> "$conf_dir/openssl.cnf" + else + if ! awk 'f&&/^[[:space:]]*base[[:space:]]*=/{found=1} /^\[/{f=($0 ~ /provider_sect/)} END{exit found?0:1}' "$conf_dir/openssl.cnf"; then + awk ' + BEGIN{in_prov=0} + /^\[ *provider_sect *\]/{in_prov=1; print; next} + in_prov && NF==0{print "base = base_sect"; in_prov=0} + {print} + ' "$conf_dir/openssl.cnf" > "$conf_dir/openssl.cnf.tmp" && mv "$conf_dir/openssl.cnf.tmp" "$conf_dir/openssl.cnf" + fi + fi + + if ! grep -q "^base = base_sect" "$conf_dir/openssl.cnf"; then + sed -i '/^fips = fips_sect/a base = base_sect' "$conf_dir/openssl.cnf" + fi + + if ! grep -q "^\[ base_sect \]" "$conf_dir/openssl.cnf"; then + echo "" >> "$conf_dir/openssl.cnf" + echo "[ base_sect ]" >> "$conf_dir/openssl.cnf" + echo "activate = 1" >> "$conf_dir/openssl.cnf" + fi + done + echo "FIPS openssl.cnf configured with fips + base providers" + '' + } + + echo "OpenSSL modules and config installed to $out/usr/local/cosmian/lib/" + + # Normalize all file timestamps for deterministic builds + echo "Normalizing timestamps for reproducibility..." + find "$out" -exec touch --date=@1 {} + runHook postInstall ''; diff --git a/nix/scripts/generate_sbom.sh b/nix/scripts/generate_sbom.sh index 1e01194035..fda1977462 100755 --- a/nix/scripts/generate_sbom.sh +++ b/nix/scripts/generate_sbom.sh @@ -213,19 +213,19 @@ echo "" # Note: "Failed reading nix meta information" warning is expected when scanning store paths # The SBOM still includes all package information, just without Nixpkgs-specific metadata echo "Generating CycloneDX SBOM..." -(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --cdx="$OUTPUT_DIR/bom.cdx.json") 2>&1 | grep -v "Failed reading nix meta" || true +(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --include-vulns --cdx="$OUTPUT_DIR/bom.cdx.json") 2>&1 | grep -v "Failed reading nix meta" || true echo " βœ“ bom.cdx.json" echo "" # Generate SPDX SBOM (JSON format - ISO standard) echo "Generating SPDX SBOM..." -(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --spdx="$OUTPUT_DIR/bom.spdx.json") 2>&1 | grep -v "Failed reading nix meta" || true +(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --include-vulns --spdx="$OUTPUT_DIR/bom.spdx.json") 2>&1 | grep -v "Failed reading nix meta" || true echo " βœ“ bom.spdx.json" echo "" # Generate CSV format echo "Generating CSV report..." -(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --csv="$OUTPUT_DIR/sbom.csv") 2>&1 | grep -v "Failed reading nix meta" || true +(cd "$SBOM_WORKDIR" && run_sbomnix "$NIX_RESULT" --impure --include-vulns --csv="$OUTPUT_DIR/sbom.csv") 2>&1 | grep -v "Failed reading nix meta" || true echo " βœ“ sbom.csv" echo "" @@ -264,7 +264,7 @@ echo "" echo "Generating dependency graph..." # Save current directory and change to output dir pushd "$OUTPUT_DIR" >/dev/null -if run_nixgraph "$NIX_RESULT" 2>&1 | grep -E "INFO|Wrote" || true; then +if run_nixgraph --depth 30 "$NIX_RESULT" 2>&1 | grep -E "INFO|Wrote" || true; then : fi popd >/dev/null diff --git a/nix/scripts/package_common.sh b/nix/scripts/package_common.sh index 3c9f10a910..b49451c82d 100755 --- a/nix/scripts/package_common.sh +++ b/nix/scripts/package_common.sh @@ -20,10 +20,9 @@ cd "$REPO_ROOT" FORMAT="" VARIANT="fips" LINK="static" -ENFORCE_DETERMINISTIC_HASH="${ENFORCE_DETERMINISTIC_HASH:-false}" usage() { - echo "Usage: $0 --format deb|rpm [--variant fips|non-fips] [--link static|dynamic] [--enforce-deterministic-hash true|false]" >&2 + echo "Usage: $0 --format deb|rpm [--variant fips|non-fips] [--link static|dynamic]" >&2 exit 2 } @@ -42,28 +41,11 @@ while [ $# -gt 0 ]; do LINK="${2:-}" shift 2 || true ;; - --enforce-deterministic-hash | --enforce_deterministic_hash) - ENFORCE_DETERMINISTIC_HASH="${2:-}" - shift 2 || true - ;; -h | --help) usage ;; *) shift ;; esac done -# Normalize boolean-ish inputs -case "${ENFORCE_DETERMINISTIC_HASH}" in -true | TRUE | 1) ENFORCE_DETERMINISTIC_HASH="true" ;; -false | FALSE | 0 | "") ENFORCE_DETERMINISTIC_HASH="false" ;; -*) - echo "Error: --enforce-deterministic-hash must be true/false" >&2 - exit 2 - ;; -esac - -# Nix arg for kms-server.nix and ui.nix -NIX_ENFORCE_ARGS=(--arg enforceDeterministicHash "$ENFORCE_DETERMINISTIC_HASH") - case "$FORMAT" in deb | rpm) : ;; *) @@ -190,7 +172,7 @@ prewarm_store() { else server_attr="kms-server-${VARIANT}-static-openssl" fi - [ $need_server -eq 1 ] && nix-build -I "nixpkgs=${PIN_URL}" "${NIX_ENFORCE_ARGS[@]}" -A "$server_attr" --no-out-link >/dev/null || echo "Server derivation already present" + [ $need_server -eq 1 ] && nix-build -I "nixpkgs=${PIN_URL}" -A "$server_attr" --no-out-link >/dev/null || echo "Server derivation already present" } # 0.2) Pre-warm Cargo registry/cache so cargo-deb/cargo generate-rpm can operate offline @@ -220,22 +202,9 @@ build_or_reuse_server() { OUT_LINK="$REPO_ROOT/result-server-${VARIANT}-${LINK}" - # In strict mode, always run nix-build so changes to expected hashes (e.g., server.vendor.*) - # are actually validated. In relaxed mode, reuse the existing result link to avoid rebuilds. - if [ "$ENFORCE_DETERMINISTIC_HASH" = "true" ]; then - nix-build -I "nixpkgs=${PIN_URL}" "${NIX_ENFORCE_ARGS[@]}" --option substituters "" "$REPO_ROOT/default.nix" -A "$attr" -o "$OUT_LINK" - REAL_SERVER=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") - else - # If we already have a built server at the expected link, reuse it blindly. - # The link name encodes variant/linkage, so no need to parse --info output, - # which can be ambiguous across modes and caused false mismatches. - if [ -L "$OUT_LINK" ] && [ -x "$(readlink -f "$OUT_LINK")/bin/cosmian_kms" ]; then - REAL_SERVER=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") - else - nix-build -I "nixpkgs=${PIN_URL}" "${NIX_ENFORCE_ARGS[@]}" --option substituters "" "$REPO_ROOT/default.nix" -A "$attr" -o "$OUT_LINK" - REAL_SERVER=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") - fi - fi + # Always run nix-build to validate changes to expected hashes (e.g., server.vendor.*) + nix-build -I "nixpkgs=${PIN_URL}" --option substituters "" "$REPO_ROOT/default.nix" -A "$attr" -o "$OUT_LINK" + REAL_SERVER=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") BIN_OUT="$REAL_SERVER/bin/cosmian_kms" @@ -244,6 +213,51 @@ build_or_reuse_server() { echo "Reusing/built server OK: binary present (UI handled separately)" } +# Ensure the expected-hash file for the server binary exists under nix/expected-hashes. +# This is used for deterministic build tracking and may be required by CI. +sync_server_expected_hash_file() { + local sys arch os impl filename src dst actual_hash + + if sys=$(nix eval --raw --expr 'builtins.currentSystem' 2>/dev/null); then + : + else + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) sys="x86_64-linux" ;; + Linux-aarch64 | Linux-arm64) sys="aarch64-linux" ;; + Darwin-x86_64) sys="x86_64-darwin" ;; + Darwin-arm64) sys="aarch64-darwin" ;; + *) sys="$(uname -m)-$(uname | tr '[:upper:]' '[:lower:]')" ;; + esac + fi + + arch="${sys%%-*}" + os="${sys#*-}" + impl=$([ "$LINK" = "dynamic" ] && echo dynamic-openssl || echo static-openssl) + filename="cosmian-kms-server.${VARIANT}.${impl}.${arch}.${os}.sha256" + + mkdir -p "$REPO_ROOT/nix/expected-hashes" + dst="$REPO_ROOT/nix/expected-hashes/$filename" + + # Prefer the file emitted by the Nix server derivation (install tests write it to $out/bin/). + src="$REAL_SERVER/bin/$filename" + if [ -f "$src" ]; then + cp -f "$src" "$dst" + echo "Synced expected-hash: nix/expected-hashes/$filename" + return 0 + fi + + # Fallback: compute from the built binary. + if [ -f "$BIN_OUT" ]; then + actual_hash=$(sha256sum "$BIN_OUT" | awk '{print $1}') + printf '%s\n' "$actual_hash" >"$dst" + echo "Wrote expected-hash: nix/expected-hashes/$filename = $actual_hash" + return 0 + fi + + echo "ERROR: Cannot produce expected-hash file (missing $src and $BIN_OUT)" >&2 + return 1 +} + # Build (or reuse) the Web UI once per variant, independent from server builds. # This avoids rebuilding the UI for each linkage type (static/dynamic) and keeps # the server derivations focused solely on the Rust backend. @@ -261,7 +275,7 @@ build_or_reuse_ui() { REAL_UI=$(readlink -f "$UI_OUT_LINK" || echo "$UI_OUT_LINK") else echo "Building UI derivation ($ui_attr) once for variant $VARIANT…" - nix-build -I "nixpkgs=${PIN_URL}" "${NIX_ENFORCE_ARGS[@]}" "$REPO_ROOT/default.nix" -A "$ui_attr" -o "$UI_OUT_LINK" + nix-build -I "nixpkgs=${PIN_URL}" "$REPO_ROOT/default.nix" -A "$ui_attr" -o "$UI_OUT_LINK" REAL_UI=$(readlink -f "$UI_OUT_LINK" || echo "$UI_OUT_LINK") if [ ! -d "$REAL_UI/dist" ]; then echo "ERROR: UI derivation $REAL_UI lacks dist/ directory" >&2 @@ -366,21 +380,11 @@ resolve_expected_hash_file() { } enforce_binary_hash() { - # Skip for non-fips variant - if [ "$VARIANT" = "non-fips" ]; then - echo "Skipping hash enforcement for non-fips variant" - return 0 - fi - # Build base key for lookup (variant + link to derive impl) local base_for_hash="${VARIANT}-${LINK}" local expected_file if ! expected_file=$(resolve_expected_hash_file "$base_for_hash"); then - if [ "$ENFORCE_DETERMINISTIC_HASH" = "true" ]; then - echo "WARNING: Expected server binary hash file missing; generating it via Nix and continuing (bootstrapping)." >&2 - else - echo "Expected hash file missing; generating it via Nix…" - fi + echo "WARNING: Expected server binary hash file missing; generating it via Nix and continuing (bootstrapping)." >&2 # Build the Nix attribute that produces the expected-hash file local attr case "$VARIANT-$LINK" in @@ -394,7 +398,7 @@ enforce_binary_hash() { ;; esac local store_out - store_out=$(nix-build -I "nixpkgs=${PIN_URL}" "${NIX_ENFORCE_ARGS[@]}" -A "$attr" --no-out-link) + store_out=$(nix-build -I "nixpkgs=${PIN_URL}" -A "$attr" --no-out-link) mkdir -p "$REPO_ROOT/nix/expected-hashes" # Copy all .sha256 files from the derivation output (there should be exactly one) cp -f "$store_out"/*.sha256 "$REPO_ROOT/nix/expected-hashes/" @@ -1174,12 +1178,8 @@ prewarm_store ensure_expected_hashes build_or_reuse_ui build_or_reuse_server -if [ "$ENFORCE_DETERMINISTIC_HASH" = "true" ]; then - enforce_binary_hash -else - write_binary_hash_file || true - echo "Skipping deterministic binary hash enforcement (ENFORCE_DETERMINISTIC_HASH=false)" -fi +sync_server_expected_hash_file +enforce_binary_hash resolve_openssl_path prewarm_cargo_registry prepare_workspace diff --git a/nix/scripts/package_dmg.sh b/nix/scripts/package_dmg.sh index a109dee6db..149a45afe9 100755 --- a/nix/scripts/package_dmg.sh +++ b/nix/scripts/package_dmg.sh @@ -11,7 +11,6 @@ source "$REPO_ROOT/.github/scripts/common.sh" # Determine variant and link mode from CLI arguments VARIANT="fips" LINK="static" -ENFORCE_DETERMINISTIC_HASH="${ENFORCE_DETERMINISTIC_HASH:-false}" while [ $# -gt 0 ]; do case "$1" in -v | --variant) @@ -22,10 +21,6 @@ while [ $# -gt 0 ]; do LINK="${2:-}" shift 2 || true ;; - --enforce-deterministic-hash | --enforce_deterministic_hash) - ENFORCE_DETERMINISTIC_HASH="${2:-}" - shift 2 || true - ;; *) shift ;; esac done @@ -44,16 +39,6 @@ static | dynamic) : ;; ;; esac -# Normalize boolean-ish inputs -case "${ENFORCE_DETERMINISTIC_HASH}" in -true | TRUE | 1) ENFORCE_DETERMINISTIC_HASH="true" ;; -false | FALSE | 0 | "") ENFORCE_DETERMINISTIC_HASH="false" ;; -*) - echo "Error: --enforce-deterministic-hash must be true/false" >&2 - exit 1 - ;; -esac - # Get version from Cargo.toml VERSION_STR=$("$REPO_ROOT/nix/scripts/get_version.sh") @@ -91,7 +76,7 @@ else echo "Building server derivation (variant: $VARIANT) via nix-build…" # Preserve existing link if reuse failed; replace atomically. rm -f "$OUT_LINK" 2>/dev/null || true - nix-build -I "nixpkgs=${PIN_URL}" --arg enforceDeterministicHash "$ENFORCE_DETERMINISTIC_HASH" -A "$ATTR" -o "$OUT_LINK" + nix-build -I "nixpkgs=${PIN_URL}" -A "$ATTR" -o "$OUT_LINK" REAL_OUT=$(readlink -f "$OUT_LINK" || echo "$OUT_LINK") fi @@ -184,11 +169,12 @@ if [ -z "$APP_BUNDLE" ]; then exit 1 fi -# Ensure FIPS OpenSSL assets are embedded in the app bundle for FIPS variants +# Ensure OpenSSL assets are embedded in the app bundle +RES_DIR="$APP_BUNDLE/Contents/Resources/usr/local/cosmian/lib" +mkdir -p "$RES_DIR/ossl-modules" "$RES_DIR/ssl" + if [ "$VARIANT" = "fips" ]; then - RES_DIR="$APP_BUNDLE/Contents/Resources/usr/local/cosmian/lib" - mkdir -p "$RES_DIR/ossl-modules" "$RES_DIR/ssl" - # Locate the OpenSSL store path built earlier + # FIPS variant: embed FIPS provider and configs from OpenSSL 3.1.2 OPENSSL_STORE=$(find /nix/store -maxdepth 1 -type d -name '*-openssl-3.1.2' 2>/dev/null | head -n1 || true) if [ -n "$OPENSSL_STORE" ]; then SRC_MOD="$OPENSSL_STORE/usr/local/cosmian/lib/ossl-modules/fips.dylib" @@ -209,6 +195,19 @@ if [ "$VARIANT" = "fips" ]; then else echo "Warning: OpenSSL store path not found; skipping FIPS asset embedding" >&2 fi +else + # Non-FIPS variant: embed legacy provider and non-FIPS openssl.cnf from the server derivation + SERVER_OSSL_DIR="$REAL_OUT/usr/local/cosmian/lib" + if [ -d "$SERVER_OSSL_DIR/ossl-modules" ]; then + cp -f -r "$SERVER_OSSL_DIR/ossl-modules/"* "$RES_DIR/ossl-modules/" 2>/dev/null || true + echo "Embedded non-FIPS OpenSSL modules from server derivation" + else + echo "Warning: No ossl-modules found in server derivation at $SERVER_OSSL_DIR" >&2 + fi + if [ -f "$SERVER_OSSL_DIR/ssl/openssl.cnf" ]; then + cp -f "$SERVER_OSSL_DIR/ssl/openssl.cnf" "$RES_DIR/ssl/openssl.cnf" + echo "Embedded non-FIPS OpenSSL config from server derivation" + fi fi arch_raw="$(uname -m)" case "$arch_raw" in diff --git a/nix/ui.nix b/nix/ui.nix index 39b68bca05..eb5d1ae4ad 100644 --- a/nix/ui.nix +++ b/nix/ui.nix @@ -6,8 +6,6 @@ version, features ? [ ], # [ "non-fips" ] or [] rustToolchain ? null, # Optional custom Rust toolchain (e.g., 1.90.0 for edition2024 support) - # Allow callers to bypass strict enforcement for NPM deps hash discovery - enforceDeterministicHash ? false, }: let @@ -36,17 +34,10 @@ let raw = builtins.readFile hashFile; trimmed = lib.replaceStrings [ "\n" "\r" " " "\t" ] [ "" "" "" "" ] raw; in - if enforceDeterministicHash then - ( - assert trimmed != placeholder && trimmed != ""; - trimmed - ) - else - trimmed - else if enforceDeterministicHash then - builtins.throw ("Expected UI vendor cargo hash file not found: " + hashFile) + assert trimmed != placeholder && trimmed != ""; + trimmed else - placeholder; + builtins.throw ("Expected UI vendor cargo hash file not found: " + hashFile); # Filter source to exclude large directories sourceFilter = @@ -134,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"; @@ -172,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" @@ -205,17 +207,10 @@ let raw = builtins.readFile hashFile; trimmed = lib.replaceStrings [ "\n" "\r" " " "\t" ] [ "" "" "" "" ] raw; in - if enforceDeterministicHash then - ( - assert trimmed != placeholder && trimmed != ""; - trimmed - ) - else - trimmed - else if enforceDeterministicHash then - builtins.throw ("Expected UI npm deps hash file not found: " + hashFile) + assert trimmed != placeholder && trimmed != ""; + trimmed else - placeholder; + builtins.throw ("Expected UI npm deps hash file not found: " + hashFile); # Disable build phase - we only want dependencies installed dontBuild = true; diff --git a/shell.nix b/shell.nix index b2c62b7142..9dde3ea5d6 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 @@ -55,6 +56,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) @@ -76,15 +93,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 rustToolchain @@ -139,8 +157,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 @@ -166,15 +185,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 @@ -193,25 +212,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/ui/package.json b/ui/package.json index 6125d1f1f0..5b446e7583 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; + }, + }, + }, + }, +}); From 0f1a794ddd7709723ffff37fbc895a64a08750e3 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Sat, 21 Feb 2026 15:12:05 +0100 Subject: [PATCH 2/3] chore: fix Nix expected hashes --- nix/expected-hashes/server.vendor.dynamic.darwin.sha256 | 2 +- nix/expected-hashes/server.vendor.dynamic.linux.sha256 | 2 +- nix/expected-hashes/server.vendor.static.darwin.sha256 | 2 +- nix/expected-hashes/server.vendor.static.linux.sha256 | 2 +- nix/expected-hashes/ui.vendor.fips.sha256 | 2 +- nix/expected-hashes/ui.vendor.non-fips.sha256 | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 index 642cf2a923..a5a5987e60 100644 --- a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 @@ -1 +1 @@ -sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= +sha256-LCzumx6j7JVBDrBwXfi5kVPwTfZYKC5htCoiuWjnzzs= diff --git a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 index 642cf2a923..a5a5987e60 100644 --- a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 +++ b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 @@ -1 +1 @@ -sha256-Wld9KFXRzAoDgS/Qiz0kPPTEAZgiJWr1McFJ9oDg32k= +sha256-LCzumx6j7JVBDrBwXfi5kVPwTfZYKC5htCoiuWjnzzs= diff --git a/nix/expected-hashes/server.vendor.static.darwin.sha256 b/nix/expected-hashes/server.vendor.static.darwin.sha256 index 0767f4bc15..2997c1e707 100644 --- a/nix/expected-hashes/server.vendor.static.darwin.sha256 +++ b/nix/expected-hashes/server.vendor.static.darwin.sha256 @@ -1 +1 @@ -sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= +sha256-9/jf0Ubsz9k1U5RDETgY2f7FVESMa/RPZjFdgIaRKII= diff --git a/nix/expected-hashes/server.vendor.static.linux.sha256 b/nix/expected-hashes/server.vendor.static.linux.sha256 index 0767f4bc15..2997c1e707 100644 --- a/nix/expected-hashes/server.vendor.static.linux.sha256 +++ b/nix/expected-hashes/server.vendor.static.linux.sha256 @@ -1 +1 @@ -sha256-F85VrBiUKhYzB/2JM1W2uVhADosUC10W3TWk2AeNbhM= +sha256-9/jf0Ubsz9k1U5RDETgY2f7FVESMa/RPZjFdgIaRKII= diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index 2aacce5131..31f13d44e2 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1 @@ -sha256-IBvwHtcoyOX45BN/4/rZRCdse/n0r5iGHqiMtKWwWos= +sha256-2kTcd76LHRvULZh98ccbys3g+W13YUE8HMGtXX+VWec= diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index 9a7544f89d..d91f1c7ec4 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-vVEMcIsU6lVfVrRTtHzBRzOHNX5+3D41CdjFSOPd9oA= +sha256-XqnoC6KdxDgp4TMirLKBemJqu7ShIyFX5IGAjvmDgT8= From 895d3c1571b2dac6997765e77e6d3ff15e49cc91 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Sat, 21 Feb 2026 16:08:01 +0100 Subject: [PATCH 3/3] fix: merge duplicated Nix expected hashes --- .github/scripts/update_hashes.sh | 36 ++++++------------- .../server.vendor.dynamic.linux.sha256 | 1 - ...in.sha256 => server.vendor.dynamic.sha256} | 0 .../server.vendor.static.linux.sha256 | 1 - ...win.sha256 => server.vendor.static.sha256} | 0 nix/kms-server.nix | 11 +++--- 6 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 nix/expected-hashes/server.vendor.dynamic.linux.sha256 rename nix/expected-hashes/{server.vendor.dynamic.darwin.sha256 => server.vendor.dynamic.sha256} (100%) delete mode 100644 nix/expected-hashes/server.vendor.static.linux.sha256 rename nix/expected-hashes/{server.vendor.static.darwin.sha256 => server.vendor.static.sha256} (100%) diff --git a/.github/scripts/update_hashes.sh b/.github/scripts/update_hashes.sh index 7bdc66438c..2ccda5fc85 100644 --- a/.github/scripts/update_hashes.sh +++ b/.github/scripts/update_hashes.sh @@ -88,6 +88,7 @@ if [ -z "$RUN_ID" ]; then # Fetch recent workflow runs on this branch. # Prefer failures (likely hash mismatches), then fall back to the newest completed run. + # shellcheck disable=SC2016 # $runs is a jq variable, not a shell variable RUN_ID=$(gh run list --branch "$CURRENT_BRANCH" --limit 50 --json databaseId,status,conclusion \ --jq 'map(select(.status=="completed" and .conclusion != "cancelled")) as $runs | ($runs | map(select(.conclusion=="failure")) | .[0].databaseId) // ($runs | .[0].databaseId)') @@ -205,34 +206,19 @@ while IFS=$'\t' read -r JOB_ID JOB_NAME; do elif [[ "$last_drv_name" =~ ui-wasm-non-fips.*-vendor ]]; then target_file="$EXPECTED_DIR/ui.vendor.non-fips.sha256" # Server vendor (Cargo vendoring). Derivation names do not reliably include platform/linkage; - # infer those from the GitHub Actions job name. + # infer linkage from the GitHub Actions job name. Linux and Darwin share the same hash files. elif [[ "$last_drv_name" =~ (kms-server|server).*vendor|(^|-)vendor($|-) ]]; then - if [[ "$JOB_NAME" == *"macos"* ]] || [[ "$JOB_NAME" == *"darwin"* ]]; then - if [[ "$JOB_NAME" == *"static"* ]]; then - target_file="$EXPECTED_DIR/server.vendor.static.darwin.sha256" - elif [[ "$JOB_NAME" == *"dynamic"* ]]; then - target_file="$EXPECTED_DIR/server.vendor.dynamic.darwin.sha256" - else - FILE_TO_HASH["$EXPECTED_DIR/server.vendor.static.darwin.sha256"]="$got_hash" - FILE_TO_HASH["$EXPECTED_DIR/server.vendor.dynamic.darwin.sha256"]="$got_hash" - echo " Found hash for $EXPECTED_DIR/server.vendor.static.darwin.sha256: $got_hash" - echo " Found hash for $EXPECTED_DIR/server.vendor.dynamic.darwin.sha256: $got_hash" - target_file="" - fi + if [[ "$JOB_NAME" == *"dynamic"* ]]; then + target_file="$EXPECTED_DIR/server.vendor.dynamic.sha256" + elif [[ "$JOB_NAME" == *"static"* ]] || [[ "$JOB_NAME" == *"docker"* ]]; then + target_file="$EXPECTED_DIR/server.vendor.static.sha256" else - # Linux server vendor hashes are tracked per linkage mode. # Docker packaging builds are always static-linked. - if [[ "$JOB_NAME" == *"dynamic"* ]]; then - target_file="$EXPECTED_DIR/server.vendor.dynamic.linux.sha256" - elif [[ "$JOB_NAME" == *"static"* ]] || [[ "$JOB_NAME" == *"docker"* ]]; then - target_file="$EXPECTED_DIR/server.vendor.static.linux.sha256" - else - FILE_TO_HASH["$EXPECTED_DIR/server.vendor.static.linux.sha256"]="$got_hash" - FILE_TO_HASH["$EXPECTED_DIR/server.vendor.dynamic.linux.sha256"]="$got_hash" - echo " Found hash for $EXPECTED_DIR/server.vendor.static.linux.sha256: $got_hash" - echo " Found hash for $EXPECTED_DIR/server.vendor.dynamic.linux.sha256: $got_hash" - target_file="" - fi + FILE_TO_HASH["$EXPECTED_DIR/server.vendor.static.sha256"]="$got_hash" + FILE_TO_HASH["$EXPECTED_DIR/server.vendor.dynamic.sha256"]="$got_hash" + echo " Found hash for $EXPECTED_DIR/server.vendor.static.sha256: $got_hash" + echo " Found hash for $EXPECTED_DIR/server.vendor.dynamic.sha256: $got_hash" + target_file="" fi fi diff --git a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 b/nix/expected-hashes/server.vendor.dynamic.linux.sha256 deleted file mode 100644 index a5a5987e60..0000000000 --- a/nix/expected-hashes/server.vendor.dynamic.linux.sha256 +++ /dev/null @@ -1 +0,0 @@ -sha256-LCzumx6j7JVBDrBwXfi5kVPwTfZYKC5htCoiuWjnzzs= diff --git a/nix/expected-hashes/server.vendor.dynamic.darwin.sha256 b/nix/expected-hashes/server.vendor.dynamic.sha256 similarity index 100% rename from nix/expected-hashes/server.vendor.dynamic.darwin.sha256 rename to nix/expected-hashes/server.vendor.dynamic.sha256 diff --git a/nix/expected-hashes/server.vendor.static.linux.sha256 b/nix/expected-hashes/server.vendor.static.linux.sha256 deleted file mode 100644 index 2997c1e707..0000000000 --- a/nix/expected-hashes/server.vendor.static.linux.sha256 +++ /dev/null @@ -1 +0,0 @@ -sha256-9/jf0Ubsz9k1U5RDETgY2f7FVESMa/RPZjFdgIaRKII= diff --git a/nix/expected-hashes/server.vendor.static.darwin.sha256 b/nix/expected-hashes/server.vendor.static.sha256 similarity index 100% rename from nix/expected-hashes/server.vendor.static.darwin.sha256 rename to nix/expected-hashes/server.vendor.static.sha256 diff --git a/nix/kms-server.nix b/nix/kms-server.nix index 80645e4909..e72e75c11b 100644 --- a/nix/kms-server.nix +++ b/nix/kms-server.nix @@ -322,16 +322,13 @@ rustPlatform.buildRustPackage rec { # Deterministic vendoring: pinned cargo hash for workspace vendoring # Support cargoHash for compatibility across nixpkgs versions. - # Platform-specific vendor hashes (target-dependent deps). If out-of-date, temporarily set to "" - # and rebuild to obtain the new suggested value from Nix ("got: sha256-..."). + # Vendor hashes are per linkage mode (static/dynamic) and shared across platforms. + # If out-of-date, temporarily set to "" and rebuild to obtain the new suggested value from Nix ("got: sha256-..."). cargoHash = let - sys = pkgs.stdenv.hostPlatform.system; # e.g., x86_64-linux - parts = lib.splitString "-" sys; - os = builtins.elemAt parts 1; - # Both Darwin and Linux now use separate vendor files for static/dynamic (glibc 2.34 requirement) + # Linux and Darwin share the same vendor hash per linkage mode. linkSuffix = if static then "static" else "dynamic"; - vendorFile = ./expected-hashes + "/server.vendor.${linkSuffix}.${os}.sha256"; + vendorFile = ./expected-hashes + "/server.vendor.${linkSuffix}.sha256"; placeholder = "sha256-BBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; in if builtins.pathExists vendorFile then