diff --git a/CHANGELOG/feat_openssl_cpu_features.md b/CHANGELOG/feat_openssl_cpu_features.md new file mode 100644 index 0000000000..8ac04b7e3d --- /dev/null +++ b/CHANGELOG/feat_openssl_cpu_features.md @@ -0,0 +1,3 @@ +## Features + +- Log OpenSSL CPU hardware-acceleration feature flags (AES-NI, PCLMULQDQ, AVX, AVX2, SHA, VAES, RDRAND, etc.) at server startup for compliance and audit purposes. Decoded for x86_64, AArch64, and PowerPC; raw string logged for other architectures. diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index 3912aeffa2..1404554cbc 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use cosmian_kms_server::{ config::{ClapConfig, OpenTelemetryConfig, ServerParams, wizard::run_configure_wizard}, core::KMS, - openssl_providers::safe_openssl_version_info, + openssl_providers::{cpu_features_info, safe_openssl_version_info}, result::{KResult, KResultHelper}, }; use cosmian_logger::{TelemetryConfig, TracingConfig, info, tracing_init}; @@ -124,6 +124,7 @@ async fn run() -> KResult<()> { env!("CARGO_PKG_VERSION") ); info!("OpenSSL version: {ossl_version}, in {ossl_dir}, number: {ossl_number:x}"); + info!("{}", cpu_features_info()); // For an explanation of OpenSSL providers, // https://docs.openssl.org/3.1/man7/crypto/#openssl-providers diff --git a/crate/server/src/openssl_providers.rs b/crate/server/src/openssl_providers.rs index 1c7bceb03b..0da66e049b 100644 --- a/crate/server/src/openssl_providers.rs +++ b/crate/server/src/openssl_providers.rs @@ -1,4 +1,4 @@ -use std::ffi::CStr; +use std::ffi::{CStr, c_int}; use cosmian_logger::info; @@ -62,6 +62,227 @@ pub fn safe_openssl_version_info() -> (String, String, u64) { (version, dir, num) } +/// Retrieve human-readable OpenSSL CPU hardware-acceleration feature flags detected +/// at runtime. +/// +/// `OpenSSL_version(OPENSSL_CPU_INFO)` (constant `9`, not yet exposed by `openssl-sys`) +/// returns the raw capability bitmask string that OpenSSL assembled during its own +/// `OPENSSL_cpuid_setup` call. This function decodes that bitmask for the known +/// architectures and falls back to the raw string for unknown ones. +/// +/// Supported architectures: +/// - **x86_64**: five packed `u64` words (`OPENSSL_ia32cap_P[0..10]`) covering +/// CPUID leaves 1, 7.0, 7.1, and 24. Each bit is mapped to a well-known flag name. +/// - **AArch64**: a single `u32` word (`OPENSSL_armcap_P`) with ARMv7/v8 extension flags. +/// - **PowerPC**: a single `u32` word (`OPENSSL_ppccap_P`) with AltiVec / VSX flags. +/// - **s390x, RISC-V, other**: the raw OpenSSL string is returned verbatim. +/// +/// # Returns +/// +/// A `String` such as: +/// ```text +/// OpenSSL CPU features (x86_64): AES-NI, PCLMULQDQ, AVX, AVX2, SHA, RDRAND +/// ``` +#[allow(unsafe_code)] +#[must_use] +pub fn cpu_features_info() -> String { + // OPENSSL_CPU_INFO = 9; this constant is not yet exposed by openssl-sys. + const OPENSSL_CPU_INFO_QUERY: c_int = 9; + + let raw = unsafe { + let ptr = openssl_sys::OpenSSL_version(OPENSSL_CPU_INFO_QUERY); + if ptr.is_null() { + return "OpenSSL CPU features: ".to_owned(); + } + match CStr::from_ptr(ptr).to_str() { + Ok(s) => s.to_owned(), + Err(_) => return "OpenSSL CPU features: ".to_owned(), + } + }; + + // Strip optional " env:..." suffix that OpenSSL may append when the + // OPENSSL_ia32cap / OPENSSL_armcap env vars override capabilities. + // Also strip the leading "CPUINFO: " prefix that OpenSSL_version(9) includes. + let raw = raw + .split_once(" env:") + .map_or(raw.as_str(), |(head, _)| head) + .trim_start_matches("CPUINFO: ") + .trim() + .to_owned(); + + if let Some(hex) = raw.strip_prefix("OPENSSL_ia32cap=") { + return format!("OpenSSL CPU features (x86_64): {}", decode_ia32cap(hex)); + } + if let Some(hex) = raw.strip_prefix("OPENSSL_armcap=") { + return format!("OpenSSL CPU features (aarch64): {}", decode_armcap(hex)); + } + if let Some(hex) = raw.strip_prefix("OPENSSL_ppccap=") { + return format!("OpenSSL CPU features (powerpc): {}", decode_ppccap(hex)); + } + + // s390x, RISC-V, or any future architecture: log the raw string verbatim. + format!("OpenSSL CPU features: {raw}") +} + +/// Decode the x86_64 `OPENSSL_ia32cap` five-word hex string into named flags. +/// +/// Format: `0xW0:0xW1:0xW2:0xW3:0xW4` — five packed `u64` where each word packs +/// two 32-bit CPUID output registers (low = first register, high = second register): +/// +/// | Word | Low 32 bits | High 32 bits | +/// |------|-----------------|-----------------| +/// | 0 | CPUID.1.EDX | CPUID.1.ECX | +/// | 1 | CPUID.7.0.EBX | CPUID.7.0.ECX | +/// | 2 | CPUID.7.0.EDX | CPUID.7.1.EAX | +/// | 3 | CPUID.7.1.EDX | CPUID.7.1.EBX | +/// | 4 | CPUID.7.1.ECX | CPUID.24.0.EBX | +fn decode_ia32cap(hex: &str) -> String { + let words: Vec = hex + .split(':') + .map(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).unwrap_or(0)) + .collect(); + + if words.len() < 5 { + return format!(""); + } + + // (word_index, bit_position_within_word, display_name) + // Bits 0-31 map to the low u32 register; bits 32-63 to the high u32 register. + #[rustfmt::skip] + const FLAGS: &[(usize, u32, &str)] = &[ + // Word 0 — CPUID leaf 1, EDX (bits 0-31) + ECX (bits 32-63) + (0, 23, "MMX"), + (0, 25, "SSE"), + (0, 26, "SSE2"), + (0, 32, "SSE3"), + (0, 33, "PCLMULQDQ"), + (0, 41, "SSSE3"), + (0, 44, "FMA"), + (0, 51, "SSE4.1"), + (0, 52, "SSE4.2"), + (0, 54, "MOVBE"), + (0, 55, "POPCNT"), + (0, 57, "AES-NI"), + (0, 58, "XSAVE"), + (0, 60, "AVX"), + (0, 62, "RDRAND"), + // Word 1 — CPUID leaf 7 subleaf 0, EBX (bits 0-31) + ECX (bits 32-63) + (1, 3, "BMI1"), + (1, 5, "AVX2"), + (1, 8, "BMI2"), + (1, 16, "AVX512F"), + (1, 17, "AVX512DQ"), + (1, 18, "RDSEED"), + (1, 19, "ADX"), + (1, 21, "AVX512IFMA"), + (1, 28, "AVX512CD"), + (1, 29, "SHA"), + (1, 30, "AVX512BW"), + (1, 31, "AVX512VL"), + (1, 33, "AVX512VBMI"), + (1, 38, "AVX512VBMI2"), + (1, 40, "GFNI"), + (1, 41, "VAES"), + (1, 42, "VPCLMULQDQ"), + (1, 43, "AVX512VNNI"), + (1, 44, "AVX512BITALG"), + (1, 46, "AVX512VPOPCNTDQ"), + // Word 2 — CPUID.7.0.EDX (bits 0-31) + CPUID.7.1.EAX (bits 32-63) + (2, 2, "AVX512-4VNNIW"), + (2, 3, "AVX512-4FMAPS"), + (2, 8, "AVX512VP2INTERSECT"), + (2, 36, "AVX-VNNI"), + (2, 37, "AVX512-BF16"), + // Word 3 — CPUID.7.1.EDX (bits 0-31) + CPUID.7.1.EBX (bits 32-63) + (3, 32, "SHA512"), + (3, 33, "SM3"), + (3, 34, "SM4"), + // Word 4 — CPUID.7.1.ECX (bits 0-31) + CPUID.24.0.EBX (bits 32-63, AVX10) + (4, 4, "AVX-NE-CONVERT"), + (4, 5, "AVX-VNNI-INT8"), + ]; + + let enabled: Vec<&str> = FLAGS + .iter() + .filter(|&&(w, bit, _)| (words[w] >> bit) & 1 == 1) + .map(|&(_, _, name)| name) + .collect(); + + if enabled.is_empty() { + "".to_owned() + } else { + enabled.join(", ") + } +} + +/// Decode the AArch64 `OPENSSL_armcap` single-word hex string into named flags. +/// +/// The value is a 32-bit bitmask (`OPENSSL_armcap_P`) populated by the kernel +/// auxiliary vector (AT_HWCAP / AT_HWCAP2) during `OPENSSL_cpuid_setup`. +fn decode_armcap(hex: &str) -> String { + let cap = u32::from_str_radix(hex.trim_start_matches("0x"), 16).unwrap_or(0); + + #[rustfmt::skip] + const FLAGS: &[(u32, &str)] = &[ + (1 << 0, "NEON"), + (1 << 2, "ARMV8_AES"), + (1 << 3, "ARMV8_SHA1"), + (1 << 4, "ARMV8_SHA256"), + (1 << 5, "ARMV8_PMULL"), + (1 << 6, "ARMV8_SHA512"), + (1 << 8, "ARMV8_RNG"), + (1 << 9, "ARMV8_SM3"), + (1 << 10, "ARMV8_SM4"), + (1 << 11, "ARMV8_SHA3"), + (1 << 12, "ARMV8_UNROLL8_EOR3"), + (1 << 13, "ARMV8_SVE"), + (1 << 14, "ARMV8_SVE2"), + (1 << 16, "ARMV8_UNROLL12_EOR3"), + ]; + + let enabled: Vec<&str> = FLAGS + .iter() + .filter(|&&(mask, _)| cap & mask != 0) + .map(|&(_, name)| name) + .collect(); + + if enabled.is_empty() { + "".to_owned() + } else { + enabled.join(", ") + } +} + +/// Decode the PowerPC `OPENSSL_ppccap` single-word hex string into named flags. +/// +/// The value is a 32-bit bitmask (`OPENSSL_ppccap_P`) populated during +/// `OPENSSL_cpuid_setup` from the Linux auxiliary vector. +fn decode_ppccap(hex: &str) -> String { + let cap = u32::from_str_radix(hex.trim_start_matches("0x"), 16).unwrap_or(0); + + #[rustfmt::skip] + const FLAGS: &[(u32, &str)] = &[ + (1 << 0, "PPC_FPU64"), + (1 << 1, "PPC_ALTIVEC"), + (1 << 2, "PPC_CRYPTO207"), + (1 << 3, "PPC_FPU"), + (1 << 4, "PPC_MADD300"), + (1 << 5, "PPC_MFTB"), + ]; + + let enabled: Vec<&str> = FLAGS + .iter() + .filter(|&&(mask, _)| cap & mask != 0) + .map(|&(_, name)| name) + .collect(); + + if enabled.is_empty() { + "".to_owned() + } else { + enabled.join(", ") + } +} + /// Initialize OpenSSL providers for test environments. /// /// In non-FIPS mode with OpenSSL >= 3.0: loads the legacy provider for old PKCS#12 formats.