From 3ca3b476d457d38154a728fdb2a96995d4b48a37 Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Fri, 12 Jun 2026 17:22:54 +0200 Subject: [PATCH 1/5] feat(config): add secret management for KMS config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credentials for secret backends (Vault, AWS SSM, Azure KV, Cosmian KMS) are now passed exclusively through clap structs — zero std::env::var() in production code. - command_line/secret_backends.rs: pure clap relay structs (SecretBackendKind, VaultBackendConfig, AwsSsmBackendConfig, AzureKvBackendConfig, CosmianKmsSecretConfig, SecretBackendConfig) — zero logic - config/secret_resolver.rs: all HTTP resolution logic for vault/aws-ssm/ azure-kv/cosmian-kms backends, called from load_from_args between TOML parse and deserialization - TOML values use neutral 'secret://' prefix; backend selected via --secret-backend / KMS_SECRET_BACKEND --- .github/scripts/nix.sh | 54 +- .github/scripts/test/test_secret_aws.sh | 64 + .github/scripts/test/test_secret_azure.sh | 107 ++ .../scripts/test/test_secret_cosmian_kms.sh | 196 +++ .github/scripts/test/test_secret_vault.sh | 96 ++ .github/scripts/test/test_secrets.sh | 25 + .github/workflows/test_all.yml | 23 + Cargo.lock | 1088 ++++++++++------- Cargo.toml | 2 + crate/server/Cargo.toml | 3 + .../config/command_line/azure_ekm_config.rs | 8 +- .../src/config/command_line/clap_config.rs | 61 +- crate/server/src/config/command_line/db.rs | 3 +- .../config/command_line/google_cse_config.rs | 8 +- .../src/config/command_line/http_config.rs | 3 +- .../config/command_line/idp_auth_config.rs | 3 +- .../server/src/config/command_line/logging.rs | 3 +- crate/server/src/config/command_line/mod.rs | 5 + .../src/config/command_line/proxy_config.rs | 8 +- .../config/command_line/secret_backends.rs | 144 +++ .../command_line/socket_server_config.rs | 8 +- .../src/config/command_line/tls_config.rs | 3 +- .../src/config/command_line/ui_config.rs | 22 +- .../src/config/command_line/workspace.rs | 3 +- crate/server/src/config/mod.rs | 1 + crate/server/src/config/secret_resolver.rs | 592 +++++++++ crate/server/src/main.rs | 1 + .../src/routes/aws_xks/aws_xks_config.rs | 9 +- nix/expected-hashes/cli.vendor.linux.sha256 | 2 +- .../server.vendor.static.sha256 | 2 +- nix/expected-hashes/ui.vendor.fips.sha256 | 2 +- nix/expected-hashes/ui.vendor.non-fips.sha256 | 2 +- pkg/kms.toml | 53 + shell.nix | 8 +- 34 files changed, 2142 insertions(+), 470 deletions(-) create mode 100644 .github/scripts/test/test_secret_aws.sh create mode 100644 .github/scripts/test/test_secret_azure.sh create mode 100755 .github/scripts/test/test_secret_cosmian_kms.sh create mode 100644 .github/scripts/test/test_secret_vault.sh create mode 100755 .github/scripts/test/test_secrets.sh create mode 100644 crate/server/src/config/command_line/secret_backends.rs create mode 100644 crate/server/src/config/secret_resolver.rs diff --git a/.github/scripts/nix.sh b/.github/scripts/nix.sh index 71b48c80e..e6b1b98a5 100755 --- a/.github/scripts/nix.sh +++ b/.github/scripts/nix.sh @@ -526,6 +526,32 @@ test_command() { luks) SCRIPT="$REPO_ROOT/.github/scripts/test/test_luks.sh" ;; + secret_vault) + SCRIPT="$REPO_ROOT/.github/scripts/test/test_secret_vault.sh" + ;; + secret_aws) + for var in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_REGION; do + if [ -z "${!var:-}" ]; then + echo "Error: Required environment variable $var is not set" >&2 + echo "AWS SSM secret backend tests require AWS credentials." >&2 + exit 1 + fi + done + SCRIPT="$REPO_ROOT/.github/scripts/test/test_secret_aws.sh" + ;; + secret_azure) + for var in AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_KV_NAME; do + if [ -z "${!var:-}" ]; then + echo "Error: Required environment variable $var is not set" >&2 + echo "Azure KV secret backend tests require Azure credentials." >&2 + exit 1 + fi + done + SCRIPT="$REPO_ROOT/.github/scripts/test/test_secret_azure.sh" + ;; + secret_cosmian_kms) + SCRIPT="$REPO_ROOT/.github/scripts/test/test_secret_cosmian_kms.sh" + ;; ui) SCRIPT="$REPO_ROOT/.github/scripts/test/test_ui.sh" ;; @@ -561,7 +587,7 @@ test_command() { ;; *) echo "Error: Unknown test type '$TEST_TYPE'" >&2 - echo "Valid types: aws_xks, sqlite, mysql, percona, mariadb, psql, redis, google_cse, gcp_cmek, pykmip, openssh, luks, otel_export, iris, jose, hsm [softhsm2|utimaco|proteccio|all], ui" >&2 + echo "Valid types: aws_xks, sqlite, mysql, percona, mariadb, psql, redis, google_cse, gcp_cmek, pykmip, openssh, luks, otel_export, iris, jose, hsm [softhsm2|utimaco|proteccio|all], ui, secret_vault, secret_aws, secret_azure, secret_cosmian_kms" >&2 usage ;; esac @@ -586,6 +612,24 @@ test_command() { if [ "$TEST_TYPE" = "luks" ]; then export WITH_LUKS=1 fi + # Cosmian KMS secret backend test: python3 is used for HTTP calls and JSON parsing (no curl needed) + if [ "$TEST_TYPE" = "secret_cosmian_kms" ]; then + export WITH_PYTHON=1 + fi + # AWS secret backend test: awscli2 is needed to create/delete SSM parameters + if [ "$TEST_TYPE" = "secret_aws" ]; then + export WITH_AWS=1 + fi + # Vault secret backend test: Docker is needed to start the Vault dev container; curl for readiness checks + if [ "$TEST_TYPE" = "secret_vault" ]; then + export WITH_DOCKER=1 + export WITH_CURL=1 + fi + # Azure KV secret backend test: curl + python3 are needed for REST API calls and JSON parsing + if [ "$TEST_TYPE" = "secret_azure" ]; then + export WITH_CURL=1 + export WITH_PYTHON=1 + fi # Ensure curl is present for test types that use HTTP readiness probes # or curl-based integration helpers inside the nix-shell. if [ "$TEST_TYPE" = "azure_ekm" ] || [ "$TEST_TYPE" = "ui" ] || [ "$TEST_TYPE" = "all" ] || [ "$TEST_TYPE" = "gcp_cmek" ] || [ "$TEST_TYPE" = "openssh" ] || [ "$TEST_TYPE" = "luks" ] || [ "$TEST_TYPE" = "jose" ]; then @@ -621,6 +665,14 @@ test_command() { --keep WITH_PYTHON \ --keep WITH_OPENSSH \ --keep WITH_LUKS \ + --keep WITH_AWS \ + --keep AWS_ACCESS_KEY_ID \ + --keep AWS_SECRET_ACCESS_KEY \ + --keep AWS_REGION \ + --keep AZURE_TENANT_ID \ + --keep AZURE_CLIENT_ID \ + --keep AZURE_CLIENT_SECRET \ + --keep AZURE_KV_NAME \ --keep IRIS_DOCKER_IMAGE \ --keep IRIS_LICENSE_KEY \ --keep VARIANT \ diff --git a/.github/scripts/test/test_secret_aws.sh b/.github/scripts/test/test_secret_aws.sh new file mode 100644 index 000000000..956da49fa --- /dev/null +++ b/.github/scripts/test/test_secret_aws.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Secret backend integration test — AWS Systems Manager Parameter Store +# +# Creates a SecureString parameter in SSM, runs the Rust #[ignore] integration +# test, then deletes the parameter. +# +# Required env vars (from GitHub secrets): +# AWS_ACCESS_KEY_ID — IAM credentials with ssm:GetParameter / ssm:PutParameter +# AWS_SECRET_ACCESS_KEY — (or AWS_PROFILE / instance role) +# AWS_REGION — region where the parameter is created (e.g. eu-west-1) +# KMS_TEST_AWS_KMS_KEY_ID — (optional) KMS key ID for SecureString encryption; +# defaults to "alias/aws/ssm" +# + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${SCRIPT_DIR}/../common.sh" + +init_build_env "$@" +setup_test_logging + +require_cmd cargo "Cargo is required." +require_cmd aws "AWS CLI v2 is required." + +echo "=========================================" +echo "Running secret backend test: AWS SSM Parameter Store" +echo "Variant: ${VARIANT_NAME}" +echo "=========================================" + +AWS_REGION="${AWS_REGION:-eu-west-1}" +PARAM_NAME="/kms/ci/secret-backend-test" +SECRET_VALUE="ci-secret-value" +KMS_KEY="${KMS_TEST_AWS_KMS_KEY_ID:-alias/aws/ssm}" + +cleanup() { + echo "Deleting SSM parameter ${PARAM_NAME}..." + aws ssm delete-parameter \ + --name "${PARAM_NAME}" \ + --region "${AWS_REGION}" 2>/dev/null || true +} +trap cleanup EXIT + +echo "Creating SSM SecureString parameter ${PARAM_NAME} in ${AWS_REGION}..." +aws ssm put-parameter \ + --name "${PARAM_NAME}" \ + --value "${SECRET_VALUE}" \ + --type SecureString \ + --key-id "${KMS_KEY}" \ + --region "${AWS_REGION}" \ + --overwrite + +echo "Building cosmian_kms_server with secret-aws feature..." +cargo build -p cosmian_kms_server + +echo "Running AWS SSM integration test..." +AWS_REGION="${AWS_REGION}" \ +KMS_TEST_AWS_SSM_URI="aws-ssm://${AWS_REGION}${PARAM_NAME}" \ +KMS_TEST_AWS_SSM_EXPECTED="${SECRET_VALUE}" \ +cargo test -p cosmian_kms_server --lib -- \ + --ignored --nocapture test_secret_aws_ssm + +echo "AWS SSM secret backend test completed successfully." diff --git a/.github/scripts/test/test_secret_azure.sh b/.github/scripts/test/test_secret_azure.sh new file mode 100644 index 000000000..15f62717f --- /dev/null +++ b/.github/scripts/test/test_secret_azure.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Secret backend integration test — Azure Key Vault +# +# Creates a secret in an existing Azure Key Vault, runs the Rust #[ignore] +# integration test, then deletes the secret (soft-delete; purge protection +# may prevent immediate purge). +# +# Required env vars (from GitHub secrets): +# AZURE_TENANT_ID — Azure AD tenant ID +# AZURE_CLIENT_ID — service-principal application (client) ID +# AZURE_CLIENT_SECRET — service-principal secret +# AZURE_KV_NAME — Key Vault name (e.g. keyvault-workload-v3) +# AZURE_KV_SP_OBJECT_ID — Object ID of the service principal in Enterprise +# Applications (for role assignment) +# + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${SCRIPT_DIR}/../common.sh" + +init_build_env "$@" +setup_test_logging + +require_cmd cargo "Cargo is required." +require_cmd curl "curl is required for Azure REST API calls." +require_cmd python3 "python3 is required for JSON parsing." + +echo "=========================================" +echo "Running secret backend test: Azure Key Vault" +echo "Variant: ${VARIANT_NAME}" +echo "=========================================" + +: "${AZURE_TENANT_ID:?AZURE_TENANT_ID must be set}" +: "${AZURE_CLIENT_ID:?AZURE_CLIENT_ID must be set}" +: "${AZURE_CLIENT_SECRET:?AZURE_CLIENT_SECRET must be set}" +: "${AZURE_KV_NAME:?AZURE_KV_NAME must be set}" + +SECRET_NAME="kms-ci-secret-$$" +SECRET_VALUE="ci-secret-value" +KV_BASE_URL="https://${AZURE_KV_NAME}.vault.azure.net" + +# ── OAuth2 helper ───────────────────────────────────────────────────────────── +get_kv_token() { + curl -sf -X POST \ + "https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token" \ + -d "grant_type=client_credentials" \ + -d "client_id=${AZURE_CLIENT_ID}" \ + -d "client_secret=${AZURE_CLIENT_SECRET}" \ + -d "scope=https://vault.azure.net/.default" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" +} + +cleanup() { + echo "Deleting Azure KV secret ${SECRET_NAME}..." + local token + token=$(get_kv_token) || true + if [ -n "${token:-}" ]; then + curl -sf -X DELETE \ + "${KV_BASE_URL}/secrets/${SECRET_NAME}?api-version=7.4" \ + -H "Authorization: Bearer ${token}" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "Obtaining Azure AD access token..." +KV_TOKEN=$(get_kv_token) + +echo "Purging any soft-deleted secret with the same name (idempotent)..." +PURGE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${KV_BASE_URL}/deletedsecrets/${SECRET_NAME}?api-version=7.4" \ + -H "Authorization: Bearer ${KV_TOKEN}") || true +echo "Purge response: HTTP ${PURGE_STATUS} (404 means not in recycle bin — OK)" +# Azure purge is async; wait a moment for it to propagate +if [ "${PURGE_STATUS}" = "204" ]; then + echo "Waiting 10s for purge to propagate..." + sleep 10 +fi + +echo "Creating secret ${SECRET_NAME} in vault ${AZURE_KV_NAME}..." +PUT_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ + "${KV_BASE_URL}/secrets/${SECRET_NAME}?api-version=7.4" \ + -H "Authorization: Bearer ${KV_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"value\":\"${SECRET_VALUE}\"}") +PUT_STATUS=$(echo "${PUT_RESPONSE}" | tail -1) +PUT_BODY=$(echo "${PUT_RESPONSE}" | head -n -1) +if [ "${PUT_STATUS}" -lt 200 ] || [ "${PUT_STATUS}" -ge 300 ]; then + echo "ERROR: PUT secret failed (HTTP ${PUT_STATUS}): ${PUT_BODY}" >&2 + exit 1 +fi +echo "Secret created (HTTP ${PUT_STATUS})" + +echo "Building cosmian_kms_server with secret-azure feature..." +cargo build -p cosmian_kms_server + +echo "Running Azure KV integration test..." +AZURE_TENANT_ID="${AZURE_TENANT_ID}" \ +AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" \ +AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET}" \ +KMS_TEST_AZURE_KV_URI="azure-kv://${AZURE_KV_NAME}/secrets/${SECRET_NAME}" \ +KMS_TEST_AZURE_KV_EXPECTED="${SECRET_VALUE}" \ +cargo test -p cosmian_kms_server --lib -- \ + --ignored --nocapture test_secret_azure_kv + +echo "Azure Key Vault secret backend test completed successfully." diff --git a/.github/scripts/test/test_secret_cosmian_kms.sh b/.github/scripts/test/test_secret_cosmian_kms.sh new file mode 100755 index 000000000..47a1fddaf --- /dev/null +++ b/.github/scripts/test/test_secret_cosmian_kms.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Secret backend integration test — Cosmian KMS +# +# Starts a local Cosmian KMS server (no auth, SQLite), imports a SecretData +# object with a known value via the KMIP HTTP API, runs the Rust #[ignore] +# integration test, then cleans up. +# +# No external credentials required; the server runs on localhost. +# Required tools: cargo, python3 + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${SCRIPT_DIR}/../common.sh" + +init_build_env "$@" +setup_test_logging + +require_cmd cargo "Cargo is required." +require_cmd python3 "python3 is required for JSON parsing." + +echo "=========================================" +echo "Running secret backend test: Cosmian KMS" +echo "Variant: ${VARIANT_NAME}" +echo "=========================================" + +KMS_HOST="127.0.0.1" +KMS_PORT=19998 +KMS_URL="http://${KMS_HOST}:${KMS_PORT}" +SQLITE_PATH="$(mktemp -d -t kms-secret-test-XXXXXX)" +KMS_CONF_PATH="$(mktemp -t kms-secret-test-conf-XXXXXX.toml)" +KMS_PID="" + +# The secret value we will store and expect back +SECRET_VALUE="ci-secret-value" +# UTF-8 hex encoding of SECRET_VALUE +SECRET_HEX="63692d7365637265742d76616c7565" +# Length in bits (15 bytes * 8) +SECRET_BITS=120 + +cleanup() { + if [ -n "${KMS_PID:-}" ]; then + echo "Stopping KMS server (PID ${KMS_PID})..." + kill "${KMS_PID}" 2>/dev/null || true + wait "${KMS_PID}" 2>/dev/null || true + fi + rm -rf "${SQLITE_PATH}" "${KMS_CONF_PATH}" +} +trap cleanup EXIT + +# ── Write KMS config ───────────────────────────────────────────────────────── +cat >"${KMS_CONF_PATH}" <&2 + exit 1 +fi +echo "KMS server ready." + +# ── Import a SecretData object ──────────────────────────────────────────────── +# Build a KMIP Register request carrying a SecretData (Password type) whose +# key material is the UTF-8 bytes of SECRET_VALUE. +echo "Registering SecretData object with value '${SECRET_VALUE}'..." +REGISTER_PAYLOAD=$(cat <&2 + exit 1 +fi +echo "Registered SecretData with ID: ${OBJECT_ID}" + +# ── Run the integration test ────────────────────────────────────────────────── +echo "Running Cosmian KMS secret backend integration test..." +# shellcheck disable=SC2068 +KMS_TEST_COSMIAN_KMS_URI="cosmian-kms://${KMS_HOST}:${KMS_PORT}/${OBJECT_ID}" \ +KMS_TEST_COSMIAN_KMS_EXPECTED="${SECRET_VALUE}" \ +cargo test ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server --lib -- \ + --ignored --nocapture test_secret_cosmian_kms + +echo "Cosmian KMS secret backend test completed successfully." diff --git a/.github/scripts/test/test_secret_vault.sh b/.github/scripts/test/test_secret_vault.sh new file mode 100644 index 000000000..2b8ed59c2 --- /dev/null +++ b/.github/scripts/test/test_secret_vault.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Secret backend integration test — HashiCorp Vault KV-v2 +# +# Starts a dev-mode Vault container, creates a test secret, runs the Rust +# #[ignore] integration test, then cleans up. +# +# Required tools: docker, cargo +# Feature flag: secret-vault + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +source "${SCRIPT_DIR}/../common.sh" + +init_build_env "$@" +setup_test_logging + +require_cmd cargo "Cargo is required." +require_cmd docker "Docker is required to run the Vault container." +require_cmd curl "curl is required for Vault readiness checks." + +echo "=========================================" +echo "Running secret backend test: HashiCorp Vault" +echo "Variant: ${VARIANT_NAME}" +echo "=========================================" + +VAULT_CONTAINER="kms-ci-vault-$$" +VAULT_PORT=18200 +VAULT_ADDR="http://127.0.0.1:${VAULT_PORT}" +VAULT_TOKEN="ci-root-token" + +# Test secret parameters +VAULT_MOUNT="secret" +VAULT_PATH="kms-ci/db" +VAULT_FIELD="password" +SECRET_VALUE="ci-secret-value" + +cleanup() { + echo "Stopping Vault container..." + docker rm -f "${VAULT_CONTAINER}" 2>/dev/null || true +} +trap cleanup EXIT + +echo "Starting Vault dev container on port ${VAULT_PORT}..." +docker run -d \ + --name "${VAULT_CONTAINER}" \ + -p "${VAULT_PORT}:8200" \ + -e "VAULT_DEV_ROOT_TOKEN_ID=${VAULT_TOKEN}" \ + -e "VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200" \ + hashicorp/vault:1.17 \ + vault server -dev + +echo "Waiting for Vault to be ready..." +for i in $(seq 1 60); do + # Accept any HTTP response (200=active, 429=standby, 501=not init, 503=sealed) + # curl without -f so it doesn't fail on non-2xx; we just need the server to respond. + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${VAULT_ADDR}/v1/sys/health" 2>/dev/null || true) + if [ "${STATUS}" = "200" ] || [ "${STATUS}" = "429" ]; then + echo "Vault is ready (attempt ${i}, status ${STATUS})" + break + fi + if [ "${i}" -eq 60 ]; then + echo "ERROR: Vault did not become ready in time (last status: ${STATUS})" >&2 + echo "--- Vault container logs ---" >&2 + docker logs "${VAULT_CONTAINER}" >&2 || true + exit 1 + fi + sleep 2 +done + +echo "Enabling KV-v2 secrets engine at mount '${VAULT_MOUNT}'..." +curl -sf -X POST \ + -H "X-Vault-Token: ${VAULT_TOKEN}" \ + -d '{"type":"kv","options":{"version":"2"}}' \ + "${VAULT_ADDR}/v1/sys/mounts/${VAULT_MOUNT}" || true # may already exist in dev mode + +echo "Writing test secret ${VAULT_MOUNT}/${VAULT_PATH}#${VAULT_FIELD}..." +curl -sf -X POST \ + -H "X-Vault-Token: ${VAULT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"data\":{\"${VAULT_FIELD}\":\"${SECRET_VALUE}\"}}" \ + "${VAULT_ADDR}/v1/${VAULT_MOUNT}/data/${VAULT_PATH}" + +echo "Building cosmian_kms_server with secret-vault feature..." +cargo build -p cosmian_kms_server + +echo "Running Vault integration test..." +VAULT_ADDR="${VAULT_ADDR}" \ +VAULT_TOKEN="${VAULT_TOKEN}" \ +KMS_TEST_VAULT_URI="vault://${VAULT_MOUNT}/${VAULT_PATH}#${VAULT_FIELD}" \ +KMS_TEST_VAULT_EXPECTED="${SECRET_VALUE}" \ +cargo test -p cosmian_kms_server --lib -- \ + --ignored --nocapture test_secret_vault + +echo "Vault secret backend test completed successfully." diff --git a/.github/scripts/test/test_secrets.sh b/.github/scripts/test/test_secrets.sh new file mode 100755 index 000000000..f0bba4797 --- /dev/null +++ b/.github/scripts/test/test_secrets.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# Secret backend integration tests — runs all backends in sequence: +# 1. HashiCorp Vault KV-v2 (vault://) +# 2. AWS SSM Parameter Store (aws-ssm://) +# 3. Azure Key Vault (azure-kv://) +# 4. Cosmian KMS (cosmian-kms://) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Running Vault secret backend tests ===" +bash "$SCRIPT_DIR/test_secret_vault.sh" + +echo "=== Running AWS SSM secret backend tests ===" +bash "$SCRIPT_DIR/test_secret_aws.sh" + +echo "=== Running Azure Key Vault secret backend tests ===" +bash "$SCRIPT_DIR/test_secret_azure.sh" + +echo "=== Running Cosmian KMS secret backend tests ===" +bash "$SCRIPT_DIR/test_secret_cosmian_kms.sh" + +echo "=== All secret backend tests passed ===" diff --git a/.github/workflows/test_all.yml b/.github/workflows/test_all.yml index ad682f836..969972f54 100644 --- a/.github/workflows/test_all.yml +++ b/.github/workflows/test_all.yml @@ -36,6 +36,10 @@ jobs: - luks - jose - iris + - secret_vault + - secret_aws + - secret_azure + - secret_cosmian_kms features: [fips, non-fips] exclude: # redis is exclusively for non-fips @@ -65,6 +69,14 @@ jobs: # iris requires non-fips (PKCS#12 + full TLS stack) - type: iris features: fips + # secret_cosmian_kms runs against a local KMS server — works with both fips and non-fips + # secret_vault, secret_aws, secret_azure require external services — run non-fips only + - type: secret_vault + features: fips + - type: secret_aws + features: fips + - type: secret_azure + features: fips include: # Docker services required per test type (types without an entry need no containers) - type: psql @@ -182,6 +194,17 @@ jobs: # InterSystems IRIS mTLS integration test IRIS_DOCKER_IMAGE: ${{ vars.IRIS_DOCKER_IMAGE || 'intersystemsdc/iris-community:latest' }} IRIS_LICENSE_KEY: ${{ secrets.IRIS_LICENSE_KEY }} + + # AWS secret backend variables (secret_aws test type) + AWS_ACCESS_KEY_ID: ${{ secrets.KMS_CI_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.KMS_CI_AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.KMS_CI_AWS_REGION }} + + # Azure Key Vault secret backend variables (secret_azure test type) + AZURE_TENANT_ID: ${{ secrets.KMS_CI_AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.KMS_CI_AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.KMS_CI_AZURE_CLIENT_SECRET }} + AZURE_KV_NAME: ${{ secrets.KMS_CI_AZURE_KV_NAME }} run: | set -ex bash .github/scripts/nix.sh --variant ${{ matrix.features }} test ${{ matrix.type }} diff --git a/Cargo.lock b/Cargo.lock index d5e9fc4a1..b27569814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,7 +46,7 @@ dependencies = [ "actix-web", "bitflags", "bytes", - "derive_more 2.1.0", + "derive_more 2.1.1", "futures-core", "http-range", "log", @@ -71,7 +71,7 @@ dependencies = [ "bitflags", "bytes", "bytestring", - "derive_more 2.1.0", + "derive_more 2.1.1", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -234,7 +234,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.1.0", + "derive_more 2.1.1", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -251,7 +251,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.4", "time", "tracing", "url", @@ -344,7 +344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" dependencies = [ "aes 0.9.1", - "const-oid 0.10.1", + "const-oid 0.10.2", ] [[package]] @@ -390,23 +390,70 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "argon2" @@ -422,9 +469,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -432,7 +479,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -461,9 +508,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -518,6 +565,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -526,9 +582,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -540,7 +596,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "http-body-util", "itoa", @@ -552,7 +608,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -566,7 +622,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "http-body-util", "mime", @@ -612,9 +668,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bit-set" @@ -633,9 +689,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -686,9 +742,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -704,9 +766,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] @@ -719,9 +781,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "shlex", @@ -776,9 +838,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -856,10 +918,10 @@ dependencies = [ "serde", "serde_json", "serial_test", - "strum", + "strum 0.27.2", "tempfile", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "url", "uuid", @@ -868,29 +930,58 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", ] +[[package]] +name = "clap-config-fallback" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f61a99d6c64d6325cde9a15be55c7674ad1cd6446e41a2b8e662ae5c328b10" +dependencies = [ + "clap", + "clap-config-fallback-derive", + "figment", + "serde", +] + +[[package]] +name = "clap-config-fallback-derive" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4911ec65e11d77a914d9a0bfe0063e83fb33152b770f41442a0f5b303961be29" +dependencies = [ + "darling", + "derive_more 2.1.1", + "heck", + "proc-macro2", + "quote", + "strum 0.28.0", + "syn", +] + [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -900,9 +991,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmov" @@ -910,6 +1001,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -955,9 +1052,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-oid" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-random" @@ -974,7 +1071,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -1014,9 +1111,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1038,7 +1135,7 @@ dependencies = [ "cosmian_logger", "serial_test", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "uuid", "windows-sys 0.59.0", @@ -1054,7 +1151,7 @@ dependencies = [ "base64 0.21.7", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "toml 0.8.23", "tracing", "url", @@ -1090,7 +1187,7 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "gensym", - "getrandom 0.2.16", + "getrandom 0.2.17", "leb128", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -1130,9 +1227,9 @@ dependencies = [ "regex", "serde", "serde_json", - "strum", - "strum_macros", - "thiserror 2.0.17", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -1156,10 +1253,10 @@ dependencies = [ "cosmian_logger", "futures", "libloading", - "lru 0.16.3", + "lru 0.16.4", "pkcs11-sys", "rand 0.10.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", "zeroize", ] @@ -1192,10 +1289,10 @@ dependencies = [ "serde_json", "serial_test", "sha2 0.10.9", - "strum", + "strum 0.27.2", "tempfile", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "url", @@ -1219,9 +1316,9 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", "url", ] @@ -1239,8 +1336,8 @@ dependencies = [ "pem", "serde", "serde_json", - "strum", - "thiserror 2.0.17", + "strum 0.27.2", + "thiserror 2.0.18", "time", "zeroize", ] @@ -1252,7 +1349,7 @@ dependencies = [ "base64 0.22.1", "console_error_panic_hook", "cosmian_kms_client_utils", - "getrandom 0.2.16", + "getrandom 0.2.17", "js-sys", "pem", "serde", @@ -1296,7 +1393,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "uuid", "x509-parser", @@ -1312,7 +1409,7 @@ dependencies = [ "cosmian_logger", "num-bigint-dig", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "zeroize", ] @@ -1332,6 +1429,7 @@ dependencies = [ "base64 0.22.1", "chrono", "clap", + "clap-config-fallback", "cosmian_kms_access", "cosmian_kms_base_hsm", "cosmian_kms_client_utils", @@ -1344,7 +1442,8 @@ dependencies = [ "futures", "governor", "hex", - "http 1.4.0", + "hmac 0.12.1", + "http 1.4.2", "jsonwebtoken", "native-tls", "num-bigint-dig", @@ -1362,13 +1461,13 @@ dependencies = [ "sha2 0.10.9", "smartcardhsm_pkcs11_loader", "softhsm2_pkcs11_loader", - "strum", + "strum 0.27.2", "subtle", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", "url", "utimaco_pkcs11_loader", @@ -1390,7 +1489,7 @@ dependencies = [ "cosmian_logger", "cosmian_sse_memories", "deadpool-postgres", - "lru 0.16.3", + "lru 0.16.4", "mysql_async", "num_cpus", "openssl", @@ -1400,9 +1499,9 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "strum", + "strum 0.27.2", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-postgres", "tokio-rusqlite", @@ -1422,7 +1521,7 @@ dependencies = [ "opentelemetry-stdout", "opentelemetry_sdk 0.29.0", "syslog-tracing", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "tracing-appender", "tracing-opentelemetry", @@ -1457,7 +1556,7 @@ dependencies = [ "serial_test", "sha3", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing-error", "tracing-subscriber", @@ -1479,8 +1578,8 @@ dependencies = [ "pkcs11-sys", "rand 0.10.1", "serial_test", - "strum_macros", - "thiserror 2.0.17", + "strum_macros 0.27.2", + "thiserror 2.0.18", "tracing", "zeroize", ] @@ -1747,11 +1846,46 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1763,9 +1897,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deadpool" @@ -1787,7 +1921,7 @@ checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9" dependencies = [ "async-trait", "deadpool", - "getrandom 0.2.16", + "getrandom 0.2.17", "tokio", "tokio-postgres", "tracing", @@ -1842,11 +1976,10 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1865,18 +1998,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case 0.10.0", "proc-macro2", @@ -1925,16 +2058,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.1", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1996,9 +2129,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -2083,9 +2216,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -2103,11 +2236,26 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "serde", + "serde_json", + "serde_yaml", + "toml 0.8.23", + "uncased", + "version_check", +] + [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flagset" @@ -2117,9 +2265,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-sys", @@ -2250,9 +2398,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -2305,9 +2453,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2406,7 +2554,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2415,17 +2563,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.4.0", - "indexmap 2.12.1", + "http 1.4.2", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2565,9 +2713,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2580,7 +2728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -2591,7 +2739,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body", "pin-project-lite", ] @@ -2634,22 +2782,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.4.0", + "h2 0.4.14", + "http 1.4.2", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2657,15 +2804,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.2", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -2703,23 +2849,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -2727,9 +2872,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2751,12 +2896,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2764,9 +2910,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2777,9 +2923,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2791,15 +2937,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2811,15 +2957,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2836,6 +2982,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -2849,9 +3001,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2875,12 +3027,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2905,19 +3057,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "iri-string" -version = "0.7.9" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2937,11 +3085,20 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -2955,13 +3112,13 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "getrandom 0.2.16", + "getrandom 0.2.17", "hmac 0.12.1", "js-sys", "p256", @@ -2974,6 +3131,7 @@ dependencies = [ "sha2 0.10.9", "signature", "simple_asn1", + "zeroize", ] [[package]] @@ -3015,7 +3173,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", ] [[package]] @@ -3043,9 +3201,9 @@ dependencies = [ [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" [[package]] name = "leb128fmt" @@ -3071,9 +3229,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -3097,9 +3255,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -3108,15 +3266,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-waker" @@ -3135,15 +3293,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -3190,9 +3348,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -3238,9 +3396,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3250,9 +3408,9 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97befee0c869cb56f3118f49d0f9bb68c9e3f380dec23c1100aedc4ec3ba239a" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" dependencies = [ "hybrid-array 0.2.3", "kem", @@ -3282,8 +3440,8 @@ dependencies = [ "percent-encoding", "rand 0.10.1", "serde", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.4", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-util", @@ -3313,15 +3471,15 @@ dependencies = [ "serde_json", "sha1", "sha2 0.10.9", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -3394,9 +3552,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -3455,8 +3613,8 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64 0.22.1", "chrono", - "getrandom 0.2.16", - "http 1.4.0", + "getrandom 0.2.17", + "http 1.4.2", "rand 0.8.6", "reqwest", "serde", @@ -3496,9 +3654,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -3539,9 +3703,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -3579,7 +3743,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -3591,7 +3755,7 @@ checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" dependencies = [ "async-trait", "bytes", - "http 1.4.0", + "http 1.4.2", "opentelemetry 0.29.1", "reqwest", "tracing", @@ -3605,7 +3769,7 @@ checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" dependencies = [ "async-trait", "futures-core", - "http 1.4.0", + "http 1.4.2", "opentelemetry 0.27.1", "opentelemetry-proto 0.27.0", "opentelemetry_sdk 0.27.1", @@ -3623,14 +3787,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" dependencies = [ "futures-core", - "http 1.4.0", + "http 1.4.2", "opentelemetry 0.29.1", "opentelemetry-http", "opentelemetry-proto 0.29.0", "opentelemetry_sdk 0.29.0", "prost", "reqwest", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tonic", "tracing", @@ -3713,7 +3877,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -3831,18 +3995,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -3851,15 +4015,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -3890,9 +4048,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "poly1305" @@ -3925,9 +4083,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-openssl" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f86f073ad570f76e9e278ce6f05775fc723eed7daa6b4f9c2aa078080a564a0" +checksum = "06743eefaa1a5c0ef2ccb6d9abf6528790a229eabd62ddcabf9b2a3aeff09fa4" dependencies = [ "openssl", "tokio", @@ -3969,9 +4127,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3993,9 +4151,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -4007,15 +4165,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -4042,9 +4200,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -4085,7 +4243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -4142,8 +4300,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.4", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4164,7 +4322,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4179,16 +4337,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.4", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4223,7 +4381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4254,7 +4412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4273,14 +4431,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4307,7 +4465,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4345,7 +4503,7 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tokio-util", "url", @@ -4362,9 +4520,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -4374,9 +4532,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4385,29 +4543,29 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.4.0", + "h2 0.4.14", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -4429,7 +4587,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -4472,7 +4630,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -4549,9 +4707,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -4562,9 +4720,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring 0.17.14", @@ -4576,9 +4734,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -4615,9 +4773,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -4643,20 +4801,11 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -4685,7 +4834,7 @@ dependencies = [ "async-trait", "chrono", "hex", - "http 1.4.0", + "http 1.4.2", "lazy_static", "log", "regex", @@ -4695,12 +4844,6 @@ dependencies = [ "tower 0.4.13", ] -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -4717,9 +4860,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -4730,9 +4873,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4740,9 +4883,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -4787,16 +4930,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4821,9 +4964,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4840,25 +4983,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serial_test" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", @@ -4906,9 +5062,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -4931,16 +5087,17 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4956,39 +5113,39 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smartcardhsm_pkcs11_loader" @@ -5011,12 +5168,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5076,13 +5233,28 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -5097,17 +5269,35 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5147,12 +5337,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5179,7 +5369,7 @@ dependencies = [ "serde_json", "time", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "zeroize", ] @@ -5194,11 +5384,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5214,9 +5404,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -5234,12 +5424,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -5251,15 +5440,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -5276,9 +5465,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -5296,9 +5485,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5311,9 +5500,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5321,16 +5510,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5378,7 +5567,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.10.1", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tokio-util", "whoami", @@ -5407,9 +5596,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5418,9 +5607,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5443,17 +5632,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.3", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -5467,9 +5656,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -5480,21 +5669,21 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] @@ -5505,9 +5694,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -5520,8 +5709,8 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2 0.4.12", - "http 1.4.0", + "h2 0.4.14", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -5561,9 +5750,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5576,20 +5765,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", - "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5606,9 +5795,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5618,12 +5807,13 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", - "thiserror 2.0.17", + "symlink", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -5641,9 +5831,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -5690,9 +5880,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -5728,11 +5918,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -5742,9 +5941,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -5763,9 +5962,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -5789,6 +5988,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" @@ -5803,14 +6008,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5819,6 +6025,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "utimaco_pkcs11_loader" version = "5.23.0" @@ -5832,7 +6044,7 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -5904,11 +6116,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen 0.57.1", ] [[package]] @@ -6044,7 +6256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.12.1", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -6057,7 +6269,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.12.1", + "indexmap 2.14.0", "semver", ] @@ -6443,18 +6655,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -6465,6 +6677,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -6484,7 +6702,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.12.1", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -6515,7 +6733,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.12.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6534,7 +6752,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.12.1", + "indexmap 2.14.0", "log", "semver", "serde", @@ -6546,9 +6764,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-cert" @@ -6575,7 +6793,7 @@ dependencies = [ "oid-registry", "ring 0.17.14", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -6591,9 +6809,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6602,9 +6820,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -6614,18 +6832,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -6634,18 +6852,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -6655,9 +6873,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "serde", "zeroize_derive", @@ -6665,9 +6883,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", @@ -6676,9 +6894,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6687,9 +6905,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6698,11 +6916,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 2dcbc9d9c..9f482c1e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,7 @@ base64 = "0.22" bitflags = "2.9" chrono = "0.4" clap = { version = "4.5", default-features = false } +clap-config-fallback = { version = "0.1", features = ["derive"] } criterion = { version = "0.6", default-features = false, features = [ "html_reports", "async_tokio", @@ -170,6 +171,7 @@ dialoguer = { version = "0.11", default-features = false, features = [ dotenvy = "0.15" futures = "0.3" hex = { version = "0.4", default-features = false } +hmac = { version = "0.12", features = ["std"] } http = "1.4" jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } lazy_static = "1.5" diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index c3242f2a3..a7ce08eb1 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -70,6 +70,7 @@ clap = { workspace = true, features = [ "derive", "cargo", ] } +clap-config-fallback = { workspace = true } cosmian_kms_access = { path = "../access", version = "5.23.0" } cosmian_kms_base_hsm = { path = "../hsm/base_hsm", version = "5.23.0" } cosmian_kms_server_database = { path = "../server_database", version = "5.23.0" } @@ -79,6 +80,7 @@ dialoguer = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } hex = { workspace = true, features = ["serde"] } +hmac = { workspace = true } http = { workspace = true } jsonwebtoken = { workspace = true } num-bigint-dig = { workspace = true, features = [ @@ -116,6 +118,7 @@ utimaco_pkcs11_loader = { path = "../hsm/utimaco", version = "5.23.0" } uuid = { workspace = true, features = ["v4"] } x509-parser = { workspace = true } zeroize = { workspace = true } +sha2.workspace = true [target.'cfg(windows)'.dependencies] windows-service = "0.8" diff --git a/crate/server/src/config/command_line/azure_ekm_config.rs b/crate/server/src/config/command_line/azure_ekm_config.rs index 09424187e..818485064 100644 --- a/crate/server/src/config/command_line/azure_ekm_config.rs +++ b/crate/server/src/config/command_line/azure_ekm_config.rs @@ -1,11 +1,17 @@ +// Field names intentionally share a prefix (`azure_ekm_*`) for disambiguation +// in flat CLI / env-var namespaces. The same prefix appears in the structs +// generated by `ConfigArgs`, so we suppress the lint at file level. +#![allow(clippy::struct_field_names)] + use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; #[allow(clippy::trivially_copy_pass_by_ref)] // this is required by serde fn is_false(b: &bool) -> bool { !b } -#[derive(Debug, Args, Deserialize, Serialize, Clone)] +#[derive(Debug, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] #[derive(Default)] pub struct AzureEkmConfig { diff --git a/crate/server/src/config/command_line/clap_config.rs b/crate/server/src/config/command_line/clap_config.rs index 9fd71faa4..46192006c 100644 --- a/crate/server/src/config/command_line/clap_config.rs +++ b/crate/server/src/config/command_line/clap_config.rs @@ -5,6 +5,8 @@ use std::{ }; use clap::{CommandFactory, Parser}; + +use super::secret_backends::SecretBackendConfig; use cosmian_kms_server_database::reexport::cosmian_kmip::kmip_2_1::extra::tagging::VENDOR_ID_COSMIAN; use serde::{Deserialize, Serialize}; @@ -70,6 +72,7 @@ impl Default for ClapConfig { aws_xks_config: AwsXksConfig::default(), kmip_policy: KmipPolicyConfig::default(), azure_ekm_config: AzureEkmConfig::default(), + secret_backends: SecretBackendConfig::default(), } } } @@ -120,7 +123,7 @@ pub struct ClapConfig { /// Legacy single-HSM configuration (flat CLI flags / top-level TOML fields). /// Keys use the old prefix convention: `hsm::::`. /// Kept for backward compatibility; prefer `[[hsm_instances]]` for new deployments. - #[clap(flatten)] + #[command(flatten)] #[serde(flatten)] pub hsm: HsmConfig, @@ -155,37 +158,37 @@ pub struct ClapConfig { #[clap(verbatim_doc_comment, long, env = "KMS_PUBLIC_URL")] pub kms_public_url: Option, - #[clap(flatten)] + #[command(flatten)] pub db: MainDBConfig, - #[clap(flatten)] + #[command(flatten)] pub socket_server: SocketServerConfig, - #[clap(flatten)] + #[command(flatten)] pub tls: TlsConfig, - #[clap(flatten)] + #[command(flatten)] pub http: HttpConfig, - #[clap(flatten)] + #[command(flatten)] pub proxy: ProxyConfig, - #[clap(flatten)] + #[command(flatten)] pub idp_auth: IdpAuthConfig, - #[clap(flatten)] + #[command(flatten)] pub ui_config: UiConfig, - #[clap(flatten)] + #[command(flatten)] pub google_cse_config: GoogleCseConfig, - #[clap(flatten)] + #[command(flatten)] pub azure_ekm_config: AzureEkmConfig, - #[clap(flatten)] + #[command(flatten)] pub workspace: WorkspaceConfig, - #[clap(flatten)] + #[command(flatten)] pub logging: LoggingConfig, /// The non-revocable key ID used for demo purposes @@ -197,7 +200,7 @@ pub struct ClapConfig { #[clap(long, verbatim_doc_comment)] pub privileged_users: Option>, - #[clap(flatten)] + #[command(flatten)] pub aws_xks_config: AwsXksConfig, /// KMIP algorithm policy. @@ -210,9 +213,17 @@ pub struct ClapConfig { /// /// The `DEFAULT` policy enforces built-in conservative allowlists (aligned with ANSSI/NIST/FIPS /// recommendations). - #[clap(flatten)] + #[command(flatten)] #[serde(rename = "kmip")] pub kmip_policy: KmipPolicyConfig, + + /// Authentication credentials for secret URI resolution backends. + /// + /// These are provided via CLI flags or environment variables only — + /// never stored in the TOML config file. + #[command(flatten)] + #[serde(skip)] + pub secret_backends: SecretBackendConfig, } impl ClapConfig { @@ -418,7 +429,13 @@ impl ClapConfig { let env_path = std::env::var("COSMIAN_KMS_CONF").ok().map(PathBuf::from); let default_path = PathBuf::from(get_default_config_path()); - // Helper to load a TOML file into ClapConfig + // Helper to load a TOML file into ClapConfig. + // + // Steps: + // 1. Read the file. + // 2. Parse into a `toml::Value`. + // 3. Resolve `secret://` URIs in string leaves via the selected backend. + // 4. Deserialize into `ClapConfig`. let load_file = |p: &PathBuf| -> KResult { let conf_content = std::fs::read_to_string(p).map_err(|e| { KmsError::ServerError(format!( @@ -426,11 +443,23 @@ impl ClapConfig { p.display() )) })?; - toml::from_str(&conf_content).map_err(|e| { + let mut config_value: toml::Value = toml::from_str(&conf_content).map_err(|e| { KmsError::ServerError(format!( "Cannot parse kms server config at: {} - {e:?}", p.display() )) + })?; + + crate::config::secret_resolver::resolve_config( + &mut config_value, + &preliminary.secret_backends, + )?; + + config_value.try_into().map_err(|e| { + KmsError::ServerError(format!( + "Cannot deserialize KMS server config at: {} - {e:?}", + p.display() + )) }) }; diff --git a/crate/server/src/config/command_line/db.rs b/crate/server/src/config/command_line/db.rs index cf33155b5..b519b54ff 100644 --- a/crate/server/src/config/command_line/db.rs +++ b/crate/server/src/config/command_line/db.rs @@ -1,6 +1,7 @@ use std::{fmt::Display, path::PathBuf}; use clap::Args; +use clap_config_fallback::ConfigArgs; use cosmian_kms_server_database::MainDbParams; #[cfg(feature = "non-fips")] use cosmian_kms_server_database::redis_master_key_from_password; @@ -87,7 +88,7 @@ impl Display for DatabaseType { } /// Configuration for the database -#[derive(Args, Clone, Deserialize, Serialize)] +#[derive(Args, ConfigArgs, Clone, Deserialize, Serialize)] #[serde(default)] pub struct MainDBConfig { /// The main database of the KMS server that holds default cryptographic objects and permissions. diff --git a/crate/server/src/config/command_line/google_cse_config.rs b/crate/server/src/config/command_line/google_cse_config.rs index 4696f0dc3..d0822317a 100644 --- a/crate/server/src/config/command_line/google_cse_config.rs +++ b/crate/server/src/config/command_line/google_cse_config.rs @@ -1,7 +1,13 @@ +// Field names intentionally share a prefix (`google_cse_*`) for disambiguation +// in flat CLI / env-var namespaces. The same prefix appears in the structs +// generated by `ConfigArgs`, so we suppress the lint at file level. +#![allow(clippy::struct_field_names)] + use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; -#[derive(Debug, Args, Deserialize, Serialize, Clone)] +#[derive(Debug, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] #[derive(Default)] pub struct GoogleCseConfig { diff --git a/crate/server/src/config/command_line/http_config.rs b/crate/server/src/config/command_line/http_config.rs index 873cb54fd..2b4310fe3 100644 --- a/crate/server/src/config/command_line/http_config.rs +++ b/crate/server/src/config/command_line/http_config.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; use super::tls_config::TlsConfig; @@ -11,7 +12,7 @@ const DEFAULT_HOSTNAME: &str = "127.0.0.1"; #[cfg(not(target_os = "windows"))] const DEFAULT_HOSTNAME: &str = "0.0.0.0"; -#[derive(Args, Clone, Deserialize, Serialize)] +#[derive(Args, ConfigArgs, Clone, Deserialize, Serialize)] #[serde(default)] pub struct HttpConfig { /// The KMS HTTP server port diff --git a/crate/server/src/config/command_line/idp_auth_config.rs b/crate/server/src/config/command_line/idp_auth_config.rs index 1c322a6ee..26752ad04 100644 --- a/crate/server/src/config/command_line/idp_auth_config.rs +++ b/crate/server/src/config/command_line/idp_auth_config.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; use crate::{config::IdpConfig as IdpConfigStruct, error::KmsError}; @@ -8,7 +9,7 @@ use crate::{config::IdpConfig as IdpConfigStruct, error::KmsError}; // Support for JWT token inspired by the doc at : https://cloud.google.com/api-gateway/docs/authenticating-users-jwt // and following pages -#[derive(Debug, Default, Args, Deserialize, Serialize)] +#[derive(Debug, Default, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] pub struct IdpAuthConfig { /// JWT authentication provider configuration. diff --git a/crate/server/src/config/command_line/logging.rs b/crate/server/src/config/command_line/logging.rs index 752cd516e..3218ce842 100644 --- a/crate/server/src/config/command_line/logging.rs +++ b/crate/server/src/config/command_line/logging.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; #[cfg(target_os = "linux")] @@ -33,7 +34,7 @@ pub fn get_default_rolling_log_dir() -> PathBuf { } #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Default, Args, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] pub struct LoggingConfig { /// An alternative to setting the `RUST_LOG` environment variable. diff --git a/crate/server/src/config/command_line/mod.rs b/crate/server/src/config/command_line/mod.rs index 7ddce4c11..2cd288a56 100644 --- a/crate/server/src/config/command_line/mod.rs +++ b/crate/server/src/config/command_line/mod.rs @@ -8,6 +8,7 @@ mod idp_auth_config; mod kmip_policy_config; mod logging; mod proxy_config; +pub mod secret_backends; mod socket_server_config; mod tls_config; mod ui_config; @@ -27,6 +28,10 @@ pub use kmip_policy_config::{ }; pub use logging::{LoggingConfig, get_default_rolling_log_dir}; pub use proxy_config::ProxyConfig; +pub use secret_backends::{ + AwsSsmBackendConfig, AzureKvBackendConfig, CosmianKmsSecretConfig, SecretBackendConfig, + SecretBackendKind, VaultBackendConfig, +}; pub use socket_server_config::SocketServerConfig; pub use tls_config::TlsConfig; pub use ui_config::{OidcConfig, UiConfig, get_default_ui_dist_path}; diff --git a/crate/server/src/config/command_line/proxy_config.rs b/crate/server/src/config/command_line/proxy_config.rs index 4c5f4d489..7164d85a3 100644 --- a/crate/server/src/config/command_line/proxy_config.rs +++ b/crate/server/src/config/command_line/proxy_config.rs @@ -1,9 +1,15 @@ +// Field names intentionally share a prefix (`proxy_*`) for disambiguation in +// flat CLI / env-var namespaces. The same prefix appears in the structs +// generated by `ConfigArgs`, so we suppress the lint at file level. +#![allow(clippy::struct_field_names)] + use std::fmt::Display; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; -#[derive(Args, Clone, Deserialize, Serialize)] +#[derive(Args, ConfigArgs, Clone, Deserialize, Serialize)] #[serde(default)] #[derive(Default)] pub struct ProxyConfig { diff --git a/crate/server/src/config/command_line/secret_backends.rs b/crate/server/src/config/command_line/secret_backends.rs new file mode 100644 index 000000000..b41b67476 --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends.rs @@ -0,0 +1,144 @@ +//! Clap credential structs for secret URI backends. +//! +//! This file contains **only** clap structs — no logic, no HTTP calls. +//! The actual resolution is performed elsewhere once the config is loaded. + +use clap::{Args, ValueEnum}; + +/// Selects which backend resolves `secret://` URIs in the KMS config file. +/// +/// Pass via `--secret-backend ` or the `KMS_SECRET_BACKEND` env var. +/// The TOML config uses the neutral `secret://` scheme; which backend is +/// actually used is **never** revealed in the config file itself. +#[derive(Clone, Default, ValueEnum, Debug)] +pub enum SecretBackendKind { + /// `HashiCorp` Vault KV-v2. + /// Path format: `secret:///[#]` + #[default] + #[value(name = "vault")] + Vault, + /// AWS Systems Manager Parameter Store. + /// Path format: `secret:///` + #[value(name = "aws-ssm")] + AwsSsm, + /// Azure Key Vault. + /// Path format: `secret:///secrets/[/]` + #[value(name = "azure-kv")] + AzureKv, + /// Another Cosmian KMS server (KMIP Get). + /// Path format: `secret://[:]/` + #[value(name = "cosmian-kms")] + CosmianKms, +} + +/// Credentials for the `HashiCorp` Vault KV-v2 backend. +/// +/// Injected exclusively via clap (CLI flags or environment variables). +/// Never read from the TOML config file. +#[derive(Args, Clone, Default)] +pub struct VaultBackendConfig { + /// Vault server URL (e.g. `https://vault.internal:8200`). + /// + /// Required when `secret://` URIs are resolved via the `vault` backend. + #[clap(long, env = "VAULT_ADDR", default_value = "http://127.0.0.1:8200")] + pub vault_addr: String, + + /// Vault token with `read` access on the target paths. + /// + /// Required when `secret://` URIs are resolved via the `vault` backend. + #[clap(long, env = "VAULT_TOKEN", default_value = "")] + pub vault_token: String, +} + +/// Credentials for the AWS Systems Manager Parameter Store backend. +/// +/// Injected exclusively via clap (CLI flags or environment variables). +/// Never read from the TOML config file. +#[derive(Args, Clone, Default)] +pub struct AwsSsmBackendConfig { + /// AWS access key ID. + /// + /// Required when `secret://` URIs are resolved via the `aws-ssm` backend. + #[clap(long, env = "AWS_ACCESS_KEY_ID", default_value = "")] + pub aws_access_key_id: String, + + /// AWS secret access key. + /// + /// Required when `secret://` URIs are resolved via the `aws-ssm` backend. + #[clap(long, env = "AWS_SECRET_ACCESS_KEY", default_value = "")] + pub aws_secret_access_key: String, + + /// AWS session token (for temporary credentials / STS). + #[clap(long, env = "AWS_SESSION_TOKEN")] + pub aws_session_token: Option, +} + +/// Credentials for the Azure Key Vault backend. +/// +/// Injected exclusively via clap (CLI flags or environment variables). +/// Never read from the TOML config file. +#[derive(Args, Clone, Default)] +pub struct AzureKvBackendConfig { + /// Azure AD tenant ID. + /// + /// Required when `secret://` URIs are resolved via the `azure-kv` backend. + #[clap(long, env = "AZURE_TENANT_ID", default_value = "")] + pub azure_tenant_id: String, + + /// Azure service-principal application (client) ID. + /// + /// Required when `secret://` URIs are resolved via the `azure-kv` backend. + #[clap(long, env = "AZURE_CLIENT_ID", default_value = "")] + pub azure_client_id: String, + + /// Azure service-principal client secret. + /// + /// Required when `secret://` URIs are resolved via the `azure-kv` backend. + #[clap(long, env = "AZURE_CLIENT_SECRET", default_value = "")] + pub azure_client_secret: String, +} + +/// Credentials for the Cosmian KMS secret backend. +/// +/// Injected exclusively via clap (CLI flags or environment variables). +/// Never read from the TOML config file. +#[derive(Args, Clone, Default)] +pub struct CosmianKmsSecretConfig { + /// Bearer token / API key for authenticating to the Cosmian KMS secret backend. + #[clap(long, env = "COSMIAN_KMS_SECRET_TOKEN")] + pub cosmian_kms_secret_token: Option, + + /// Skip TLS certificate verification for the Cosmian KMS secret backend. + /// + /// **For development and testing only.** Never enable in production. + #[clap(long, env = "COSMIAN_KMS_INSECURE_CERTS")] + pub cosmian_kms_insecure_certs: bool, +} + +/// Authentication credentials and backend selection for secret URI resolution. +/// +/// These values are provided exclusively via clap (CLI flags or environment +/// variables) and are **never** read from the TOML config file, preventing +/// accidental secret exposure through configuration files. +#[derive(Args, Clone, Default)] +pub struct SecretBackendConfig { + /// Secret backend to use for resolving `secret://` URIs in the config file. + /// + /// When set, all `secret://` values in the config file are resolved through + /// the selected backend. Credentials must be provided via the corresponding + /// flags or environment variables below. + #[clap(long, env = "KMS_SECRET_BACKEND", value_enum)] + pub backend: Option, + + #[command(flatten)] + pub vault: VaultBackendConfig, + + #[command(flatten)] + pub aws: AwsSsmBackendConfig, + + #[command(flatten)] + pub azure: AzureKvBackendConfig, + + #[command(flatten)] + pub cosmian_kms: CosmianKmsSecretConfig, +} diff --git a/crate/server/src/config/command_line/socket_server_config.rs b/crate/server/src/config/command_line/socket_server_config.rs index a20b4ac7e..7d3ba3535 100644 --- a/crate/server/src/config/command_line/socket_server_config.rs +++ b/crate/server/src/config/command_line/socket_server_config.rs @@ -1,6 +1,12 @@ +// Field names intentionally share a prefix (`socket_server_*`) for disambiguation +// in flat CLI / env-var namespaces. The same prefix appears in the structs +// generated by `ConfigArgs`, so we suppress the lint at file level. +#![allow(clippy::struct_field_names)] + use std::fmt::Display; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; const DEFAULT_SOCKET_SERVER_PORT: u16 = 5696; @@ -9,7 +15,7 @@ const DEFAULT_SOCKET_SERVER_HOSTNAME: &str = "127.0.0.1"; #[cfg(not(target_os = "windows"))] const DEFAULT_SOCKET_SERVER_HOSTNAME: &str = "0.0.0.0"; -#[derive(Args, Clone, Deserialize, Serialize)] +#[derive(Args, ConfigArgs, Clone, Deserialize, Serialize)] #[serde(default)] pub struct SocketServerConfig { /// Start the KMIP socket server? diff --git a/crate/server/src/config/command_line/tls_config.rs b/crate/server/src/config/command_line/tls_config.rs index ee8f7c49b..1188c37af 100644 --- a/crate/server/src/config/command_line/tls_config.rs +++ b/crate/server/src/config/command_line/tls_config.rs @@ -1,9 +1,10 @@ use std::{fmt::Display, path::PathBuf}; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; -#[derive(Args, Clone, Deserialize, Serialize)] +#[derive(Args, ConfigArgs, Clone, Deserialize, Serialize)] #[serde(default)] #[derive(Default)] pub struct TlsConfig { diff --git a/crate/server/src/config/command_line/ui_config.rs b/crate/server/src/config/command_line/ui_config.rs index b2cc429ce..4ce305817 100644 --- a/crate/server/src/config/command_line/ui_config.rs +++ b/crate/server/src/config/command_line/ui_config.rs @@ -1,6 +1,14 @@ +// `collection_is_never_read`: the `ConfigArgs` derive generates intermediate +// Vec collections used only as building blocks in the arg-merge logic; clippy +// cannot see through the macro boundary. +// `struct_field_names`: `ui_oidc_*` fields share a prefix because they are +// flattened into the CLI namespace and serde keys. +#![allow(clippy::collection_is_never_read, clippy::struct_field_names)] + use std::fmt; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; /// Default UI distribution folder path on Linux. @@ -36,7 +44,7 @@ pub fn get_default_ui_dist_path() -> String { "/Applications/Cosmian KMS Server.app/Contents/Resources/ui/".to_owned() } -#[derive(Default, Debug, Args, Deserialize, Serialize, Clone)] +#[derive(Default, Debug, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] pub struct UiConfig { /// Disable the embedded web UI. When set to false, the UI HTML assets are @@ -55,7 +63,15 @@ pub struct UiConfig { #[clap(verbatim_doc_comment, long, env = "KMS_SESSION_SALT")] pub ui_session_salt: Option, - #[clap(flatten)] + // `#[config(no_flatten)]` tells `clap_config_fallback` NOT to flatten this + // nested struct into the parent TOML table: the KMS config file must use a + // `[ui_config.ui_oidc_auth]` sub-table. + // `#[command(flatten)]` is a clap directive that inlines CLI flags from + // `OidcConfig` into the parent struct; it is unrelated to TOML layout. + // The two attributes operate at different layers (config file vs CLI) and + // are intentionally used together. + #[config(no_flatten)] + #[command(flatten)] pub ui_oidc_auth: OidcConfig, } @@ -72,7 +88,7 @@ const fn default_true() -> bool { true } -#[derive(Default, Args, Deserialize, Serialize, Clone)] +#[derive(Default, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] pub struct OidcConfig { /// The client ID of the configured OIDC tenant for UI Auth diff --git a/crate/server/src/config/command_line/workspace.rs b/crate/server/src/config/command_line/workspace.rs index dfd5a7599..32a7510d4 100644 --- a/crate/server/src/config/command_line/workspace.rs +++ b/crate/server/src/config/command_line/workspace.rs @@ -4,6 +4,7 @@ use std::{ }; use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; use crate::{ @@ -14,7 +15,7 @@ use crate::{ const DEFAULT_ROOT_DATA_PATH: &str = "./cosmian-kms"; const DEFAULT_TMP_PATH: &str = "/tmp"; -#[derive(Debug, Args, Deserialize, Serialize)] +#[derive(Debug, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] pub struct WorkspaceConfig { /// The root folder where the KMS will store its data diff --git a/crate/server/src/config/mod.rs b/crate/server/src/config/mod.rs index d20e58f87..69a696593 100644 --- a/crate/server/src/config/mod.rs +++ b/crate/server/src/config/mod.rs @@ -1,5 +1,6 @@ mod command_line; mod params; +mod secret_resolver; pub mod wizard; pub use command_line::*; diff --git a/crate/server/src/config/secret_resolver.rs b/crate/server/src/config/secret_resolver.rs new file mode 100644 index 000000000..912e551ac --- /dev/null +++ b/crate/server/src/config/secret_resolver.rs @@ -0,0 +1,592 @@ +//! Secret URI resolution — called by `load_from_args` between TOML parsing +//! and deserialisation into `ClapConfig`. +//! +//! Credentials are passed in via `SecretBackendConfig` (populated by clap). +//! Zero `std::env::var` calls in production code. + +use crate::{ + config::command_line::secret_backends::{SecretBackendConfig, SecretBackendKind}, + error::KmsError, + result::KResult, +}; + +// ─── Shared async helper ──────────────────────────────────────────────────── + +fn run_blocking(f: F) -> KResult +where + F: FnOnce() -> KResult + Send + 'static, + T: Send + 'static, +{ + std::thread::spawn(f) + .join() + .map_err(|e| KmsError::ServerError(format!("Secret resolution thread panicked: {e:?}")))? +} + +// ─── Trait ────────────────────────────────────────────────────────────────── + +trait SecretBackend { + fn resolve(&self, uri: &str) -> KResult; +} + +// ─── TOML walker ──────────────────────────────────────────────────────────── + +/// Replace every TOML string starting with `secret://` using the active backend. +fn resolve_secret_uris(value: &mut toml::Value, backend: &dyn SecretBackend) -> KResult<()> { + match value { + toml::Value::String(s) => { + if s.starts_with("secret://") { + *s = backend.resolve(s)?; + } + } + toml::Value::Table(map) => { + for (_, v) in map.iter_mut() { + resolve_secret_uris(v, backend)?; + } + } + toml::Value::Array(arr) => { + for v in arr { + resolve_secret_uris(v, backend)?; + } + } + _ => {} + } + Ok(()) +} + +/// Entry point called from `load_from_args`. +/// Does nothing when `config.backend` is `None`. +pub(crate) fn resolve_config(value: &mut toml::Value, config: &SecretBackendConfig) -> KResult<()> { + match &config.backend { + None => Ok(()), + Some(SecretBackendKind::Vault) => { + resolve_secret_uris(value, &vault::VaultBackend::new(&config.vault)) + } + Some(SecretBackendKind::AwsSsm) => { + resolve_secret_uris(value, &aws::AwsSsmBackend::new(&config.aws)) + } + Some(SecretBackendKind::AzureKv) => { + resolve_secret_uris(value, &azure::AzureKvBackend::new(&config.azure)) + } + Some(SecretBackendKind::CosmianKms) => resolve_secret_uris( + value, + &cosmian_kms::CosmianKmsBackend::new(&config.cosmian_kms), + ), + } +} + +// ─── HashiCorp Vault KV-v2 ────────────────────────────────────────────────── + +mod vault { + use super::{KResult, KmsError, SecretBackend, run_blocking}; + use crate::config::command_line::secret_backends::VaultBackendConfig; + + pub(super) struct VaultBackend { + addr: String, + token: String, + } + + impl VaultBackend { + pub(super) fn new(cfg: &VaultBackendConfig) -> Self { + Self { + addr: cfg.vault_addr.clone(), + token: cfg.vault_token.clone(), + } + } + } + + impl SecretBackend for VaultBackend { + fn resolve(&self, uri: &str) -> KResult { + if self.token.is_empty() { + return Err(KmsError::ServerError( + "vault_token (VAULT_TOKEN) is empty; required for the vault secret backend" + .to_owned(), + )); + } + let rest = uri + .strip_prefix("secret://") + .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; + let (path_part, field) = rest + .split_once('#') + .map_or((rest, "value"), |(p, f)| (p, f)); + let slash = path_part.find('/').ok_or_else(|| { + KmsError::InvalidRequest(format!( + "vault URI must be secret:///[#], got: {uri}" + )) + })?; + let mount = &path_part[..slash]; + let secret_path = &path_part[slash + 1..]; + let url = format!( + "{}/v1/{mount}/data/{secret_path}", + self.addr.trim_end_matches('/') + ); + let token = self.token.clone(); + let uri_owned = uri.to_owned(); + let field_owned = field.to_owned(); + let value: serde_json::Value = run_blocking(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? + .block_on(async { + let resp = reqwest::Client::new() + .get(&url) + .header("X-Vault-Token", &token) + .send() + .await + .map_err(|e| { + KmsError::ServerError(format!( + "Vault request failed for {uri_owned}: {e}" + )) + })?; + if !resp.status().is_success() { + return Err(KmsError::ServerError(format!( + "Vault returned HTTP {} for {uri_owned}", + resp.status() + ))); + } + resp.json::().await.map_err(|e| { + KmsError::ServerError(format!( + "Vault response parse error for {uri_owned}: {e}" + )) + }) + }) + })?; + value + .get("data") + .and_then(|d| d.get("data")) + .and_then(|d| d.get(&field_owned)) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!( + "Field '{field_owned}' not found in Vault secret at {uri}" + )) + }) + .map(str::to_owned) + } + } +} + +// ─── AWS SSM Parameter Store ───────────────────────────────────────────────── + +mod aws { + use chrono::Utc; + use hmac::{Hmac, Mac}; + use sha2::{Digest, Sha256}; + + use super::{KResult, KmsError, SecretBackend, run_blocking}; + use crate::config::command_line::secret_backends::AwsSsmBackendConfig; + + type HmacSha256 = Hmac; + + pub(super) struct AwsSsmBackend { + access_key_id: String, + secret_access_key: String, + session_token: Option, + } + + impl AwsSsmBackend { + pub(super) fn new(cfg: &AwsSsmBackendConfig) -> Self { + Self { + access_key_id: cfg.aws_access_key_id.clone(), + secret_access_key: cfg.aws_secret_access_key.clone(), + session_token: cfg.aws_session_token.clone(), + } + } + } + + fn hex(b: &[u8]) -> String { + hex::encode(b) + } + fn sha256(d: &[u8]) -> String { + hex(&Sha256::digest(d)) + } + fn hmac(key: &[u8], data: &[u8]) -> KResult> { + let mut mac = HmacSha256::new_from_slice(key) + .map_err(|e| KmsError::ServerError(format!("HMAC key error: {e}")))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + fn signing_key(secret: &str, date: &str, region: &str, service: &str) -> KResult> { + let k = hmac(format!("AWS4{secret}").as_bytes(), date.as_bytes())?; + let k = hmac(&k, region.as_bytes())?; + let k = hmac(&k, service.as_bytes())?; + hmac(&k, b"aws4_request") + } + + impl SecretBackend for AwsSsmBackend { + fn resolve(&self, uri: &str) -> KResult { + if self.access_key_id.is_empty() { + return Err(KmsError::ServerError( + "aws_access_key_id (AWS_ACCESS_KEY_ID) is not set; required for aws-ssm backend".to_owned(), + )); + } + if self.secret_access_key.is_empty() { + return Err(KmsError::ServerError( + "aws_secret_access_key (AWS_SECRET_ACCESS_KEY) is not set; required for aws-ssm backend".to_owned(), + )); + } + let rest = uri + .strip_prefix("secret://") + .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; + let slash = rest.find('/').ok_or_else(|| { + KmsError::InvalidRequest(format!( + "aws-ssm URI must be secret:///, got: {uri}" + )) + })?; + let region = rest[..slash].to_owned(); + let param_name = rest[slash..].to_owned(); + let access_key_id = self.access_key_id.clone(); + let secret_key = self.secret_access_key.clone(); + let session_token = self.session_token.clone(); + let uri_owned = uri.to_owned(); + run_blocking(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? + .block_on(call_ssm( + ®ion, + ¶m_name, + &access_key_id, + &secret_key, + session_token.as_deref(), + &uri_owned, + )) + }) + } + } + + async fn call_ssm( + region: &str, + param: &str, + key_id: &str, + secret: &str, + token: Option<&str>, + uri: &str, + ) -> KResult { + let now = Utc::now(); + let dt = now.format("%Y%m%dT%H%M%SZ").to_string(); + let date = now.format("%Y%m%d").to_string(); + let host = format!("ssm.{region}.amazonaws.com"); + let body = serde_json::json!({"Name": param, "WithDecryption": true}).to_string(); + let payload_hash = sha256(body.as_bytes()); + let (canon_hdrs, signed_hdrs) = token.map_or_else( + || (format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-target:AmazonSSM.GetParameter\n"), + "content-type;host;x-amz-date;x-amz-target".to_owned()), + |t| (format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-security-token:{t}\nx-amz-target:AmazonSSM.GetParameter\n"), + "content-type;host;x-amz-date;x-amz-security-token;x-amz-target".to_owned()), + ); + let canon_req = format!("POST\n/\n\n{canon_hdrs}\n{signed_hdrs}\n{payload_hash}"); + let scope = format!("{date}/{region}/ssm/aws4_request"); + let sts = format!( + "AWS4-HMAC-SHA256\n{dt}\n{scope}\n{}", + sha256(canon_req.as_bytes()) + ); + let key = signing_key(secret, &date, region, "ssm")?; + let sig = hex(&hmac(&key, sts.as_bytes())?); + let auth = format!( + "AWS4-HMAC-SHA256 Credential={key_id}/{scope}, SignedHeaders={signed_hdrs}, Signature={sig}" + ); + let client = reqwest::Client::new(); + let req = client + .post(format!("https://{host}/")) + .header("Content-Type", "application/x-amz-json-1.1") + .header("X-Amz-Date", &dt) + .header("X-Amz-Target", "AmazonSSM.GetParameter") + .header("Authorization", &auth); + let req = if let Some(t) = token { + req.header("X-Amz-Security-Token", t) + } else { + req + }; + let resp = + req.body(body).send().await.map_err(|e| { + KmsError::ServerError(format!("AWS SSM request failed for {uri}: {e}")) + })?; + if !resp.status().is_success() { + let s = resp.status(); + let b = resp.text().await.unwrap_or_default(); + return Err(KmsError::ServerError(format!( + "AWS SSM HTTP {s} for {uri}: {b}" + ))); + } + let j: serde_json::Value = resp.json().await.map_err(|e| { + KmsError::ServerError(format!("AWS SSM response parse error for {uri}: {e}")) + })?; + j.get("Parameter") + .and_then(|p| p.get("Value")) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!("AWS SSM missing Parameter.Value for {uri}")) + }) + .map(str::to_owned) + } +} + +// ─── Azure Key Vault ───────────────────────────────────────────────────────── + +mod azure { + use super::{KResult, KmsError, SecretBackend, run_blocking}; + use crate::config::command_line::secret_backends::AzureKvBackendConfig; + + pub(super) struct AzureKvBackend { + tenant_id: String, + client_id: String, + client_secret: String, + } + + impl AzureKvBackend { + pub(super) fn new(cfg: &AzureKvBackendConfig) -> Self { + Self { + tenant_id: cfg.azure_tenant_id.clone(), + client_id: cfg.azure_client_id.clone(), + client_secret: cfg.azure_client_secret.clone(), + } + } + } + + impl SecretBackend for AzureKvBackend { + fn resolve(&self, uri: &str) -> KResult { + for (name, val) in [ + ("azure_tenant_id (AZURE_TENANT_ID)", &self.tenant_id), + ("azure_client_id (AZURE_CLIENT_ID)", &self.client_id), + ( + "azure_client_secret (AZURE_CLIENT_SECRET)", + &self.client_secret, + ), + ] { + if val.is_empty() { + return Err(KmsError::ServerError(format!( + "{name} is not set; required for azure-kv secret backend" + ))); + } + } + let rest = uri + .strip_prefix("secret://") + .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; + let slash = rest.find('/').ok_or_else(|| { + KmsError::InvalidRequest(format!( + "azure-kv URI must be secret:///secrets/, got: {uri}" + )) + })?; + let vault_name = rest[..slash].to_owned(); + let secret_path = rest[slash + 1..].to_owned(); + let kv_url = + format!("https://{vault_name}.vault.azure.net/{secret_path}?api-version=7.4"); + let tenant_id = self.tenant_id.clone(); + let client_id = self.client_id.clone(); + let client_secret = self.client_secret.clone(); + let uri_owned = uri.to_owned(); + run_blocking(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? + .block_on(async { + let token_url = format!( + "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" + ); + let token_resp = reqwest::Client::new() + .post(&token_url) + .form(&[ + ("grant_type", "client_credentials"), + ("client_id", client_id.as_str()), + ("client_secret", client_secret.as_str()), + ("scope", "https://vault.azure.net/.default"), + ]) + .send() + .await + .map_err(|e| { + KmsError::ServerError(format!("Azure AD token request failed: {e}")) + })?; + if !token_resp.status().is_success() { + return Err(KmsError::ServerError(format!( + "Azure AD token endpoint returned HTTP {}", + token_resp.status() + ))); + } + let token_body: serde_json::Value = + token_resp.json().await.map_err(|e| { + KmsError::ServerError(format!("Azure AD token parse error: {e}")) + })?; + let token = token_body + .get("access_token") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError( + "No access_token in Azure AD response".to_owned(), + ) + })? + .to_owned(); + let resp = reqwest::Client::new() + .get(&kv_url) + .bearer_auth(&token) + .send() + .await + .map_err(|e| { + KmsError::ServerError(format!( + "Azure KV request failed for {uri_owned}: {e}" + )) + })?; + if !resp.status().is_success() { + return Err(KmsError::ServerError(format!( + "Azure KV returned HTTP {} for {uri_owned}", + resp.status() + ))); + } + let body: serde_json::Value = resp.json().await.map_err(|e| { + KmsError::ServerError(format!( + "Azure KV response parse error for {uri_owned}: {e}" + )) + })?; + body.get("value") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!( + "Field 'value' not found in Azure KV secret at {uri_owned}" + )) + }) + .map(str::to_owned) + }) + }) + } + } +} + +// ─── Cosmian KMS (KMIP Get) ─────────────────────────────────────────────────── + +mod cosmian_kms { + use cosmian_kms_server_database::reexport::cosmian_kmip::{ + kmip_2_1::{ + kmip_objects::Object, + kmip_operations::{Get, GetResponse}, + }, + ttlv::{TTLV, from_ttlv, to_ttlv}, + }; + + use super::{KResult, KmsError, SecretBackend, run_blocking}; + use crate::config::command_line::secret_backends::CosmianKmsSecretConfig; + + pub(super) struct CosmianKmsBackend { + token: Option, + insecure_certs: bool, + } + + impl CosmianKmsBackend { + pub(super) fn new(cfg: &CosmianKmsSecretConfig) -> Self { + Self { + token: cfg.cosmian_kms_secret_token.clone(), + insecure_certs: cfg.cosmian_kms_insecure_certs, + } + } + } + + impl SecretBackend for CosmianKmsBackend { + fn resolve(&self, uri: &str) -> KResult { + let rest = uri + .strip_prefix("secret://") + .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; + let slash = rest.find('/').ok_or_else(|| { + KmsError::InvalidRequest(format!( + "cosmian-kms URI must be secret://[:]/, got: {uri}" + )) + })?; + let host_port = &rest[..slash]; + let object_id = rest[slash + 1..].to_owned(); + if object_id.is_empty() { + return Err(KmsError::InvalidRequest(format!( + "cosmian-kms URI must have a non-empty object ID: {uri}" + ))); + } + let is_local = host_port.starts_with("localhost") + || host_port.starts_with("127.0.0.1") + || host_port.starts_with("[::1]"); + let scheme = if is_local || self.insecure_certs { + "http" + } else { + "https" + }; + let server_url = if host_port.contains(':') { + format!("{scheme}://{host_port}") + } else { + format!("{scheme}://{host_port}:9998") + }; + let token = self.token.clone(); + let insecure = self.insecure_certs; + let uri_owned = uri.to_owned(); + run_blocking(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? + .block_on(fetch_object( + &server_url, + &object_id, + token.as_deref(), + insecure, + &uri_owned, + )) + }) + } + } + + async fn fetch_object( + url: &str, + id: &str, + token: Option<&str>, + insecure: bool, + uri: &str, + ) -> KResult { + let req_ttlv = to_ttlv(&Get::from(id)).map_err(|e| { + KmsError::ServerError(format!("KMIP Get serialise error for {uri}: {e}")) + })?; + let mut b = reqwest::Client::builder(); + if insecure { + b = b.danger_accept_invalid_certs(true); + } + let client = b + .build() + .map_err(|e| KmsError::ServerError(format!("HTTP client build error: {e}")))?; + let endpoint = format!("{}/kmip/2_1", url.trim_end_matches('/')); + let mut req = client.post(&endpoint).json(&req_ttlv); + if let Some(t) = token { + req = req.bearer_auth(t); + } + let resp = req.send().await.map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS request failed for {uri}: {e}")) + })?; + if !resp.status().is_success() { + return Err(KmsError::ServerError(format!( + "Cosmian KMS HTTP {} for {uri}", + resp.status() + ))); + } + let ttlv: TTLV = resp.json().await.map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS response parse error for {uri}: {e}")) + })?; + let get_resp: GetResponse = from_ttlv(ttlv).map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS deserialise error for {uri}: {e}")) + })?; + extract_secret(get_resp.object, uri) + } + + fn extract_secret(object: Object, uri: &str) -> KResult { + match object { + Object::SecretData(sd) => { + let bytes = sd.key_block.key_bytes().map_err(|e| { + KmsError::ServerError(format!("SecretData key_bytes error at {uri}: {e}")) + })?; + String::from_utf8(bytes.to_vec()).map_err(|e| { + KmsError::ServerError(format!("SecretData non-UTF-8 at {uri}: {e}")) + }) + } + Object::OpaqueObject(o) => String::from_utf8(o.opaque_data_value).map_err(|e| { + KmsError::ServerError(format!("OpaqueObject non-UTF-8 at {uri}: {e}")) + }), + other => Err(KmsError::ServerError(format!( + "Cosmian KMS object at {uri} has type {:?}; expected SecretData or OpaqueObject", + other.object_type() + ))), + } + } +} diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index 6a02f9f7f..5473b233c 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -346,6 +346,7 @@ mod tests { default_unwrap_type: None, non_revocable_key_id: None, privileged_users: None, + secret_backends: cosmian_kms_server::config::SecretBackendConfig::default(), print_default_config: false, }; diff --git a/crate/server/src/routes/aws_xks/aws_xks_config.rs b/crate/server/src/routes/aws_xks/aws_xks_config.rs index c91872b38..6ead6196f 100644 --- a/crate/server/src/routes/aws_xks/aws_xks_config.rs +++ b/crate/server/src/routes/aws_xks/aws_xks_config.rs @@ -1,10 +1,15 @@ +// Field names intentionally share a prefix (`aws_xks_*`) for disambiguation in +// flat CLI / env-var namespaces. The same prefix appears in the structs +// generated by `ConfigArgs`, so we suppress the lint at file level. +#![allow(clippy::struct_field_names)] + use clap::Args; +use clap_config_fallback::ConfigArgs; use serde::{Deserialize, Serialize}; -#[derive(Debug, Args, Deserialize, Serialize, Clone)] +#[derive(Debug, Args, ConfigArgs, Deserialize, Serialize, Clone)] #[serde(default)] #[derive(Default)] -#[allow(clippy::struct_field_names)] pub struct AwsXksConfig { /// This setting turns on endpoints handling the AWS XKS feature #[clap(long, env = "KMS_AWS_XKS_ENABLE", default_value = "false")] diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index 2017701c6..3e82b7626 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1 +1 @@ -sha256-BLYlBznRLu5RBetai9ZGsdb8C7cpm5PLCIf3/MJMOnM= +sha256-plobon7/NDFYFPcdHwz6qg7lQ4IXJ45rTRJbsBizkzM= diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 986451d83..80aaf3645 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1 +1 @@ -sha256-ZzuWLgecB3Hpz1TyDw0/ew6dihrrkdNP3iq+Ts8WEHw= +sha256-5R15t1lUFy77NVRl+9fRQ4CcqC0Af8iEOWsS9xewAW4= diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index d1b16e798..4f180e2ac 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1 @@ -sha256-UUy49PEGv1Xe60YAxUD8rbPnIO0B08CpiHL9vClfXjY= +sha256-LGDg3tXMnlIS0V9KyuuT6PpWSXaZc6qrgYkOLHrTBMY= diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index 81b379abb..9e10bba6d 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-/C3v8txjB8P8mRbYkcT8hbBRapzB4cdbTBdmZFsphlo= +sha256-tlJl4+o7OCL6WKCBgrvEey40IbH7y0c4fVIajrKo5qg= diff --git a/pkg/kms.toml b/pkg/kms.toml index e7f05c51b..6ed2e5fea 100644 --- a/pkg/kms.toml +++ b/pkg/kms.toml @@ -1,6 +1,48 @@ # The default username to use when no authentication method is provided default_username = "admin" +# Optional: path to a secrets file containing sensitive values (passwords, tokens, credentials). +# The secrets file is a TOML file with the same structure as this file; its values are deep-merged +# over this file at startup (secrets file takes precedence). +# This allows keeping sensitive data in a separate, root-only file (e.g. mode 400) while sharing +# the main config with teams. +# Can also be set via the COSMIAN_KMS_SECRETS_CONF environment variable (takes precedence over this key). +# Example: +# secrets_file = "/etc/cosmian/kms-secrets.toml" + +# Any string value in this file (and in the secrets file) can reference environment variables +# using the ${VAR_NAME} syntax. The variable is resolved at startup and must be set. +# Example: +# database_url = "postgresql://kms:${KMS_DB_PASSWORD}@db.internal/kms" + +# Phase-2: secret URI backends (requires building with the matching feature flag). +# Any string value can use a secret URI instead of a literal; the value is fetched at startup. +# +# Scheme Feature flag Description +# ─────────────── ─────────────── ────────────────────────────────────────────── +# vault:// secret-vault HashiCorp Vault KV-v2 +# Format: vault:///[#] +# Required env: VAULT_ADDR, VAULT_TOKEN +# Example: vault://secret/kms/db#password +# +# aws-ssm:// secret-aws AWS Systems Manager Parameter Store +# Format: aws-ssm:/// +# Required: standard AWS credential chain +# Example: aws-ssm://eu-west-1/kms/prod/db-url +# +# azure-kv:// secret-azure Azure Key Vault +# Format: azure-kv:///secrets/[/] +# Required env: AZURE_TENANT_ID, AZURE_CLIENT_ID, +# AZURE_CLIENT_SECRET +# Example: azure-kv://my-vault/secrets/kms-tls-p12 +# +# cosmian-kms:// secret-cosmian-kms Another Cosmian KMS server (KMIP Get) +# Format: cosmian-kms://[:]/ +# Fetches a SecretData or OpaqueObject via KMIP 2.1 +# Optional env: COSMIAN_KMS_TOKEN (Bearer auth), +# COSMIAN_KMS_INSECURE_CERTS (skip TLS verify) +# Example: cosmian-kms://kms.internal:9998/b4c2f00a-1234-5678-abcd-ef0123456789 + # When an authentication method is provided, perform the authentication # but always use the default username instead of the one provided by the authentication method force_default_username = false @@ -48,6 +90,8 @@ info = false # see `--hsm_slot` for more information. # Set `KMS_HSM_PASSWORD` to avoid the password appearing in `ps` output. # hsm_password = ["changeme"] # Login passwords (same order as hsm_slot) +# hsm_password = ["${KMS_HSM_SLOT1_PASSWORD}", "${KMS_HSM_SLOT2_PASSWORD}"] +# hsm_password = ["vault://secret/kms/hsm-slot1", "vault://secret/kms/hsm-slot2"] # # ── New multi-instance format ───────────────────────────────────────────────── # Uses the new UID convention: hsm:::::: @@ -101,10 +145,17 @@ info = false database_type = "sqlite" # The URL of the database for `Postgres`, `MySQL`, or `Findex-Redis` # database_url = "" +# Sensitive: use env var interpolation or a secret URI backend for the password: +# database_url = "postgresql://kms:${KMS_DB_PASSWORD}@db.internal/kms" +# database_url = "vault://secret/kms/db#url" +# database_url = "aws-ssm://eu-west-1//kms/prod/db-url" +# database_url = "azure-kv://my-vault/secrets/kms-db-url" +# database_url = "cosmian-kms://kms.internal:9998/b4c2f00a-1234-5678-abcd-ef0123456789" # The directory path of the `SQLite` # sqlite_path = "" # redis-findex: a master password used to encrypt the Redis data and indexes # redis_master_password = "" +# redis_master_password = "${KMS_REDIS_MASTER_PASSWORD}" # Clear the database on start. # WARNING: This will delete ALL the data in the database @@ -142,6 +193,8 @@ unwrapped_cache_max_age = 15 # minutes # tls_p12_file = "[tls p12 file]" # The password to open the PKCS#12 Certificates and Key file # tls_p12_password = "[tls p12 password]" +# tls_p12_password = "${KMS_TLS_P12_PASSWORD}" +# tls_p12_password = "vault://secret/kms/tls#p12_password" # The server's optional X. 509 certificate in PEM format validates the client certificate presented for authentication. # If provided, clients must present a certificate signed by this authority for authentication. diff --git a/shell.nix b/shell.nix index d234bc402..3a87dffd0 100644 --- a/shell.nix +++ b/shell.nix @@ -44,7 +44,9 @@ let withOpenssh = (builtins.getEnv "WITH_OPENSSH") == "1"; # LUKS disk-encryption PKCS#11 test: pkcs11-tool (opensc) lists objects on Linux withLuks = (builtins.getEnv "WITH_LUKS") == "1"; - # IRIS mTLS test: Docker CLI must be available inside the nix-shell to pull and run the IRIS container + # AWS secret backend test: awscli2 is needed to create/delete SSM parameters + withAws = (builtins.getEnv "WITH_AWS") == "1"; + # IRIS mTLS test / Vault secret backend test: Docker CLI must be available inside the nix-shell to pull and run containers withDocker = (builtins.getEnv "WITH_DOCKER") == "1"; rustToolchain = @@ -180,7 +182,9 @@ pkgs.mkShell { ++ pkgs.lib.optionals (withOpenssh && pkgs.stdenv.isLinux) [ pkgs.openssh ] # LUKS disk-encryption test: include opensc for pkcs11-tool on Linux CI ++ pkgs.lib.optionals (withLuks && pkgs.stdenv.isLinux) [ pkgs.opensc ] - # IRIS mTLS test: include docker CLI so the nix-shell pure environment can pull and run containers + # AWS secret backend test: include awscli2 for SSM parameter management + ++ pkgs.lib.optionals withAws [ pkgs.awscli2 ] + # IRIS mTLS test + Vault secret backend test: include docker CLI ++ pkgs.lib.optionals withDocker [ pkgs.docker ]; shellHook = '' From 97f7e78dcd5e047a2dd387cea054c898b59739de Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Mon, 15 Jun 2026 16:56:15 +0200 Subject: [PATCH 2/5] fix: use parse_borrowed for time format description and pin tracing-appender to 0.2.4 --- Cargo.lock | 27 +++++++------------ .../client_utils/src/attributes_utils.rs | 4 +-- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b27569814..d078b2635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -437,7 +437,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2182,7 +2182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3520,7 +3520,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4715,7 +4715,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5173,7 +5173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5287,12 +5287,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - [[package]] name = "syn" version = "2.0.117" @@ -5345,7 +5339,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5807,12 +5801,11 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -6343,7 +6336,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/crate/clients/client_utils/src/attributes_utils.rs b/crate/clients/client_utils/src/attributes_utils.rs index bacfe8ab1..b2131b84c 100644 --- a/crate/clients/client_utils/src/attributes_utils.rs +++ b/crate/clients/client_utils/src/attributes_utils.rs @@ -10,7 +10,7 @@ use cosmian_kmip::kmip_2_1::{ }; use serde_json::Value; use strum::{EnumIter, EnumString, IntoEnumIterator}; -use time::{OffsetDateTime, format_description::parse}; +use time::{OffsetDateTime, format_description}; use crate::{ error::UtilsError, @@ -461,7 +461,7 @@ pub fn build_selected_attribute( ) -> Result { let attribute = match attribute_name { "activation_date" => { - let format = parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z") + let format = format_description::parse_borrowed::<2>("[year]-[month]-[day]T[hour]:[minute]:[second]Z") .map_err(|e| UtilsError::Default(e.to_string()))?; let activation_date = OffsetDateTime::parse(&attribute_value, &format) .map_err(|e| UtilsError::Default(e.to_string()))?; From ed752857e9120884b12056d4d7df5b285b084f4c Mon Sep 17 00:00:00 2001 From: Manuthor Date: Tue, 16 Jun 2026 07:49:35 +0200 Subject: [PATCH 3/5] chore: reorganize secret_backends module while splitting secret_resolver --- .github/scripts/test/test_secret_aws.sh | 9 +- .github/scripts/test/test_secret_azure.sh | 20 +- .github/scripts/test/test_secret_vault.sh | 18 +- Cargo.lock | 2 - _typos.toml | 4 + .../client_utils/src/attributes_utils.rs | 6 +- crate/server/Cargo.toml | 6 - .../src/config/command_line/clap_config.rs | 9 +- .../config/command_line/secret_backends.rs | 144 ----- .../command_line/secret_backends/aws.rs | 137 ++++ .../command_line/secret_backends/azure.rs | 85 +++ .../command_line/secret_backends/common.rs | 71 +++ .../secret_backends/cosmian_kms.rs | 131 ++++ .../command_line/secret_backends/mod.rs | 297 +++++++++ .../command_line/secret_backends/vault.rs | 63 ++ crate/server/src/config/mod.rs | 1 - crate/server/src/config/secret_resolver.rs | 592 ------------------ .../docs/configuration/secret_backends.md | 200 ++++++ documentation/mkdocs.yml | 93 +-- test_data | 2 +- 20 files changed, 1067 insertions(+), 823 deletions(-) delete mode 100644 crate/server/src/config/command_line/secret_backends.rs create mode 100644 crate/server/src/config/command_line/secret_backends/aws.rs create mode 100644 crate/server/src/config/command_line/secret_backends/azure.rs create mode 100644 crate/server/src/config/command_line/secret_backends/common.rs create mode 100644 crate/server/src/config/command_line/secret_backends/cosmian_kms.rs create mode 100644 crate/server/src/config/command_line/secret_backends/mod.rs create mode 100644 crate/server/src/config/command_line/secret_backends/vault.rs delete mode 100644 crate/server/src/config/secret_resolver.rs create mode 100644 documentation/docs/configuration/secret_backends.md diff --git a/.github/scripts/test/test_secret_aws.sh b/.github/scripts/test/test_secret_aws.sh index 956da49fa..89fe4ed90 100644 --- a/.github/scripts/test/test_secret_aws.sh +++ b/.github/scripts/test/test_secret_aws.sh @@ -51,14 +51,13 @@ aws ssm put-parameter \ --region "${AWS_REGION}" \ --overwrite -echo "Building cosmian_kms_server with secret-aws feature..." -cargo build -p cosmian_kms_server +echo "Building cosmian_kms_server..." +cargo build ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server echo "Running AWS SSM integration test..." -AWS_REGION="${AWS_REGION}" \ KMS_TEST_AWS_SSM_URI="aws-ssm://${AWS_REGION}${PARAM_NAME}" \ -KMS_TEST_AWS_SSM_EXPECTED="${SECRET_VALUE}" \ -cargo test -p cosmian_kms_server --lib -- \ + KMS_TEST_AWS_SSM_EXPECTED="${SECRET_VALUE}" \ + cargo test ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server --lib -- \ --ignored --nocapture test_secret_aws_ssm echo "AWS SSM secret backend test completed successfully." diff --git a/.github/scripts/test/test_secret_azure.sh b/.github/scripts/test/test_secret_azure.sh index 15f62717f..783f98c47 100644 --- a/.github/scripts/test/test_secret_azure.sh +++ b/.github/scripts/test/test_secret_azure.sh @@ -13,8 +13,6 @@ set -x # AZURE_CLIENT_ID — service-principal application (client) ID # AZURE_CLIENT_SECRET — service-principal secret # AZURE_KV_NAME — Key Vault name (e.g. keyvault-workload-v3) -# AZURE_KV_SP_OBJECT_ID — Object ID of the service principal in Enterprise -# Applications (for role assignment) # SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) @@ -48,8 +46,8 @@ get_kv_token() { -d "grant_type=client_credentials" \ -d "client_id=${AZURE_CLIENT_ID}" \ -d "client_secret=${AZURE_CLIENT_SECRET}" \ - -d "scope=https://vault.azure.net/.default" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" + -d "scope=https://vault.azure.net/.default" | + python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" } cleanup() { @@ -92,16 +90,16 @@ if [ "${PUT_STATUS}" -lt 200 ] || [ "${PUT_STATUS}" -ge 300 ]; then fi echo "Secret created (HTTP ${PUT_STATUS})" -echo "Building cosmian_kms_server with secret-azure feature..." -cargo build -p cosmian_kms_server +echo "Building cosmian_kms_server..." +cargo build ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server echo "Running Azure KV integration test..." AZURE_TENANT_ID="${AZURE_TENANT_ID}" \ -AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" \ -AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET}" \ -KMS_TEST_AZURE_KV_URI="azure-kv://${AZURE_KV_NAME}/secrets/${SECRET_NAME}" \ -KMS_TEST_AZURE_KV_EXPECTED="${SECRET_VALUE}" \ -cargo test -p cosmian_kms_server --lib -- \ + AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" \ + AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET}" \ + KMS_TEST_AZURE_KV_URI="azure-kv://${AZURE_KV_NAME}/secrets/${SECRET_NAME}" \ + KMS_TEST_AZURE_KV_EXPECTED="${SECRET_VALUE}" \ + cargo test ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server --lib -- \ --ignored --nocapture test_secret_azure_kv echo "Azure Key Vault secret backend test completed successfully." diff --git a/.github/scripts/test/test_secret_vault.sh b/.github/scripts/test/test_secret_vault.sh index 2b8ed59c2..0b61e7a28 100644 --- a/.github/scripts/test/test_secret_vault.sh +++ b/.github/scripts/test/test_secret_vault.sh @@ -2,7 +2,7 @@ set -euo pipefail set -x -# Secret backend integration test — HashiCorp Vault KV-v2 +# Secret backend integration test — HashCorp Vault KV-v2 # # Starts a dev-mode Vault container, creates a test secret, runs the Rust # #[ignore] integration test, then cleans up. @@ -21,7 +21,7 @@ require_cmd docker "Docker is required to run the Vault container." require_cmd curl "curl is required for Vault readiness checks." echo "=========================================" -echo "Running secret backend test: HashiCorp Vault" +echo "Running secret backend test: HashCorp Vault" echo "Variant: ${VARIANT_NAME}" echo "=========================================" @@ -73,7 +73,7 @@ echo "Enabling KV-v2 secrets engine at mount '${VAULT_MOUNT}'..." curl -sf -X POST \ -H "X-Vault-Token: ${VAULT_TOKEN}" \ -d '{"type":"kv","options":{"version":"2"}}' \ - "${VAULT_ADDR}/v1/sys/mounts/${VAULT_MOUNT}" || true # may already exist in dev mode + "${VAULT_ADDR}/v1/sys/mounts/${VAULT_MOUNT}" || true # may already exist in dev mode echo "Writing test secret ${VAULT_MOUNT}/${VAULT_PATH}#${VAULT_FIELD}..." curl -sf -X POST \ @@ -82,15 +82,15 @@ curl -sf -X POST \ -d "{\"data\":{\"${VAULT_FIELD}\":\"${SECRET_VALUE}\"}}" \ "${VAULT_ADDR}/v1/${VAULT_MOUNT}/data/${VAULT_PATH}" -echo "Building cosmian_kms_server with secret-vault feature..." -cargo build -p cosmian_kms_server +echo "Building cosmian_kms_server..." +cargo build ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server echo "Running Vault integration test..." VAULT_ADDR="${VAULT_ADDR}" \ -VAULT_TOKEN="${VAULT_TOKEN}" \ -KMS_TEST_VAULT_URI="vault://${VAULT_MOUNT}/${VAULT_PATH}#${VAULT_FIELD}" \ -KMS_TEST_VAULT_EXPECTED="${SECRET_VALUE}" \ -cargo test -p cosmian_kms_server --lib -- \ + VAULT_TOKEN="${VAULT_TOKEN}" \ + KMS_TEST_VAULT_URI="vault://${VAULT_MOUNT}/${VAULT_PATH}#${VAULT_FIELD}" \ + KMS_TEST_VAULT_EXPECTED="${SECRET_VALUE}" \ + cargo test ${FEATURES_FLAG[@]+${FEATURES_FLAG[@]}} -p cosmian_kms_server --lib -- \ --ignored --nocapture test_secret_vault echo "Vault secret backend test completed successfully." diff --git a/Cargo.lock b/Cargo.lock index d078b2635..5718ee8b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1442,7 +1442,6 @@ dependencies = [ "futures", "governor", "hex", - "hmac 0.12.1", "http 1.4.2", "jsonwebtoken", "native-tls", @@ -1458,7 +1457,6 @@ dependencies = [ "scratchstack-aws-signature", "serde", "serde_json", - "sha2 0.10.9", "smartcardhsm_pkcs11_loader", "softhsm2_pkcs11_loader", "strum 0.27.2", diff --git a/_typos.toml b/_typos.toml index 8d5c9c3ab..0ca8abdfc 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,9 +1,13 @@ +[default.extend-identifiers] +HashiCorp = "HashiCorp" + [default.extend-words] Veeam = "Veeam" GOST = "GOST" vas = "vas" passin = "passin" typ = "typ" +Hashi = "Hashi" # Unicode hex code points inside SVG font unicode-range (e.g. U+1F6BA) BA = "BA" # Common standards acronym in cryptographic references. diff --git a/crate/clients/client_utils/src/attributes_utils.rs b/crate/clients/client_utils/src/attributes_utils.rs index b2131b84c..c5b5a7aca 100644 --- a/crate/clients/client_utils/src/attributes_utils.rs +++ b/crate/clients/client_utils/src/attributes_utils.rs @@ -461,8 +461,10 @@ pub fn build_selected_attribute( ) -> Result { let attribute = match attribute_name { "activation_date" => { - let format = format_description::parse_borrowed::<2>("[year]-[month]-[day]T[hour]:[minute]:[second]Z") - .map_err(|e| UtilsError::Default(e.to_string()))?; + let format = format_description::parse_borrowed::<2>( + "[year]-[month]-[day]T[hour]:[minute]:[second]Z", + ) + .map_err(|e| UtilsError::Default(e.to_string()))?; let activation_date = OffsetDateTime::parse(&attribute_value, &format) .map_err(|e| UtilsError::Default(e.to_string()))?; Attribute::ActivationDate(activation_date) diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index a7ce08eb1..847f1d7a9 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -80,7 +80,6 @@ dialoguer = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } hex = { workspace = true, features = ["serde"] } -hmac = { workspace = true } http = { workspace = true } jsonwebtoken = { workspace = true } num-bigint-dig = { workspace = true, features = [ @@ -118,7 +117,6 @@ utimaco_pkcs11_loader = { path = "../hsm/utimaco", version = "5.23.0" } uuid = { workspace = true, features = ["v4"] } x509-parser = { workspace = true } zeroize = { workspace = true } -sha2.workspace = true [target.'cfg(windows)'.dependencies] windows-service = "0.8" @@ -134,10 +132,6 @@ tempfile = { workspace = true } [build-dependencies] actix-http = { workspace = true } time = { workspace = true, features = ["local-offset", "formatting"] } -sha2 = { workspace = true } - -[package.metadata.cargo-machete] -ignored = ["sha2"] # ------------------------------------------------------------------------------ # START DEBIAN PACKAGING diff --git a/crate/server/src/config/command_line/clap_config.rs b/crate/server/src/config/command_line/clap_config.rs index 46192006c..91cb97339 100644 --- a/crate/server/src/config/command_line/clap_config.rs +++ b/crate/server/src/config/command_line/clap_config.rs @@ -5,14 +5,13 @@ use std::{ }; use clap::{CommandFactory, Parser}; - -use super::secret_backends::SecretBackendConfig; use cosmian_kms_server_database::reexport::cosmian_kmip::kmip_2_1::extra::tagging::VENDOR_ID_COSMIAN; use serde::{Deserialize, Serialize}; use super::{ GoogleCseConfig, HsmConfig, HttpConfig, IdpAuthConfig, KmipPolicyConfig, MainDBConfig, - WorkspaceConfig, logging::LoggingConfig, ui_config::UiConfig, + WorkspaceConfig, logging::LoggingConfig, secret_backends::SecretBackendConfig, + ui_config::UiConfig, }; use crate::{ config::{AzureEkmConfig, ProxyConfig, SocketServerConfig, TlsConfig}, @@ -450,7 +449,7 @@ impl ClapConfig { )) })?; - crate::config::secret_resolver::resolve_config( + super::secret_backends::resolve_config( &mut config_value, &preliminary.secret_backends, )?; @@ -547,6 +546,8 @@ impl ClapConfig { } } +// `secret_backends` is intentionally excluded to avoid leaking credentials in logs. +#[allow(clippy::missing_fields_in_debug)] impl fmt::Debug for ClapConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut x = f.debug_struct(""); diff --git a/crate/server/src/config/command_line/secret_backends.rs b/crate/server/src/config/command_line/secret_backends.rs deleted file mode 100644 index b41b67476..000000000 --- a/crate/server/src/config/command_line/secret_backends.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Clap credential structs for secret URI backends. -//! -//! This file contains **only** clap structs — no logic, no HTTP calls. -//! The actual resolution is performed elsewhere once the config is loaded. - -use clap::{Args, ValueEnum}; - -/// Selects which backend resolves `secret://` URIs in the KMS config file. -/// -/// Pass via `--secret-backend ` or the `KMS_SECRET_BACKEND` env var. -/// The TOML config uses the neutral `secret://` scheme; which backend is -/// actually used is **never** revealed in the config file itself. -#[derive(Clone, Default, ValueEnum, Debug)] -pub enum SecretBackendKind { - /// `HashiCorp` Vault KV-v2. - /// Path format: `secret:///[#]` - #[default] - #[value(name = "vault")] - Vault, - /// AWS Systems Manager Parameter Store. - /// Path format: `secret:///` - #[value(name = "aws-ssm")] - AwsSsm, - /// Azure Key Vault. - /// Path format: `secret:///secrets/[/]` - #[value(name = "azure-kv")] - AzureKv, - /// Another Cosmian KMS server (KMIP Get). - /// Path format: `secret://[:]/` - #[value(name = "cosmian-kms")] - CosmianKms, -} - -/// Credentials for the `HashiCorp` Vault KV-v2 backend. -/// -/// Injected exclusively via clap (CLI flags or environment variables). -/// Never read from the TOML config file. -#[derive(Args, Clone, Default)] -pub struct VaultBackendConfig { - /// Vault server URL (e.g. `https://vault.internal:8200`). - /// - /// Required when `secret://` URIs are resolved via the `vault` backend. - #[clap(long, env = "VAULT_ADDR", default_value = "http://127.0.0.1:8200")] - pub vault_addr: String, - - /// Vault token with `read` access on the target paths. - /// - /// Required when `secret://` URIs are resolved via the `vault` backend. - #[clap(long, env = "VAULT_TOKEN", default_value = "")] - pub vault_token: String, -} - -/// Credentials for the AWS Systems Manager Parameter Store backend. -/// -/// Injected exclusively via clap (CLI flags or environment variables). -/// Never read from the TOML config file. -#[derive(Args, Clone, Default)] -pub struct AwsSsmBackendConfig { - /// AWS access key ID. - /// - /// Required when `secret://` URIs are resolved via the `aws-ssm` backend. - #[clap(long, env = "AWS_ACCESS_KEY_ID", default_value = "")] - pub aws_access_key_id: String, - - /// AWS secret access key. - /// - /// Required when `secret://` URIs are resolved via the `aws-ssm` backend. - #[clap(long, env = "AWS_SECRET_ACCESS_KEY", default_value = "")] - pub aws_secret_access_key: String, - - /// AWS session token (for temporary credentials / STS). - #[clap(long, env = "AWS_SESSION_TOKEN")] - pub aws_session_token: Option, -} - -/// Credentials for the Azure Key Vault backend. -/// -/// Injected exclusively via clap (CLI flags or environment variables). -/// Never read from the TOML config file. -#[derive(Args, Clone, Default)] -pub struct AzureKvBackendConfig { - /// Azure AD tenant ID. - /// - /// Required when `secret://` URIs are resolved via the `azure-kv` backend. - #[clap(long, env = "AZURE_TENANT_ID", default_value = "")] - pub azure_tenant_id: String, - - /// Azure service-principal application (client) ID. - /// - /// Required when `secret://` URIs are resolved via the `azure-kv` backend. - #[clap(long, env = "AZURE_CLIENT_ID", default_value = "")] - pub azure_client_id: String, - - /// Azure service-principal client secret. - /// - /// Required when `secret://` URIs are resolved via the `azure-kv` backend. - #[clap(long, env = "AZURE_CLIENT_SECRET", default_value = "")] - pub azure_client_secret: String, -} - -/// Credentials for the Cosmian KMS secret backend. -/// -/// Injected exclusively via clap (CLI flags or environment variables). -/// Never read from the TOML config file. -#[derive(Args, Clone, Default)] -pub struct CosmianKmsSecretConfig { - /// Bearer token / API key for authenticating to the Cosmian KMS secret backend. - #[clap(long, env = "COSMIAN_KMS_SECRET_TOKEN")] - pub cosmian_kms_secret_token: Option, - - /// Skip TLS certificate verification for the Cosmian KMS secret backend. - /// - /// **For development and testing only.** Never enable in production. - #[clap(long, env = "COSMIAN_KMS_INSECURE_CERTS")] - pub cosmian_kms_insecure_certs: bool, -} - -/// Authentication credentials and backend selection for secret URI resolution. -/// -/// These values are provided exclusively via clap (CLI flags or environment -/// variables) and are **never** read from the TOML config file, preventing -/// accidental secret exposure through configuration files. -#[derive(Args, Clone, Default)] -pub struct SecretBackendConfig { - /// Secret backend to use for resolving `secret://` URIs in the config file. - /// - /// When set, all `secret://` values in the config file are resolved through - /// the selected backend. Credentials must be provided via the corresponding - /// flags or environment variables below. - #[clap(long, env = "KMS_SECRET_BACKEND", value_enum)] - pub backend: Option, - - #[command(flatten)] - pub vault: VaultBackendConfig, - - #[command(flatten)] - pub aws: AwsSsmBackendConfig, - - #[command(flatten)] - pub azure: AzureKvBackendConfig, - - #[command(flatten)] - pub cosmian_kms: CosmianKmsSecretConfig, -} diff --git a/crate/server/src/config/command_line/secret_backends/aws.rs b/crate/server/src/config/command_line/secret_backends/aws.rs new file mode 100644 index 000000000..416ea3b52 --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/aws.rs @@ -0,0 +1,137 @@ +//! AWS Systems Manager Parameter Store backend. + +use chrono::Utc; +use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer}; + +use super::{ + AwsSsmBackendConfig, KResult, KmsError, SecretBackend, + common::{http_json, parse_uri, require_non_empty, resolve_async}, +}; + +pub(super) struct AwsSsmBackend { + access_key_id: String, + secret_access_key: String, + session_token: Option, +} + +impl AwsSsmBackend { + pub(super) fn new(cfg: &AwsSsmBackendConfig) -> Self { + Self { + access_key_id: cfg.aws_access_key_id.clone(), + secret_access_key: cfg.aws_secret_access_key.clone(), + session_token: cfg.aws_session_token.clone(), + } + } + + /// Build a SigV4-signed request and call the SSM `GetParameter` API. + async fn call_ssm(&self, region: &str, param: &str, uri: &str) -> KResult { + let now = Utc::now(); + let dt = now.format("%Y%m%dT%H%M%SZ").to_string(); + let date = now.format("%Y%m%d").to_string(); + let host = format!("ssm.{region}.amazonaws.com"); + let body = serde_json::json!({"Name": param, "WithDecryption": true}).to_string(); + let payload_hash = sha256_hex(body.as_bytes())?; + + let (canon_hdrs, signed_hdrs) = + Self::canonical_headers(&host, &dt, self.session_token.as_deref()); + let canon_req = format!("POST\n/\n\n{canon_hdrs}\n{signed_hdrs}\n{payload_hash}"); + let scope = format!("{date}/{region}/ssm/aws4_request"); + let sts = format!( + "AWS4-HMAC-SHA256\n{dt}\n{scope}\n{}", + sha256_hex(canon_req.as_bytes())? + ); + let key = signing_key(&self.secret_access_key, &date, region, "ssm")?; + let sig = hex::encode(&hmac_sha256(&key, sts.as_bytes())?); + let auth = format!( + "AWS4-HMAC-SHA256 Credential={}/{scope}, SignedHeaders={signed_hdrs}, Signature={sig}", + self.access_key_id + ); + + let mut req = reqwest::Client::new() + .post(format!("https://{host}/")) + .header("Content-Type", "application/x-amz-json-1.1") + .header("X-Amz-Date", &dt) + .header("X-Amz-Target", "AmazonSSM.GetParameter") + .header("Authorization", &auth); + if let Some(t) = &self.session_token { + req = req.header("X-Amz-Security-Token", t); + } + + http_json(req.body(body), uri, "AWS SSM").await + } + + fn canonical_headers(host: &str, dt: &str, token: Option<&str>) -> (String, String) { + token.map_or_else( + || ( + format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-target:AmazonSSM.GetParameter\n"), + "content-type;host;x-amz-date;x-amz-target".to_owned(), + ), + |t| ( + format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-security-token:{t}\nx-amz-target:AmazonSSM.GetParameter\n"), + "content-type;host;x-amz-date;x-amz-security-token;x-amz-target".to_owned(), + ), + ) + } +} + +// ─── SigV4 helpers (using OpenSSL) ─────────────────────────────────────────── + +fn sha256_hex(data: &[u8]) -> KResult { + let digest = openssl::hash::hash(MessageDigest::sha256(), data) + .map_err(|e| KmsError::ServerError(format!("SHA-256 error: {e}")))?; + Ok(hex::encode(digest)) +} + +fn hmac_sha256(key: &[u8], data: &[u8]) -> KResult> { + let pkey = + PKey::hmac(key).map_err(|e| KmsError::ServerError(format!("HMAC key error: {e}")))?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey) + .map_err(|e| KmsError::ServerError(format!("HMAC signer init error: {e}")))?; + signer + .update(data) + .map_err(|e| KmsError::ServerError(format!("HMAC update error: {e}")))?; + signer + .sign_to_vec() + .map_err(|e| KmsError::ServerError(format!("HMAC finalize error: {e}"))) +} + +fn signing_key(secret: &str, date: &str, region: &str, service: &str) -> KResult> { + let k = hmac_sha256(format!("AWS4{secret}").as_bytes(), date.as_bytes())?; + let k = hmac_sha256(&k, region.as_bytes())?; + let k = hmac_sha256(&k, service.as_bytes())?; + hmac_sha256(&k, b"aws4_request") +} + +// ─── SecretBackend impl ───────────────────────────────────────────────────── + +impl SecretBackend for AwsSsmBackend { + fn resolve(&self, uri: &str) -> KResult { + require_non_empty(&self.access_key_id, "AWS_ACCESS_KEY_ID", "aws-ssm")?; + require_non_empty(&self.secret_access_key, "AWS_SECRET_ACCESS_KEY", "aws-ssm")?; + + let (region, param_with_slash) = parse_uri(uri, "aws-ssm", "secret:///")?; + let param_name = format!("/{param_with_slash}"); + let region = region.to_owned(); + let uri_owned = uri.to_owned(); + // Clone self fields for the 'static async block + let backend = Self { + access_key_id: self.access_key_id.clone(), + secret_access_key: self.secret_access_key.clone(), + session_token: self.session_token.clone(), + }; + + resolve_async(async move { + let json = backend.call_ssm(®ion, ¶m_name, &uri_owned).await?; + + json.get("Parameter") + .and_then(|p| p.get("Value")) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!( + "AWS SSM missing Parameter.Value for {uri_owned}" + )) + }) + .map(str::to_owned) + }) + } +} diff --git a/crate/server/src/config/command_line/secret_backends/azure.rs b/crate/server/src/config/command_line/secret_backends/azure.rs new file mode 100644 index 000000000..2af6fe6d4 --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/azure.rs @@ -0,0 +1,85 @@ +//! Azure Key Vault backend. + +use super::{ + AzureKvBackendConfig, KResult, KmsError, SecretBackend, + common::{http_json, parse_uri, require_non_empty, resolve_async}, +}; + +pub(super) struct AzureKvBackend { + tenant_id: String, + client_id: String, + client_secret: String, +} + +impl AzureKvBackend { + pub(super) fn new(cfg: &AzureKvBackendConfig) -> Self { + Self { + tenant_id: cfg.azure_tenant_id.clone(), + client_id: cfg.azure_client_id.clone(), + client_secret: cfg.azure_client_secret.clone(), + } + } + + /// Obtain an `OAuth2` access token from Azure AD using client credentials. + async fn acquire_token(&self, uri: &str) -> KResult { + let token_url = format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + self.tenant_id + ); + let json = http_json( + reqwest::Client::new().post(&token_url).form(&[ + ("grant_type", "client_credentials"), + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ("scope", "https://vault.azure.net/.default"), + ]), + uri, + "Azure AD", + ) + .await?; + + json.get("access_token") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| KmsError::ServerError("No access_token in Azure AD response".to_owned())) + .map(str::to_owned) + } +} + +impl SecretBackend for AzureKvBackend { + fn resolve(&self, uri: &str) -> KResult { + require_non_empty(&self.tenant_id, "AZURE_TENANT_ID", "azure-kv")?; + require_non_empty(&self.client_id, "AZURE_CLIENT_ID", "azure-kv")?; + require_non_empty(&self.client_secret, "AZURE_CLIENT_SECRET", "azure-kv")?; + + let (vault_name, secret_path) = + parse_uri(uri, "azure-kv", "secret:///secrets/")?; + let kv_url = format!("https://{vault_name}.vault.azure.net/{secret_path}?api-version=7.4"); + let uri_owned = uri.to_owned(); + // Clone self for the 'static async block + let backend = Self { + tenant_id: self.tenant_id.clone(), + client_id: self.client_id.clone(), + client_secret: self.client_secret.clone(), + }; + + resolve_async(async move { + let token = backend.acquire_token(&uri_owned).await?; + + let json = http_json( + reqwest::Client::new().get(&kv_url).bearer_auth(&token), + &uri_owned, + "Azure KV", + ) + .await?; + + json.get("value") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!( + "Field 'value' not found in Azure KV secret at {uri_owned}" + )) + }) + .map(str::to_owned) + }) + } +} diff --git a/crate/server/src/config/command_line/secret_backends/common.rs b/crate/server/src/config/command_line/secret_backends/common.rs new file mode 100644 index 000000000..cd035aedc --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/common.rs @@ -0,0 +1,71 @@ +//! Shared helper functions for all secret backends. + +use crate::{error::KmsError, result::KResult}; + +/// Run an async future on a dedicated thread with a fresh single-threaded +/// tokio runtime. Required because config loading runs before the main runtime. +pub(super) fn resolve_async(future: F) -> KResult +where + F: std::future::Future> + Send + 'static, +{ + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? + .block_on(future) + }) + .join() + .map_err(|e| KmsError::ServerError(format!("Secret resolution thread panicked: {e:?}")))? +} + +/// Strip the `secret://` prefix and split at the first `/`. +/// Returns `(first_segment, remainder)`. +pub(super) fn parse_uri<'a>( + uri: &'a str, + backend_name: &str, + format_hint: &str, +) -> KResult<(&'a str, &'a str)> { + let rest = uri + .strip_prefix("secret://") + .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; + let slash = rest.find('/').ok_or_else(|| { + KmsError::InvalidRequest(format!( + "{backend_name} URI must be {format_hint}, got: {uri}" + )) + })?; + Ok((&rest[..slash], &rest[slash + 1..])) +} + +/// Check that a required credential is non-empty. +pub(super) fn require_non_empty(value: &str, name: &str, backend: &str) -> KResult<()> { + if value.is_empty() { + return Err(KmsError::ServerError(format!( + "{name} is not set; required for {backend} secret backend" + ))); + } + Ok(()) +} + +/// Send an HTTP request and return the parsed JSON body, +/// or a descriptive error including the URI and backend name. +pub(super) async fn http_json( + req: reqwest::RequestBuilder, + uri: &str, + backend: &str, +) -> KResult { + let resp = req + .send() + .await + .map_err(|e| KmsError::ServerError(format!("{backend} request failed for {uri}: {e}")))?; + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(KmsError::ServerError(format!( + "{backend} returned HTTP {status} for {uri}: {body}" + ))); + } + resp.json().await.map_err(|e| { + KmsError::ServerError(format!("{backend} response parse error for {uri}: {e}")) + }) +} diff --git a/crate/server/src/config/command_line/secret_backends/cosmian_kms.rs b/crate/server/src/config/command_line/secret_backends/cosmian_kms.rs new file mode 100644 index 000000000..770f5c4ff --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/cosmian_kms.rs @@ -0,0 +1,131 @@ +//! Cosmian KMS secret backend (KMIP Get). + +use cosmian_kms_server_database::reexport::cosmian_kmip::{ + kmip_2_1::{ + kmip_objects::Object, + kmip_operations::{Get, GetResponse}, + }, + ttlv::{TTLV, from_ttlv, to_ttlv}, +}; + +use super::{ + CosmianKmsSecretConfig, KResult, KmsError, SecretBackend, + common::{parse_uri, resolve_async}, +}; + +pub(super) struct CosmianKmsBackend { + token: Option, + insecure_certs: bool, +} + +impl CosmianKmsBackend { + pub(super) fn new(cfg: &CosmianKmsSecretConfig) -> Self { + Self { + token: cfg.cosmian_kms_secret_token.clone(), + insecure_certs: cfg.cosmian_kms_insecure_certs, + } + } + + /// Determine http/https scheme and default port from host string. + fn server_url(&self, host_port: &str) -> String { + let is_local = host_port.starts_with("localhost") + || host_port.starts_with("127.0.0.1") + || host_port.starts_with("[::1]"); + let scheme = if is_local || self.insecure_certs { + "http" + } else { + "https" + }; + if host_port.contains(':') { + format!("{scheme}://{host_port}") + } else { + format!("{scheme}://{host_port}:9998") + } + } + + /// Send a KMIP Get request and extract the secret string from the response. + async fn fetch_object(&self, url: &str, id: &str, uri: &str) -> KResult { + let req_ttlv = to_ttlv(&Get::from(id)).map_err(|e| { + KmsError::ServerError(format!("KMIP Get serialise error for {uri}: {e}")) + })?; + + let mut builder = reqwest::Client::builder(); + if self.insecure_certs { + builder = builder.danger_accept_invalid_certs(true); + } + let client = builder + .build() + .map_err(|e| KmsError::ServerError(format!("HTTP client build error: {e}")))?; + + let endpoint = format!("{}/kmip/2_1", url.trim_end_matches('/')); + let mut req = client.post(&endpoint).json(&req_ttlv); + if let Some(t) = &self.token { + req = req.bearer_auth(t); + } + + let resp = req.send().await.map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS request failed for {uri}: {e}")) + })?; + if !resp.status().is_success() { + return Err(KmsError::ServerError(format!( + "Cosmian KMS HTTP {} for {uri}", + resp.status() + ))); + } + + let ttlv: TTLV = resp.json().await.map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS response parse error for {uri}: {e}")) + })?; + let get_resp: GetResponse = from_ttlv(ttlv).map_err(|e| { + KmsError::ServerError(format!("Cosmian KMS deserialise error for {uri}: {e}")) + })?; + Self::extract_secret(get_resp.object, uri) + } + + /// Extract a UTF-8 string from a `SecretData` or `OpaqueObject`. + fn extract_secret(object: Object, uri: &str) -> KResult { + match object { + Object::SecretData(sd) => { + let bytes = sd.key_block.key_bytes().map_err(|e| { + KmsError::ServerError(format!("SecretData key_bytes error at {uri}: {e}")) + })?; + String::from_utf8(bytes.to_vec()).map_err(|e| { + KmsError::ServerError(format!("SecretData non-UTF-8 at {uri}: {e}")) + }) + } + Object::OpaqueObject(o) => String::from_utf8(o.opaque_data_value).map_err(|e| { + KmsError::ServerError(format!("OpaqueObject non-UTF-8 at {uri}: {e}")) + }), + other => Err(KmsError::ServerError(format!( + "Cosmian KMS object at {uri} has type {:?}; expected SecretData or OpaqueObject", + other.object_type() + ))), + } + } +} + +impl SecretBackend for CosmianKmsBackend { + fn resolve(&self, uri: &str) -> KResult { + let (host_port, object_id) = + parse_uri(uri, "cosmian-kms", "secret://[:]/")?; + if object_id.is_empty() { + return Err(KmsError::InvalidRequest(format!( + "cosmian-kms URI must have a non-empty object ID: {uri}" + ))); + } + + let server_url = self.server_url(host_port); + let object_id = object_id.to_owned(); + let uri_owned = uri.to_owned(); + let backend = Self { + token: self.token.clone(), + insecure_certs: self.insecure_certs, + }; + + resolve_async(async move { + backend + .fetch_object(&server_url, &object_id, &uri_owned) + .await + }) + } +} diff --git a/crate/server/src/config/command_line/secret_backends/mod.rs b/crate/server/src/config/command_line/secret_backends/mod.rs new file mode 100644 index 000000000..8316d8e9e --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/mod.rs @@ -0,0 +1,297 @@ +//! Secret URI backends — Clap credential structs, resolution logic, and +//! per-backend implementations. +//! +//! The TOML walker replaces every `secret://` value using the selected backend. +//! Credentials are passed via `SecretBackendConfig` (populated by clap/env). +//! Zero `std::env::var` calls in production code. + +mod aws; +mod azure; +mod common; +mod cosmian_kms; +mod vault; + +use clap::{Args, ValueEnum}; + +use crate::{error::KmsError, result::KResult}; + +// ─── Clap structs ─────────────────────────────────────────────────────────── + +/// Selects which backend resolves `secret://` URIs in the KMS config file. +/// +/// Pass via `--secret-backend ` or the `KMS_SECRET_BACKEND` env var. +/// The TOML config uses the neutral `secret://` scheme; which backend is +/// actually used is **never** revealed in the config file itself. +#[derive(Clone, Default, ValueEnum, Debug)] +pub enum SecretBackendKind { + /// `HashiCorp` Vault KV-v2. + /// Path format: `secret:///[#]` + #[default] + #[value(name = "vault")] + Vault, + /// AWS Systems Manager Parameter Store. + /// Path format: `secret:///` + #[value(name = "aws-ssm")] + AwsSsm, + /// Azure Key Vault. + /// Path format: `secret:///secrets/[/]` + #[value(name = "azure-kv")] + AzureKv, + /// Another Cosmian KMS server (KMIP Get). + /// Path format: `secret://[:]/` + #[value(name = "cosmian-kms")] + CosmianKms, +} + +/// Credentials for the `HashiCorp` Vault KV-v2 backend. +#[derive(Args, Clone, Default)] +pub struct VaultBackendConfig { + #[clap(long, env = "VAULT_ADDR", default_value = "http://127.0.0.1:8200")] + pub vault_addr: String, + #[clap(long, env = "VAULT_TOKEN", default_value = "")] + pub vault_token: String, +} + +/// Credentials for the AWS Systems Manager Parameter Store backend. +#[derive(Args, Clone, Default)] +pub struct AwsSsmBackendConfig { + #[clap(long, env = "AWS_ACCESS_KEY_ID", default_value = "")] + pub aws_access_key_id: String, + #[clap(long, env = "AWS_SECRET_ACCESS_KEY", default_value = "")] + pub aws_secret_access_key: String, + #[clap(long, env = "AWS_SESSION_TOKEN")] + pub aws_session_token: Option, +} + +/// Credentials for the Azure Key Vault backend. +#[derive(Args, Clone, Default)] +pub struct AzureKvBackendConfig { + #[clap(long, env = "AZURE_TENANT_ID", default_value = "")] + pub azure_tenant_id: String, + #[clap(long, env = "AZURE_CLIENT_ID", default_value = "")] + pub azure_client_id: String, + #[clap(long, env = "AZURE_CLIENT_SECRET", default_value = "")] + pub azure_client_secret: String, +} + +/// Credentials for the Cosmian KMS secret backend. +#[derive(Args, Clone, Default)] +pub struct CosmianKmsSecretConfig { + #[clap(long, env = "COSMIAN_KMS_SECRET_TOKEN")] + pub cosmian_kms_secret_token: Option, + #[clap(long, env = "COSMIAN_KMS_INSECURE_CERTS")] + pub cosmian_kms_insecure_certs: bool, +} + +/// Authentication credentials and backend selection for secret URI resolution. +#[derive(Args, Clone, Default)] +pub struct SecretBackendConfig { + #[clap(long, env = "KMS_SECRET_BACKEND", value_enum)] + pub backend: Option, + #[command(flatten)] + pub vault: VaultBackendConfig, + #[command(flatten)] + pub aws: AwsSsmBackendConfig, + #[command(flatten)] + pub azure: AzureKvBackendConfig, + #[command(flatten)] + pub cosmian_kms: CosmianKmsSecretConfig, +} + +// ─── Trait ────────────────────────────────────────────────────────────────── + +/// Each secret backend implements this single method. +pub(crate) trait SecretBackend { + fn resolve(&self, uri: &str) -> KResult; +} + +// ─── TOML walker ──────────────────────────────────────────────────────────── + +/// Replace every TOML string starting with `secret://` using the active backend. +fn resolve_secret_uris(value: &mut toml::Value, backend: &dyn SecretBackend) -> KResult<()> { + match value { + toml::Value::String(s) => { + if s.starts_with("secret://") { + *s = backend.resolve(s)?; + } + } + toml::Value::Table(map) => { + for (_, v) in map.iter_mut() { + resolve_secret_uris(v, backend)?; + } + } + toml::Value::Array(arr) => { + for v in arr { + resolve_secret_uris(v, backend)?; + } + } + _ => {} + } + Ok(()) +} + +/// Entry point called from `load_from_args`. +/// Does nothing when `config.backend` is `None`. +pub(crate) fn resolve_config(value: &mut toml::Value, config: &SecretBackendConfig) -> KResult<()> { + match &config.backend { + None => Ok(()), + Some(SecretBackendKind::Vault) => { + resolve_secret_uris(value, &vault::VaultBackend::new(&config.vault)) + } + Some(SecretBackendKind::AwsSsm) => { + resolve_secret_uris(value, &aws::AwsSsmBackend::new(&config.aws)) + } + Some(SecretBackendKind::AzureKv) => { + resolve_secret_uris(value, &azure::AzureKvBackend::new(&config.azure)) + } + Some(SecretBackendKind::CosmianKms) => resolve_secret_uris( + value, + &cosmian_kms::CosmianKmsBackend::new(&config.cosmian_kms), + ), + } +} + +// ─── Integration tests ────────────────────────────────────────────────────── + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + + /// Helper: wrap a URI in a minimal TOML table and resolve it. + fn resolve_single_uri(uri: &str, config: &SecretBackendConfig) -> KResult { + let mut value = toml::Value::Table(toml::map::Map::from_iter([( + "test_field".to_owned(), + toml::Value::String(uri.to_owned()), + )])); + resolve_config(&mut value, config)?; + value + .get("test_field") + .and_then(toml::Value::as_str) + .map(str::to_owned) + .ok_or_else(|| KmsError::ServerError("test_field missing after resolve".to_owned())) + } + + #[test] + #[ignore = "Requires a running HashiCorp Vault and VAULT_ADDR/VAULT_TOKEN env vars"] + fn test_secret_vault() { + let uri = std::env::var("KMS_TEST_VAULT_URI") + .expect("KMS_TEST_VAULT_URI must be set (e.g. vault://secret/kms-ci/db#password)"); + let expected = + std::env::var("KMS_TEST_VAULT_EXPECTED").expect("KMS_TEST_VAULT_EXPECTED must be set"); + let vault_addr = + std::env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_owned()); + let vault_token = std::env::var("VAULT_TOKEN").expect("VAULT_TOKEN must be set"); + + // The bash script passes URIs as "vault://mount/path#field" but resolve_config + // expects the neutral "secret://" scheme. + let secret_uri = uri.replacen("vault://", "secret://", 1); + + let config = SecretBackendConfig { + backend: Some(SecretBackendKind::Vault), + vault: VaultBackendConfig { + vault_addr, + vault_token, + }, + ..Default::default() + }; + + let resolved = resolve_single_uri(&secret_uri, &config) + .expect("resolve_config should succeed for vault"); + assert_eq!(resolved, expected, "Vault secret mismatch"); + } + + #[test] + #[ignore = "Requires AWS credentials and a SSM parameter created by CI"] + fn test_secret_aws_ssm() { + let uri = std::env::var("KMS_TEST_AWS_SSM_URI") + .expect("KMS_TEST_AWS_SSM_URI must be set (e.g. aws-ssm://eu-west-1/kms/ci/test)"); + let expected = std::env::var("KMS_TEST_AWS_SSM_EXPECTED") + .expect("KMS_TEST_AWS_SSM_EXPECTED must be set"); + let access_key_id = + std::env::var("AWS_ACCESS_KEY_ID").expect("AWS_ACCESS_KEY_ID must be set"); + let secret_access_key = + std::env::var("AWS_SECRET_ACCESS_KEY").expect("AWS_SECRET_ACCESS_KEY must be set"); + let session_token = std::env::var("AWS_SESSION_TOKEN").ok(); + + // The bash script passes URIs as "aws-ssm://region/param" but resolve_config + // expects the neutral "secret://" scheme. + let secret_uri = uri.replacen("aws-ssm://", "secret://", 1); + + let config = SecretBackendConfig { + backend: Some(SecretBackendKind::AwsSsm), + aws: AwsSsmBackendConfig { + aws_access_key_id: access_key_id, + aws_secret_access_key: secret_access_key, + aws_session_token: session_token, + }, + ..Default::default() + }; + + let resolved = resolve_single_uri(&secret_uri, &config) + .expect("resolve_config should succeed for aws-ssm"); + assert_eq!(resolved, expected, "AWS SSM secret mismatch"); + } + + #[test] + #[ignore = "Requires Azure AD credentials and a Key Vault secret created by CI"] + fn test_secret_azure_kv() { + let uri = std::env::var("KMS_TEST_AZURE_KV_URI") + .expect("KMS_TEST_AZURE_KV_URI must be set (e.g. azure-kv://myvault/secrets/myname)"); + let expected = std::env::var("KMS_TEST_AZURE_KV_EXPECTED") + .expect("KMS_TEST_AZURE_KV_EXPECTED must be set"); + let tenant_id = std::env::var("AZURE_TENANT_ID").expect("AZURE_TENANT_ID must be set"); + let client_id = std::env::var("AZURE_CLIENT_ID").expect("AZURE_CLIENT_ID must be set"); + let client_secret = + std::env::var("AZURE_CLIENT_SECRET").expect("AZURE_CLIENT_SECRET must be set"); + + // The bash script passes URIs as "azure-kv://vault/secrets/name" but + // resolve_config expects the neutral "secret://" scheme. + let secret_uri = uri.replacen("azure-kv://", "secret://", 1); + + let config = SecretBackendConfig { + backend: Some(SecretBackendKind::AzureKv), + azure: AzureKvBackendConfig { + azure_tenant_id: tenant_id, + azure_client_id: client_id, + azure_client_secret: client_secret, + }, + ..Default::default() + }; + + let resolved = resolve_single_uri(&secret_uri, &config) + .expect("resolve_config should succeed for azure-kv"); + assert_eq!(resolved, expected, "Azure KV secret mismatch"); + } + + #[test] + #[ignore = "Requires a running Cosmian KMS server with a registered SecretData object"] + fn test_secret_cosmian_kms() { + let uri = std::env::var("KMS_TEST_COSMIAN_KMS_URI").expect( + "KMS_TEST_COSMIAN_KMS_URI must be set (e.g. cosmian-kms://localhost:9998/)", + ); + let expected = std::env::var("KMS_TEST_COSMIAN_KMS_EXPECTED") + .expect("KMS_TEST_COSMIAN_KMS_EXPECTED must be set"); + let token = std::env::var("COSMIAN_KMS_SECRET_TOKEN").ok(); + let insecure = std::env::var("COSMIAN_KMS_INSECURE_CERTS") + .map(|v| v == "true" || v == "1") + .unwrap_or(false); + + // The bash script passes URIs as "cosmian-kms://host:port/id" but + // resolve_config expects the neutral "secret://" scheme. + let secret_uri = uri.replacen("cosmian-kms://", "secret://", 1); + + let config = SecretBackendConfig { + backend: Some(SecretBackendKind::CosmianKms), + cosmian_kms: CosmianKmsSecretConfig { + cosmian_kms_secret_token: token, + cosmian_kms_insecure_certs: insecure, + }, + ..Default::default() + }; + + let resolved = resolve_single_uri(&secret_uri, &config) + .expect("resolve_config should succeed for cosmian-kms"); + assert_eq!(resolved, expected, "Cosmian KMS secret mismatch"); + } +} diff --git a/crate/server/src/config/command_line/secret_backends/vault.rs b/crate/server/src/config/command_line/secret_backends/vault.rs new file mode 100644 index 000000000..e61760dfd --- /dev/null +++ b/crate/server/src/config/command_line/secret_backends/vault.rs @@ -0,0 +1,63 @@ +//! `HashiCorp` Vault KV-v2 backend. + +use super::{ + KResult, KmsError, SecretBackend, VaultBackendConfig, + common::{http_json, parse_uri, require_non_empty, resolve_async}, +}; + +pub(super) struct VaultBackend { + addr: String, + token: String, +} + +impl VaultBackend { + pub(super) fn new(cfg: &VaultBackendConfig) -> Self { + Self { + addr: cfg.vault_addr.clone(), + token: cfg.vault_token.clone(), + } + } +} + +impl SecretBackend for VaultBackend { + fn resolve(&self, uri: &str) -> KResult { + require_non_empty(&self.token, "VAULT_TOKEN", "vault")?; + + let (first, remainder) = parse_uri(uri, "vault", "secret:///[#]")?; + let (path_part, field) = format!("{first}/{remainder}").split_once('#').map_or_else( + || (format!("{first}/{remainder}"), "value".to_owned()), + |(p, f)| (p.to_owned(), f.to_owned()), + ); + let (mount, secret_path) = path_part.split_once('/').ok_or_else(|| { + KmsError::InvalidRequest(format!("vault URI must have mount/path, got: {uri}")) + })?; + let url = format!( + "{}/v1/{mount}/data/{secret_path}", + self.addr.trim_end_matches('/') + ); + let token = self.token.clone(); + let uri_owned = uri.to_owned(); + + resolve_async(async move { + let json = http_json( + reqwest::Client::new() + .get(&url) + .header("X-Vault-Token", &token), + &uri_owned, + "Vault", + ) + .await?; + + json.get("data") + .and_then(|d| d.get("data")) + .and_then(|d| d.get(&field)) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + KmsError::ServerError(format!( + "Field '{field}' not found in Vault secret at {uri_owned}" + )) + }) + .map(str::to_owned) + }) + } +} diff --git a/crate/server/src/config/mod.rs b/crate/server/src/config/mod.rs index 69a696593..d20e58f87 100644 --- a/crate/server/src/config/mod.rs +++ b/crate/server/src/config/mod.rs @@ -1,6 +1,5 @@ mod command_line; mod params; -mod secret_resolver; pub mod wizard; pub use command_line::*; diff --git a/crate/server/src/config/secret_resolver.rs b/crate/server/src/config/secret_resolver.rs deleted file mode 100644 index 912e551ac..000000000 --- a/crate/server/src/config/secret_resolver.rs +++ /dev/null @@ -1,592 +0,0 @@ -//! Secret URI resolution — called by `load_from_args` between TOML parsing -//! and deserialisation into `ClapConfig`. -//! -//! Credentials are passed in via `SecretBackendConfig` (populated by clap). -//! Zero `std::env::var` calls in production code. - -use crate::{ - config::command_line::secret_backends::{SecretBackendConfig, SecretBackendKind}, - error::KmsError, - result::KResult, -}; - -// ─── Shared async helper ──────────────────────────────────────────────────── - -fn run_blocking(f: F) -> KResult -where - F: FnOnce() -> KResult + Send + 'static, - T: Send + 'static, -{ - std::thread::spawn(f) - .join() - .map_err(|e| KmsError::ServerError(format!("Secret resolution thread panicked: {e:?}")))? -} - -// ─── Trait ────────────────────────────────────────────────────────────────── - -trait SecretBackend { - fn resolve(&self, uri: &str) -> KResult; -} - -// ─── TOML walker ──────────────────────────────────────────────────────────── - -/// Replace every TOML string starting with `secret://` using the active backend. -fn resolve_secret_uris(value: &mut toml::Value, backend: &dyn SecretBackend) -> KResult<()> { - match value { - toml::Value::String(s) => { - if s.starts_with("secret://") { - *s = backend.resolve(s)?; - } - } - toml::Value::Table(map) => { - for (_, v) in map.iter_mut() { - resolve_secret_uris(v, backend)?; - } - } - toml::Value::Array(arr) => { - for v in arr { - resolve_secret_uris(v, backend)?; - } - } - _ => {} - } - Ok(()) -} - -/// Entry point called from `load_from_args`. -/// Does nothing when `config.backend` is `None`. -pub(crate) fn resolve_config(value: &mut toml::Value, config: &SecretBackendConfig) -> KResult<()> { - match &config.backend { - None => Ok(()), - Some(SecretBackendKind::Vault) => { - resolve_secret_uris(value, &vault::VaultBackend::new(&config.vault)) - } - Some(SecretBackendKind::AwsSsm) => { - resolve_secret_uris(value, &aws::AwsSsmBackend::new(&config.aws)) - } - Some(SecretBackendKind::AzureKv) => { - resolve_secret_uris(value, &azure::AzureKvBackend::new(&config.azure)) - } - Some(SecretBackendKind::CosmianKms) => resolve_secret_uris( - value, - &cosmian_kms::CosmianKmsBackend::new(&config.cosmian_kms), - ), - } -} - -// ─── HashiCorp Vault KV-v2 ────────────────────────────────────────────────── - -mod vault { - use super::{KResult, KmsError, SecretBackend, run_blocking}; - use crate::config::command_line::secret_backends::VaultBackendConfig; - - pub(super) struct VaultBackend { - addr: String, - token: String, - } - - impl VaultBackend { - pub(super) fn new(cfg: &VaultBackendConfig) -> Self { - Self { - addr: cfg.vault_addr.clone(), - token: cfg.vault_token.clone(), - } - } - } - - impl SecretBackend for VaultBackend { - fn resolve(&self, uri: &str) -> KResult { - if self.token.is_empty() { - return Err(KmsError::ServerError( - "vault_token (VAULT_TOKEN) is empty; required for the vault secret backend" - .to_owned(), - )); - } - let rest = uri - .strip_prefix("secret://") - .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; - let (path_part, field) = rest - .split_once('#') - .map_or((rest, "value"), |(p, f)| (p, f)); - let slash = path_part.find('/').ok_or_else(|| { - KmsError::InvalidRequest(format!( - "vault URI must be secret:///[#], got: {uri}" - )) - })?; - let mount = &path_part[..slash]; - let secret_path = &path_part[slash + 1..]; - let url = format!( - "{}/v1/{mount}/data/{secret_path}", - self.addr.trim_end_matches('/') - ); - let token = self.token.clone(); - let uri_owned = uri.to_owned(); - let field_owned = field.to_owned(); - let value: serde_json::Value = run_blocking(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? - .block_on(async { - let resp = reqwest::Client::new() - .get(&url) - .header("X-Vault-Token", &token) - .send() - .await - .map_err(|e| { - KmsError::ServerError(format!( - "Vault request failed for {uri_owned}: {e}" - )) - })?; - if !resp.status().is_success() { - return Err(KmsError::ServerError(format!( - "Vault returned HTTP {} for {uri_owned}", - resp.status() - ))); - } - resp.json::().await.map_err(|e| { - KmsError::ServerError(format!( - "Vault response parse error for {uri_owned}: {e}" - )) - }) - }) - })?; - value - .get("data") - .and_then(|d| d.get("data")) - .and_then(|d| d.get(&field_owned)) - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - KmsError::ServerError(format!( - "Field '{field_owned}' not found in Vault secret at {uri}" - )) - }) - .map(str::to_owned) - } - } -} - -// ─── AWS SSM Parameter Store ───────────────────────────────────────────────── - -mod aws { - use chrono::Utc; - use hmac::{Hmac, Mac}; - use sha2::{Digest, Sha256}; - - use super::{KResult, KmsError, SecretBackend, run_blocking}; - use crate::config::command_line::secret_backends::AwsSsmBackendConfig; - - type HmacSha256 = Hmac; - - pub(super) struct AwsSsmBackend { - access_key_id: String, - secret_access_key: String, - session_token: Option, - } - - impl AwsSsmBackend { - pub(super) fn new(cfg: &AwsSsmBackendConfig) -> Self { - Self { - access_key_id: cfg.aws_access_key_id.clone(), - secret_access_key: cfg.aws_secret_access_key.clone(), - session_token: cfg.aws_session_token.clone(), - } - } - } - - fn hex(b: &[u8]) -> String { - hex::encode(b) - } - fn sha256(d: &[u8]) -> String { - hex(&Sha256::digest(d)) - } - fn hmac(key: &[u8], data: &[u8]) -> KResult> { - let mut mac = HmacSha256::new_from_slice(key) - .map_err(|e| KmsError::ServerError(format!("HMAC key error: {e}")))?; - mac.update(data); - Ok(mac.finalize().into_bytes().to_vec()) - } - fn signing_key(secret: &str, date: &str, region: &str, service: &str) -> KResult> { - let k = hmac(format!("AWS4{secret}").as_bytes(), date.as_bytes())?; - let k = hmac(&k, region.as_bytes())?; - let k = hmac(&k, service.as_bytes())?; - hmac(&k, b"aws4_request") - } - - impl SecretBackend for AwsSsmBackend { - fn resolve(&self, uri: &str) -> KResult { - if self.access_key_id.is_empty() { - return Err(KmsError::ServerError( - "aws_access_key_id (AWS_ACCESS_KEY_ID) is not set; required for aws-ssm backend".to_owned(), - )); - } - if self.secret_access_key.is_empty() { - return Err(KmsError::ServerError( - "aws_secret_access_key (AWS_SECRET_ACCESS_KEY) is not set; required for aws-ssm backend".to_owned(), - )); - } - let rest = uri - .strip_prefix("secret://") - .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; - let slash = rest.find('/').ok_or_else(|| { - KmsError::InvalidRequest(format!( - "aws-ssm URI must be secret:///, got: {uri}" - )) - })?; - let region = rest[..slash].to_owned(); - let param_name = rest[slash..].to_owned(); - let access_key_id = self.access_key_id.clone(); - let secret_key = self.secret_access_key.clone(); - let session_token = self.session_token.clone(); - let uri_owned = uri.to_owned(); - run_blocking(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? - .block_on(call_ssm( - ®ion, - ¶m_name, - &access_key_id, - &secret_key, - session_token.as_deref(), - &uri_owned, - )) - }) - } - } - - async fn call_ssm( - region: &str, - param: &str, - key_id: &str, - secret: &str, - token: Option<&str>, - uri: &str, - ) -> KResult { - let now = Utc::now(); - let dt = now.format("%Y%m%dT%H%M%SZ").to_string(); - let date = now.format("%Y%m%d").to_string(); - let host = format!("ssm.{region}.amazonaws.com"); - let body = serde_json::json!({"Name": param, "WithDecryption": true}).to_string(); - let payload_hash = sha256(body.as_bytes()); - let (canon_hdrs, signed_hdrs) = token.map_or_else( - || (format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-target:AmazonSSM.GetParameter\n"), - "content-type;host;x-amz-date;x-amz-target".to_owned()), - |t| (format!("content-type:application/x-amz-json-1.1\nhost:{host}\nx-amz-date:{dt}\nx-amz-security-token:{t}\nx-amz-target:AmazonSSM.GetParameter\n"), - "content-type;host;x-amz-date;x-amz-security-token;x-amz-target".to_owned()), - ); - let canon_req = format!("POST\n/\n\n{canon_hdrs}\n{signed_hdrs}\n{payload_hash}"); - let scope = format!("{date}/{region}/ssm/aws4_request"); - let sts = format!( - "AWS4-HMAC-SHA256\n{dt}\n{scope}\n{}", - sha256(canon_req.as_bytes()) - ); - let key = signing_key(secret, &date, region, "ssm")?; - let sig = hex(&hmac(&key, sts.as_bytes())?); - let auth = format!( - "AWS4-HMAC-SHA256 Credential={key_id}/{scope}, SignedHeaders={signed_hdrs}, Signature={sig}" - ); - let client = reqwest::Client::new(); - let req = client - .post(format!("https://{host}/")) - .header("Content-Type", "application/x-amz-json-1.1") - .header("X-Amz-Date", &dt) - .header("X-Amz-Target", "AmazonSSM.GetParameter") - .header("Authorization", &auth); - let req = if let Some(t) = token { - req.header("X-Amz-Security-Token", t) - } else { - req - }; - let resp = - req.body(body).send().await.map_err(|e| { - KmsError::ServerError(format!("AWS SSM request failed for {uri}: {e}")) - })?; - if !resp.status().is_success() { - let s = resp.status(); - let b = resp.text().await.unwrap_or_default(); - return Err(KmsError::ServerError(format!( - "AWS SSM HTTP {s} for {uri}: {b}" - ))); - } - let j: serde_json::Value = resp.json().await.map_err(|e| { - KmsError::ServerError(format!("AWS SSM response parse error for {uri}: {e}")) - })?; - j.get("Parameter") - .and_then(|p| p.get("Value")) - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - KmsError::ServerError(format!("AWS SSM missing Parameter.Value for {uri}")) - }) - .map(str::to_owned) - } -} - -// ─── Azure Key Vault ───────────────────────────────────────────────────────── - -mod azure { - use super::{KResult, KmsError, SecretBackend, run_blocking}; - use crate::config::command_line::secret_backends::AzureKvBackendConfig; - - pub(super) struct AzureKvBackend { - tenant_id: String, - client_id: String, - client_secret: String, - } - - impl AzureKvBackend { - pub(super) fn new(cfg: &AzureKvBackendConfig) -> Self { - Self { - tenant_id: cfg.azure_tenant_id.clone(), - client_id: cfg.azure_client_id.clone(), - client_secret: cfg.azure_client_secret.clone(), - } - } - } - - impl SecretBackend for AzureKvBackend { - fn resolve(&self, uri: &str) -> KResult { - for (name, val) in [ - ("azure_tenant_id (AZURE_TENANT_ID)", &self.tenant_id), - ("azure_client_id (AZURE_CLIENT_ID)", &self.client_id), - ( - "azure_client_secret (AZURE_CLIENT_SECRET)", - &self.client_secret, - ), - ] { - if val.is_empty() { - return Err(KmsError::ServerError(format!( - "{name} is not set; required for azure-kv secret backend" - ))); - } - } - let rest = uri - .strip_prefix("secret://") - .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; - let slash = rest.find('/').ok_or_else(|| { - KmsError::InvalidRequest(format!( - "azure-kv URI must be secret:///secrets/, got: {uri}" - )) - })?; - let vault_name = rest[..slash].to_owned(); - let secret_path = rest[slash + 1..].to_owned(); - let kv_url = - format!("https://{vault_name}.vault.azure.net/{secret_path}?api-version=7.4"); - let tenant_id = self.tenant_id.clone(); - let client_id = self.client_id.clone(); - let client_secret = self.client_secret.clone(); - let uri_owned = uri.to_owned(); - run_blocking(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? - .block_on(async { - let token_url = format!( - "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" - ); - let token_resp = reqwest::Client::new() - .post(&token_url) - .form(&[ - ("grant_type", "client_credentials"), - ("client_id", client_id.as_str()), - ("client_secret", client_secret.as_str()), - ("scope", "https://vault.azure.net/.default"), - ]) - .send() - .await - .map_err(|e| { - KmsError::ServerError(format!("Azure AD token request failed: {e}")) - })?; - if !token_resp.status().is_success() { - return Err(KmsError::ServerError(format!( - "Azure AD token endpoint returned HTTP {}", - token_resp.status() - ))); - } - let token_body: serde_json::Value = - token_resp.json().await.map_err(|e| { - KmsError::ServerError(format!("Azure AD token parse error: {e}")) - })?; - let token = token_body - .get("access_token") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - KmsError::ServerError( - "No access_token in Azure AD response".to_owned(), - ) - })? - .to_owned(); - let resp = reqwest::Client::new() - .get(&kv_url) - .bearer_auth(&token) - .send() - .await - .map_err(|e| { - KmsError::ServerError(format!( - "Azure KV request failed for {uri_owned}: {e}" - )) - })?; - if !resp.status().is_success() { - return Err(KmsError::ServerError(format!( - "Azure KV returned HTTP {} for {uri_owned}", - resp.status() - ))); - } - let body: serde_json::Value = resp.json().await.map_err(|e| { - KmsError::ServerError(format!( - "Azure KV response parse error for {uri_owned}: {e}" - )) - })?; - body.get("value") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| { - KmsError::ServerError(format!( - "Field 'value' not found in Azure KV secret at {uri_owned}" - )) - }) - .map(str::to_owned) - }) - }) - } - } -} - -// ─── Cosmian KMS (KMIP Get) ─────────────────────────────────────────────────── - -mod cosmian_kms { - use cosmian_kms_server_database::reexport::cosmian_kmip::{ - kmip_2_1::{ - kmip_objects::Object, - kmip_operations::{Get, GetResponse}, - }, - ttlv::{TTLV, from_ttlv, to_ttlv}, - }; - - use super::{KResult, KmsError, SecretBackend, run_blocking}; - use crate::config::command_line::secret_backends::CosmianKmsSecretConfig; - - pub(super) struct CosmianKmsBackend { - token: Option, - insecure_certs: bool, - } - - impl CosmianKmsBackend { - pub(super) fn new(cfg: &CosmianKmsSecretConfig) -> Self { - Self { - token: cfg.cosmian_kms_secret_token.clone(), - insecure_certs: cfg.cosmian_kms_insecure_certs, - } - } - } - - impl SecretBackend for CosmianKmsBackend { - fn resolve(&self, uri: &str) -> KResult { - let rest = uri - .strip_prefix("secret://") - .ok_or_else(|| KmsError::InvalidRequest(format!("Invalid secret URI: {uri}")))?; - let slash = rest.find('/').ok_or_else(|| { - KmsError::InvalidRequest(format!( - "cosmian-kms URI must be secret://[:]/, got: {uri}" - )) - })?; - let host_port = &rest[..slash]; - let object_id = rest[slash + 1..].to_owned(); - if object_id.is_empty() { - return Err(KmsError::InvalidRequest(format!( - "cosmian-kms URI must have a non-empty object ID: {uri}" - ))); - } - let is_local = host_port.starts_with("localhost") - || host_port.starts_with("127.0.0.1") - || host_port.starts_with("[::1]"); - let scheme = if is_local || self.insecure_certs { - "http" - } else { - "https" - }; - let server_url = if host_port.contains(':') { - format!("{scheme}://{host_port}") - } else { - format!("{scheme}://{host_port}:9998") - }; - let token = self.token.clone(); - let insecure = self.insecure_certs; - let uri_owned = uri.to_owned(); - run_blocking(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| KmsError::ServerError(format!("tokio runtime: {e}")))? - .block_on(fetch_object( - &server_url, - &object_id, - token.as_deref(), - insecure, - &uri_owned, - )) - }) - } - } - - async fn fetch_object( - url: &str, - id: &str, - token: Option<&str>, - insecure: bool, - uri: &str, - ) -> KResult { - let req_ttlv = to_ttlv(&Get::from(id)).map_err(|e| { - KmsError::ServerError(format!("KMIP Get serialise error for {uri}: {e}")) - })?; - let mut b = reqwest::Client::builder(); - if insecure { - b = b.danger_accept_invalid_certs(true); - } - let client = b - .build() - .map_err(|e| KmsError::ServerError(format!("HTTP client build error: {e}")))?; - let endpoint = format!("{}/kmip/2_1", url.trim_end_matches('/')); - let mut req = client.post(&endpoint).json(&req_ttlv); - if let Some(t) = token { - req = req.bearer_auth(t); - } - let resp = req.send().await.map_err(|e| { - KmsError::ServerError(format!("Cosmian KMS request failed for {uri}: {e}")) - })?; - if !resp.status().is_success() { - return Err(KmsError::ServerError(format!( - "Cosmian KMS HTTP {} for {uri}", - resp.status() - ))); - } - let ttlv: TTLV = resp.json().await.map_err(|e| { - KmsError::ServerError(format!("Cosmian KMS response parse error for {uri}: {e}")) - })?; - let get_resp: GetResponse = from_ttlv(ttlv).map_err(|e| { - KmsError::ServerError(format!("Cosmian KMS deserialise error for {uri}: {e}")) - })?; - extract_secret(get_resp.object, uri) - } - - fn extract_secret(object: Object, uri: &str) -> KResult { - match object { - Object::SecretData(sd) => { - let bytes = sd.key_block.key_bytes().map_err(|e| { - KmsError::ServerError(format!("SecretData key_bytes error at {uri}: {e}")) - })?; - String::from_utf8(bytes.to_vec()).map_err(|e| { - KmsError::ServerError(format!("SecretData non-UTF-8 at {uri}: {e}")) - }) - } - Object::OpaqueObject(o) => String::from_utf8(o.opaque_data_value).map_err(|e| { - KmsError::ServerError(format!("OpaqueObject non-UTF-8 at {uri}: {e}")) - }), - other => Err(KmsError::ServerError(format!( - "Cosmian KMS object at {uri} has type {:?}; expected SecretData or OpaqueObject", - other.object_type() - ))), - } - } -} diff --git a/documentation/docs/configuration/secret_backends.md b/documentation/docs/configuration/secret_backends.md new file mode 100644 index 000000000..32c461efe --- /dev/null +++ b/documentation/docs/configuration/secret_backends.md @@ -0,0 +1,200 @@ +# Secret backends + +Store sensitive configuration values — database passwords, API keys, tokens — in +an external secret manager instead of plain text in the KMS TOML config file. + +## How it works + +```mermaid +sequenceDiagram + participant Operator + participant KMS Server + participant Secret Backend + + Operator->>KMS Server: cosmian_kms --secret-backend vault -c kms.toml + KMS Server->>KMS Server: Parse TOML, find secret:// URIs + KMS Server->>Secret Backend: Resolve each secret:// URI + Secret Backend-->>KMS Server: Return plaintext value + KMS Server->>KMS Server: Replace URIs with values, apply config + KMS Server->>Operator: Server ready +``` + +1. In your `kms.toml`, replace any sensitive value with a `secret://...` URI. +2. Start the KMS with `--secret-backend ` (or set `KMS_SECRET_BACKEND` env var). +3. At startup the KMS resolves every `secret://` URI **once**, then applies the config normally. + +The config file never contains actual secrets — only references. + +--- + +## Supported backends + +| Backend | `--secret-backend` | URI format | Required env vars | +|---------|-------------------|-----------|-------------------| +| HashiCorp Vault | `vault` | `secret:///[#]` | `VAULT_ADDR`, `VAULT_TOKEN` | +| AWS SSM Parameter Store | `aws-ssm` | `secret:///` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | +| Azure Key Vault | `azure-kv` | `secret:///secrets/[/]` | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` | +| Cosmian KMS | `cosmian-kms` | `secret://[:]/` | _(optional)_ `COSMIAN_KMS_SECRET_TOKEN` | + +--- + +## HashiCorp Vault + +Uses the **KV-v2** secrets engine. The URI fragment `#field` selects a specific +key in the secret's JSON data; defaults to `value` if omitted. + +### 1. Store a secret in Vault + +```bash +vault kv put secret/kms/db password=my-db-password +``` + +### 2. Reference it in `kms.toml` + +```toml +[db] +database_type = "postgresql" +postgres_url = "postgresql://kms:secret://secret/kms/db#password@db.internal:5432/kms" +``` + +Or for a standalone field: + +```toml +[db] +database_type = "postgresql" +postgres_url = "secret://secret/kms/postgres-url#value" +``` + +### 3. Start the KMS + +```bash +export VAULT_ADDR=http://vault.internal:8200 +export VAULT_TOKEN=s.xxxxxxxx +cosmian_kms --secret-backend vault -c /etc/cosmian/kms.toml +``` + +--- + +## AWS SSM Parameter Store + +Fetches a **SecureString** parameter. The URI path must include the leading `/`. + +### 1. Store a secret in SSM + +```bash +aws ssm put-parameter \ + --name /kms/db-password \ + --value "my-db-password" \ + --type SecureString \ + --region eu-west-1 +``` + +### 2. Reference it in `kms.toml` + +```toml +[db] +postgres_url = "secret://eu-west-1/kms/db-password" +``` + +### 3. Start the KMS + +```bash +export AWS_ACCESS_KEY_ID=AKIA... +export AWS_SECRET_ACCESS_KEY=... +# Optional: export AWS_SESSION_TOKEN=... +cosmian_kms --secret-backend aws-ssm -c /etc/cosmian/kms.toml +``` + +--- + +## Azure Key Vault + +Uses **service-principal client credentials** (OAuth2) to authenticate to +Azure AD, then fetches the secret from the Key Vault REST API (v7.4). + +### 1. Store a secret + +```bash +az keyvault secret set \ + --vault-name myvault \ + --name db-password \ + --value "my-db-password" +``` + +### 2. Reference it in `kms.toml` + +```toml +[db] +postgres_url = "secret://myvault/secrets/db-password" +``` + +To pin a specific version: + +```toml +postgres_url = "secret://myvault/secrets/db-password/abc123def456" +``` + +### 3. Start the KMS + +```bash +export AZURE_TENANT_ID=... +export AZURE_CLIENT_ID=... +export AZURE_CLIENT_SECRET=... +cosmian_kms --secret-backend azure-kv -c /etc/cosmian/kms.toml +``` + +--- + +## Cosmian KMS (self-referencing) + +Use another Cosmian KMS instance as the secret store. The secret must be stored +as a **SecretData** or **OpaqueObject** whose raw bytes are the UTF-8 value. + +This is useful for hierarchical deployments where a "vault KMS" holds +credentials used by downstream KMS instances. + +### 1. Import a secret into the vault KMS + +```bash +ckms secrets import --secret-data "my-db-password" --content-type "text/plain" +# → UniqueIdentifier: 550e8400-e29b-41d4-a716-446655440000 +``` + +### 2. Reference it in `kms.toml` + +```toml +[db] +postgres_url = "secret://vault-kms.internal:9998/550e8400-e29b-41d4-a716-446655440000" +``` + +For localhost (uses `http` automatically): + +```toml +postgres_url = "secret://localhost:9998/550e8400-e29b-41d4-a716-446655440000" +``` + +### 3. Start the KMS + +```bash +# Optional token if the vault KMS requires authentication: +export COSMIAN_KMS_SECRET_TOKEN=eyJhbGciOi... +# Accept self-signed certs (dev only): +export COSMIAN_KMS_INSECURE_CERTS=true +cosmian_kms --secret-backend cosmian-kms -c /etc/cosmian/kms.toml +``` + +--- + +## Security considerations + +| Concern | Mitigation | +|---------|-----------| +| Secret exposure in config files | Only `secret://` references are stored on disk — never plaintext values. | +| Secret lifetime | URIs are resolved **once at startup**. Resolved values live only in process memory. | +| Backend credentials | Always inject via environment variables, never in the TOML file. | +| Network security | Use TLS for all backend communication. For Vault and Cosmian KMS, ensure `VAULT_ADDR` uses `https://` in production. | +| Rotation | Restart the KMS to pick up rotated secrets. There is no hot-reload. | + +!!! warning "Do not mix backends" + Only one `--secret-backend` can be active at a time. All `secret://` URIs + in the config must be resolvable by the selected backend. diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 1d2dd43f5..292b96b10 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -4,59 +4,59 @@ site_url: https://docs.cosmian.com/ copyright: © Copyright 2018-2026 Eviden. All rights reserved dev_addr: localhost:8003 theme: - name: material - language: en - font: - text: Open Sans - favicon: eviden-favicon.svg - logo: eviden-logo-orange.svg - custom_dir: theme_overrides - features: - - content.tabs.link + name: material + language: en + font: + text: Open Sans + favicon: eviden-favicon.svg + logo: eviden-logo-orange.svg + custom_dir: theme_overrides + features: + - content.tabs.link extra: - homepage: https://docs.cosmian.com/ + homepage: https://docs.cosmian.com/ markdown_extensions: - - tables - - smarty - - abbr - - admonition - - toc: - permalink: true - baselevel: 1 - - pymdownx.superfences: + - tables + - smarty + - abbr + - admonition + - toc: + permalink: true + baselevel: 1 + - pymdownx.superfences: # make exceptions to highlighting of code: - custom_fences: - - name: mermaid - class: mermaid + custom_fences: + - name: mermaid + class: mermaid # Uncomment the following line to check Mermaid figures # format: !!python/name:mermaid2.fence_mermaid_custom - - pymdownx.tabbed: - alternate_style: true - - pymdownx.emoji - - pymdownx.details - - pymdownx.arithmatex: - generic: true - - pymdownx.tasklist - - pymdownx.highlight - - pymdownx.inlinehilite - - attr_list - - markdown_katex: - no_inline_svg: true - insert_fonts_css: true - - markdown_include.include: - base_path: docs - inheritHeadingDepth: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji + - pymdownx.details + - pymdownx.arithmatex: + generic: true + - pymdownx.tasklist + - pymdownx.highlight + - pymdownx.inlinehilite + - attr_list + - markdown_katex: + no_inline_svg: true + insert_fonts_css: true + - markdown_include.include: + base_path: docs + inheritHeadingDepth: true extra_javascript: #see this for Katex: https://squidfunk.github.io/mkdocs-material/reference/math/#katex - - javascripts/katex.js - - https://unpkg.com/katex@0/dist/katex.min.js - - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js - - javascripts/macros.js + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + - javascripts/macros.js extra_css: - - https://unpkg.com/katex@0/dist/katex.min.css + - https://unpkg.com/katex@0/dist/katex.min.css plugins: - - search - - kroki - - meta-descriptions + - search + - kroki + - meta-descriptions nav: - Why use the Eviden KMS: index.md - Quick start: quick_start.md @@ -144,6 +144,7 @@ nav: - User interface: configuration/ui.md - UI branding: configuration/ui_branding.md - Custom OpenSSL build: configuration/openssl_override.md + - Secret backends: configuration/secret_backends.md - Certifications and compliance: - FIPS 140-3: certifications_and_compliance/fips.md - Cryptographic algorithms: @@ -192,4 +193,4 @@ nav: - Re-Key: kmip_support/_re-key.md - Re-Key Key Pair: kmip_support/_re-key_key_pair.md - Revoke: kmip_support/_revoke.md - - Sign: kmip_support/_signature.md \ No newline at end of file + - Sign: kmip_support/_signature.md diff --git a/test_data b/test_data index 47c9a06b9..4d57832bd 160000 --- a/test_data +++ b/test_data @@ -1 +1 @@ -Subproject commit 47c9a06b99f25439c70111d9bec7b67b2377527c +Subproject commit 4d57832bd1f9498cea90188f2b198b4db2b9595c From f38c882eb53f1ef44d994ebb10e24e938325e420 Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Tue, 16 Jun 2026 09:37:38 +0200 Subject: [PATCH 4/5] update nix hashes --- nix/expected-hashes/cli.vendor.linux.sha256 | 2 +- nix/expected-hashes/server.vendor.static.sha256 | 2 +- nix/expected-hashes/ui.vendor.non-fips.sha256 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index 3e82b7626..7a4f207b7 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1 +1 @@ -sha256-plobon7/NDFYFPcdHwz6qg7lQ4IXJ45rTRJbsBizkzM= +sha256-kx7OHGJSbGI0OgxwjQriLCRPs66FnDEpZLvHxPiybDU= diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 80aaf3645..d4c53251c 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1 +1 @@ -sha256-5R15t1lUFy77NVRl+9fRQ4CcqC0Af8iEOWsS9xewAW4= +sha256-MHWm6UCn+fEIMRh1qI8a+7wktwIYfeARcW+cNvUN4Do= diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index 9e10bba6d..0bfdd6729 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-tlJl4+o7OCL6WKCBgrvEey40IbH7y0c4fVIajrKo5qg= +sha256-aDbJyIE8a8HjnFt5WXNci3zvRsL3mVnR8lQhCUf8tx8= From 89ea5b5c188087a1b8265a8a6a7a42cb441b403b Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Tue, 16 Jun 2026 10:44:23 +0200 Subject: [PATCH 5/5] fix: use unique secret name and purge on cleanup for Azure KV CI test; update ui.vendor.fips nix hash --- .github/scripts/test/test_secret_azure.sh | 6 +++++- nix/expected-hashes/ui.vendor.fips.sha256 | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/scripts/test/test_secret_azure.sh b/.github/scripts/test/test_secret_azure.sh index 783f98c47..15210bdd6 100644 --- a/.github/scripts/test/test_secret_azure.sh +++ b/.github/scripts/test/test_secret_azure.sh @@ -35,7 +35,7 @@ echo "=========================================" : "${AZURE_CLIENT_SECRET:?AZURE_CLIENT_SECRET must be set}" : "${AZURE_KV_NAME:?AZURE_KV_NAME must be set}" -SECRET_NAME="kms-ci-secret-$$" +SECRET_NAME="kms-ci-secret-$(date +%s)-${RANDOM}" SECRET_VALUE="ci-secret-value" KV_BASE_URL="https://${AZURE_KV_NAME}.vault.azure.net" @@ -58,6 +58,10 @@ cleanup() { curl -sf -X DELETE \ "${KV_BASE_URL}/secrets/${SECRET_NAME}?api-version=7.4" \ -H "Authorization: Bearer ${token}" 2>/dev/null || true + # Purge immediately so the name is not stuck in soft-delete state + curl -sf -X DELETE \ + "${KV_BASE_URL}/deletedsecrets/${SECRET_NAME}?api-version=7.4" \ + -H "Authorization: Bearer ${token}" 2>/dev/null || true fi } trap cleanup EXIT diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index 4f180e2ac..d7b9632a2 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1 @@ -sha256-LGDg3tXMnlIS0V9KyuuT6PpWSXaZc6qrgYkOLHrTBMY= +sha256-ynfXYrHJ+sNxDdbsKbVCxv4OxNQYCS7Sji1/k7Lejnc=