Zero-OS Compliance Module
Source:
kernel/compliance/lib.rs
Crate:compliance(v0.1.0)
Environment:#![no_std]
Table of Contents
- Overview
- Architecture and Design Principles
- Hardening Profiles
- FIPS Mode
- Cryptographic Algorithm Policy
- FIPS Known Answer Tests (KAT)
- Audit Integration
- Integration with Security Module
- Compliance Reporting
- Key Data Structures
- Concurrency and Safety
- Line-by-Line Explanation of Critical Functions
- Boot Sequence Integration
- Bug Fix History
Overview
The compliance module is the centralized policy engine for Zero-OS kernel security posture. It governs two orthogonal axes of system hardening:
-
Hardening Profiles -- Boot-time selection of security vs. performance tradeoffs (Secure / Balanced / Performance). Each profile maps to a concrete
SecurityConfigthat controls W^X enforcement, Spectre mitigations, kernel pointer obfuscation, and audit buffer sizing. -
FIPS Mode -- A runtime-activatable, sticky (one-way until reboot) flag that enforces FIPS 140-2 / 140-3 compliance. When enabled, only FIPS-approved cryptographic algorithms are permitted, and all crypto operations are audited. Activation requires passing a suite of NIST Known Answer Tests (KAT).
The module is designed for a bare-metal no_std kernel environment. It uses only lock-free atomics for global state, never allocates heap memory, and integrates with the audit, security, and livepatch crates.
Dependencies
| Crate | Role |
|---|---|
security | Receives SecurityConfig generated from the active hardening profile |
audit | Provides SHA-256 / HMAC-SHA256 crypto primitives and the event emission API |
livepatch | Provides ECDSA P-256 verifier and its KAT (run_kat_if_needed) |
spin | Spinlock primitives (listed in Cargo.toml, not directly used in current source) |
x86_64 | VirtAddr type for physical memory offset |
Architecture and Design Principles
The compliance module follows several security engineering principles:
-
Fail-Closed: Every match on an atomic value includes a wildcard arm (
_) that falls back to the most restrictive state. Corrupted profile values default toSecure; corrupted FIPS values default toFailed. This ensures memory corruption or bit-flips never weaken the security posture. -
Sticky State: FIPS mode, once enabled, cannot be disabled. The
FIPS_MODEatomic can only transition fromDisabled -> EnabledorDisabled -> Failed. There is no reverse path. -
Single Source of Truth: Rather than duplicating cryptographic implementations, the compliance module delegates KAT execution to the owning subsystems (
audit::cryptofor SHA-256/HMAC,livepatch::ecdsa_p256for ECDSA). This prevents implementation drift. -
Zero Allocation: The entire module operates without heap allocation, making it safe to call during early boot before the allocator is initialized.
-
Lock-Free Concurrency: All global state uses
core::sync::atomictypes with explicitOrderingannotations. No mutexes or spinlocks are held during normal operation.
Hardening Profiles
Enum Definition
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum HardeningProfile {
Secure = 0,
Balanced = 1,
Performance = 2,
}
The #[repr(u8)] ensures the enum has a stable, single-byte representation suitable for atomic storage.
Profile Comparison Matrix
| Feature | Secure | Balanced | Performance |
|---|---|---|---|
| W^X Enforcement | Strict (panic on violation) | Warn only | Disabled |
| NX Enforcement | Yes | Yes | Yes |
| Spectre Mitigations (IBRS/IBPB/STIBP) | Full | Full | Disabled |
| Kernel Pointer Obfuscation (kptr guard) | Yes | Yes | Disabled |
| Identity Map Cleanup | RemoveWritable | RemoveWritable | RemoveWritable |
| RNG Initialization | Yes | Yes | Yes |
| Security Self-Tests | Yes | No | No |
| Audit Ring Capacity | 256 entries | 128 entries | 64 entries |
| Recommended Use | Production, high-security | Development, general use | Benchmarking, low-latency |
Profile Parsing
The from_str method accepts case-insensitive aliases:
| Profile | Accepted Strings |
|---|---|
| Secure | secure, strict, hardened |
| Balanced | balanced, default, normal |
| Performance | performance, perf, fast |
Parsing uses a fixed 16-byte stack buffer for case conversion, avoiding heap allocation. Strings longer than 16 bytes are silently truncated.
Default Profile
The Default implementation returns Balanced, which provides a reasonable security/performance tradeoff for development environments.
SecurityConfig Generation
Each profile generates a complete security::SecurityConfig struct via the security_config(&self, phys_offset) method. The phys_offset parameter (a x86_64::VirtAddr) must be provided by the memory subsystem and represents the virtual address where physical memory is identity-mapped.
Key differences between profiles in the generated SecurityConfig:
- Secure:
strict_wxorx = true(panics on W^X violation),run_security_tests = true - Balanced:
strict_wxorx = false(warns but continues),run_security_tests = false - Performance:
validate_wxorx = false,enable_kptr_guard = false,enable_spectre_mitigations = false
All three profiles share: cleanup_strategy = RemoveWritable, enforce_nx = true, initialize_rng = true.
FIPS Mode
State Machine
FIPS mode is modeled as a three-state machine:
enable_fips_mode()
Disabled --------------------------> Enabled
|
| (self-test failure)
+--------------> Failed
- Unknown/corrupted atomic values map to
Failed(fail-closed). EnabledandFailedare terminal states (no transitions out until reboot).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FipsState {
Disabled = 0,
Enabled = 1,
Failed = 2,
}
State transitions:
Disabled -> Enabled: Successful call toenable_fips_mode()after all KATs pass.Disabled -> Failed: Call toenable_fips_mode()where a KAT fails.Enabled -> (no transitions): FIPS mode is sticky; it cannot be disabled.Failed -> (no transitions): A failed state is permanent until reboot.
Sticky Semantics
Once FIPS_MODE is set to Enabled, there is no API to set it back to Disabled. The only way to exit FIPS mode is a system reboot, which reinitializes the AtomicU8 to 0 (Disabled).
Error Types
pub enum FipsError {
AlreadyEnabled, // FIPS mode is already active
EnableFailed, // FIPS mode previously failed (permanent)
NotPermitted, // Operation blocked by FIPS policy
SelfTestFailed, // KAT failure during enable
AccessDenied, // Privilege check failed
}
Cryptographic Algorithm Policy
Algorithm Registry
pub enum CryptoAlgorithm {
// Hash functions
Sha256, Sha384, Sha512, Blake2b,
// MACs
HmacSha256,
// Signatures
EcdsaP256, EcdsaP384, Ed25519,
// Encryption
Aes128Gcm, Aes256Gcm, ChaCha20, ChaCha20Poly1305,
}
FIPS Approval Matrix
| Algorithm | FIPS Approved | Standard |
|---|---|---|
| SHA-256 | Yes | FIPS 180-4 |
| SHA-384 | Yes | FIPS 180-4 |
| SHA-512 | Yes | FIPS 180-4 |
| Blake2b | No | RFC 7693 |
| HMAC-SHA256 | Yes | FIPS 198-1 |
| ECDSA P-256 | Yes | FIPS 186-4 |
| ECDSA P-384 | Yes | FIPS 186-4 |
| Ed25519 | No | RFC 8032 |
| AES-128-GCM | Yes | FIPS 197 / SP 800-38D |
| AES-256-GCM | Yes | FIPS 197 / SP 800-38D |
| ChaCha20 | No | RFC 8439 |
| ChaCha20-Poly1305 | No | RFC 8439 |
Policy Enforcement Logic
The is_algorithm_permitted(algorithm) function implements a three-way decision:
- FIPS Disabled: All algorithms are permitted (returns
trueimmediately). - FIPS Enabled: Only FIPS-approved algorithms are permitted (checked via match table).
- FIPS Failed: No algorithms are permitted (returns
falseimmediately). This is the fail-closed behavior -- if FIPS self-tests failed, all cryptographic operations are blocked.
FIPS Known Answer Tests (KAT)
The fips_kat submodule implements NIST-mandated Known Answer Tests that must pass before FIPS mode can be activated.
Test Suite
| Test | Standard | Vectors | Implementation |
|---|---|---|---|
| SHA-256 | FIPS 180-4 / CAVP | Empty string, "abc", 1M x 'a' | audit::crypto::sha256_digest / StreamingSha256 |
| HMAC-SHA256 | NIST CSRC / RFC 4231 | 64-byte key, 32-byte key, 100-byte key | audit::crypto::hmac_sha256_digest |
| ECDSA P-256 | RFC 6979 A.2.5 | Deterministic signature verification | livepatch::ecdsa_p256::run_kat_if_needed |
SHA-256 KAT Details
Three test vectors are validated:
- Empty message:
SHA-256("") = e3b0c442...-- Tests the base case with no input data. - Short message:
SHA-256("abc") = ba7816bf...-- Tests a single-block input. - Long message (streaming):
SHA-256("a" * 1,000,000) = cdc76e5c...-- Tests the streaming hasher interface by feeding data in 64-byte blocks (matching SHA-256's internal block size). This exercises multi-block processing and padding logic without heap allocation.
HMAC-SHA256 KAT Details
Three key-length scenarios are tested per NIST CSRC:
- Key = block length (64 bytes): Key
0x00..0x3f, message"Sample message for keylen=blocklen". Tests the standard path where the key fits exactly in one block. - Key < block length (32 bytes): Key
0x00..0x1f, message"Sample message for keylen<blocklen". Tests the zero-padding path. - Key > block length (100 bytes): Key
0x00..0x63, message"Sample message for keylen=blocklen". Tests the key-hashing path where HMAC must first hash the key to reduce it to block size.
ECDSA P-256 KAT Details
Delegates to livepatch::ecdsa_p256::run_kat_if_needed(), which runs a deterministic ECDSA test vector from RFC 6979 Appendix A.2.5:
- Private key:
d = 0xC9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721 - Message:
"sample"(prehashed with SHA-256) - The signature
(r, s)is verified against the public key derived fromd.
Constant-Time Comparison
All KAT result comparisons use a constant-time equality function (ct_eq) to prevent timing side-channels:
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for i in 0..a.len() {
diff |= a[i] ^ b[i];
}
diff == 0
}
This function accumulates XOR differences across all bytes and only checks the result at the end, ensuring the comparison time is independent of where mismatches occur.
Audit Integration
FIPS state changes are recorded via the audit subsystem. The emit_fips_audit_event function constructs an audit event with:
- Kind:
AuditKind::Security(category 5) - Outcome:
AuditOutcome::SuccessorAuditOutcome::Error - Subject:
AuditSubject::kernel()(the kernel itself is the actor) - Object:
AuditObject::None(no target resource) - Args:
[event_type, success_flag]whereevent_type = 1(FIPS enable event) andsuccess_flagis1(success) or0(failure) - Errno:
0(no error code) - Timestamp:
0(filled by the audit subsystem)
The audit event is emitted on two occasions:
- Successful FIPS activation (
success = true, message = "enabled") - Failed FIPS activation due to KAT failure (
success = false, message = "self-test failed")
The emit call result is explicitly discarded (let _ = ...) because audit failure must not prevent FIPS state transitions -- the FIPS state change is the primary operation, and audit logging is best-effort.
Integration with Security Module
The compliance module is the bridge between high-level policy (hardening profiles) and low-level enforcement (the security crate). The integration flow is:
Boot Cmdline --> compliance::HardeningProfile::from_str()
|
v
compliance::set_profile(profile)
|
v
profile.security_config(phys_offset)
|
v
security::SecurityConfig { ... }
|
v
security::init(config, &mut frame_allocator)
|
v
compliance::lock_profile() // Immutable until reboot
The SecurityConfig struct fields and their meanings:
| Field | Type | Description |
|---|---|---|
phys_offset | VirtAddr | Virtual address of physical memory mapping |
cleanup_strategy | IdentityCleanupStrategy | How to handle bootloader identity map (always RemoveWritable) |
enforce_nx | bool | Set NX bit on data pages |
validate_wxorx | bool | Check for W^X violations |
strict_wxorx | bool | Panic (vs. warn) on W^X violation |
initialize_rng | bool | Initialize RDRAND/RDSEED + ChaCha20 CSPRNG |
enable_kptr_guard | bool | Obfuscate kernel pointers in logs |
enable_spectre_mitigations | bool | Enable IBRS/IBPB/STIBP |
run_security_tests | bool | Run runtime security self-tests |
Compliance Reporting
The ComplianceStatus struct provides a snapshot of the current compliance posture:
pub struct ComplianceStatus {
pub profile: HardeningProfile,
pub profile_locked: bool,
pub fips_state: FipsState,
}
Obtained via compliance::status(), this struct aggregates:
- The active hardening profile (read from
ACTIVE_PROFILEatomic) - Whether the profile is locked (read from
PROFILE_LOCKEDatomic) - The current FIPS state (read from
FIPS_MODEatomic, with fail-closed semantics)
This is useful for runtime introspection, health checks, and compliance dashboards.
Key Data Structures
Global Atomics
| Static | Type | Initial Value | Purpose |
|---|---|---|---|
ACTIVE_PROFILE | AtomicU8 | 1 (Balanced) | Stores the active hardening profile |
PROFILE_LOCKED | AtomicBool | false | Guards against post-boot profile changes |
FIPS_MODE | AtomicU8 | 0 (Disabled) | Stores the FIPS state |
FIPS_ENABLING | AtomicBool | false | Serializes concurrent FIPS enable attempts |
Enums
| Enum | Repr | Variants | Purpose |
|---|---|---|---|
HardeningProfile | u8 | Secure(0), Balanced(1), Performance(2) | Security level selection |
FipsState | u8 | Disabled(0), Enabled(1), Failed(2) | FIPS mode state machine |
FipsError | -- | AlreadyEnabled, EnableFailed, NotPermitted, SelfTestFailed, AccessDenied | Error reporting |
CryptoAlgorithm | -- | 12 variants (hashes, MACs, signatures, encryption) | Algorithm identification |
Structs
| Struct | Fields | Purpose |
|---|---|---|
ComplianceStatus | profile, profile_locked, fips_state | Runtime compliance snapshot |
Concurrency and Safety
Memory Ordering
The module uses explicit atomic orderings throughout:
-
Ordering::Acquireon all loads: Ensures that reads of the atomic value see all writes that happened-before the correspondingReleasestore. -
Ordering::Releaseon all stores: Ensures that the store is visible to subsequentAcquireloads on other cores. -
Ordering::AcqReloncompare_exchange: Used in the FIPS enable serialization lock (FIPS_ENABLING).
FIPS Enable Serialization
The enable_fips_mode() function uses a spinlock pattern via FIPS_ENABLING to serialize concurrent enable attempts:
Thread A: CAS(FIPS_ENABLING, false -> true) --> succeeds, runs KATs
Thread B: CAS(FIPS_ENABLING, false -> true) --> fails, spins
Thread A: KATs pass, stores FIPS_MODE = Enabled
Thread A: EnableGuard drops, stores FIPS_ENABLING = false
Thread B: CAS succeeds, re-checks FIPS_MODE --> AlreadyEnabled, returns error
The EnableGuard RAII struct ensures FIPS_ENABLING is always released, even if a panic occurs during KAT execution.
TOCTOU Prevention
The enable_fips_mode() function checks fips_state() twice:
- Before acquiring the enable lock (fast-path rejection)
- After acquiring the enable lock (prevents TOCTOU race)
Profile Locking
The profile lock is a one-way latch: PROFILE_LOCKED transitions from false to true and never back. This is safe because set_profile() is only called during early single-threaded boot, and lock_profile() is called immediately after.
Line-by-Line Explanation of Critical Functions
enable_fips_mode()
pub fn enable_fips_mode() -> Result<(), FipsError> {
Public entry point. Returns Ok(()) on successful FIPS activation, or a typed error.
match fips_state() {
FipsState::Enabled => return Err(FipsError::AlreadyEnabled),
FipsState::Failed => return Err(FipsError::EnableFailed),
FipsState::Disabled => {}
}
Fast-path check (lines 249-253): Before attempting the expensive spinlock acquisition and KAT execution, reject immediately if FIPS is already enabled or permanently failed.
while FIPS_ENABLING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
core::hint::spin_loop();
}
Spinlock acquisition (lines 256-261): Atomically attempts to set FIPS_ENABLING from false to true. If another thread holds the lock, spins with core::hint::spin_loop() (emits a PAUSE instruction on x86).
struct EnableGuard;
impl Drop for EnableGuard {
fn drop(&mut self) {
FIPS_ENABLING.store(false, Ordering::Release);
}
}
let _guard = EnableGuard;
RAII guard (lines 264-270): Defines a zero-sized type whose Drop implementation releases the enable lock. This guarantees the lock is released on all exit paths.
match fips_state() {
FipsState::Enabled => return Err(FipsError::AlreadyEnabled),
FipsState::Failed => return Err(FipsError::EnableFailed),
FipsState::Disabled => {}
}
TOCTOU re-check (lines 273-277): After acquiring the lock, re-read the FIPS state to prevent race conditions.
if !run_fips_self_tests() {
FIPS_MODE.store(FipsState::Failed as u8, Ordering::Release);
emit_fips_audit_event(false, "self-test failed");
return Err(FipsError::SelfTestFailed);
}
KAT execution (lines 280-284): Runs all FIPS Known Answer Tests. If any test fails, the FIPS state is permanently set to Failed.
FIPS_MODE.store(FipsState::Enabled as u8, Ordering::Release);
emit_fips_audit_event(true, "enabled");
Ok(())
Activation (lines 287-290): All KATs passed. Set FIPS mode to Enabled (sticky), emit a success audit event, and return Ok.
current_profile()
pub fn current_profile() -> HardeningProfile {
match ACTIVE_PROFILE.load(Ordering::Acquire) {
0 => HardeningProfile::Secure,
1 => HardeningProfile::Balanced,
2 => HardeningProfile::Performance,
_ => HardeningProfile::Secure, // Fail-closed
}
}
The wildcard arm (_) implements the fail-closed principle: if the atomic value is corrupted, the system defaults to the most restrictive Secure profile.
fips_state()
pub fn fips_state() -> FipsState {
match FIPS_MODE.load(Ordering::Acquire) {
0 => FipsState::Disabled,
1 => FipsState::Enabled,
2 => FipsState::Failed,
_ => FipsState::Failed, // Fail-closed
}
}
The fail-closed wildcard maps corrupted values to Failed, which causes is_algorithm_permitted() to block all algorithms.
is_algorithm_permitted()
Two-stage decision: first check the FIPS state (short-circuit for Disabled/Failed), then check the specific algorithm against the FIPS approval list. The Failed state blocks everything.
fips_kat::ct_eq()
Constant-time byte comparison. XORs each pair of bytes and ORs the result into an accumulator. The loop always executes for the full length regardless of where mismatches occur, preventing timing side-channels.
Boot Sequence Integration
The compliance module is integrated into the Zero-OS boot sequence as follows (from kernel/src/main.rs):
[2.6/3] Applying security hardening...
1. Select profile: compliance::HardeningProfile::Balanced
2. Set profile: compliance::set_profile(profile)
3. Generate config: profile.security_config(phys_offset)
4. Print info: "Profile: Balanced (audit_capacity: 128)"
5. Init security: security::init(sec_config, &mut frame_allocator)
6. Lock profile: compliance::lock_profile()
7. Print: "Profile locked (immutable until reboot)"
After lock_profile() is called, any subsequent call to set_profile() returns false and has no effect.
Currently, the profile is hardcoded to Balanced in the kernel main. Production deployments would parse the boot command line (e.g., profile=secure) and use HardeningProfile::from_str() to select the appropriate profile.
Bug Fix History
| Fix ID | Description | Impact |
|---|---|---|
| R93-1 | Serialize FIPS enable attempts with FIPS_ENABLING spinlock and TOCTOU re-check | Prevents race conditions between concurrent enable_fips_mode() calls |
| R93-1 | Fail-closed on FIPS self-test failure in is_algorithm_permitted() | FipsState::Failed blocks all algorithms instead of allowing them |
| R93-14 | Real FIPS 140-2/140-3 Known Answer Tests (SHA-256, HMAC-SHA256, ECDSA P-256) | Replaces placeholder self-tests with NIST-specified test vectors |
| R94-3 | Fail-closed on FIPS state corruption (_ => FipsState::Failed) | Corrupted atomic values no longer fall back to Disabled |
| R94-4 | Fail-closed on profile corruption (_ => HardeningProfile::Secure) | Corrupted atomic values no longer fall back to Balanced |