Zero-OS Compliance Module

Source: kernel/compliance/lib.rs
Crate: compliance (v0.1.0)
Environment: #![no_std]

Table of Contents

  1. Overview
  2. Architecture and Design Principles
  3. Hardening Profiles
  4. FIPS Mode
  5. Cryptographic Algorithm Policy
  6. FIPS Known Answer Tests (KAT)
  7. Audit Integration
  8. Integration with Security Module
  9. Compliance Reporting
  10. Key Data Structures
  11. Concurrency and Safety
  12. Line-by-Line Explanation of Critical Functions
  13. Boot Sequence Integration
  14. 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:

  1. Hardening Profiles -- Boot-time selection of security vs. performance tradeoffs (Secure / Balanced / Performance). Each profile maps to a concrete SecurityConfig that controls W^X enforcement, Spectre mitigations, kernel pointer obfuscation, and audit buffer sizing.

  2. 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

CrateRole
securityReceives SecurityConfig generated from the active hardening profile
auditProvides SHA-256 / HMAC-SHA256 crypto primitives and the event emission API
livepatchProvides ECDSA P-256 verifier and its KAT (run_kat_if_needed)
spinSpinlock primitives (listed in Cargo.toml, not directly used in current source)
x86_64VirtAddr 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 to Secure; corrupted FIPS values default to Failed. This ensures memory corruption or bit-flips never weaken the security posture.

  • Sticky State: FIPS mode, once enabled, cannot be disabled. The FIPS_MODE atomic can only transition from Disabled -> Enabled or Disabled -> 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::crypto for SHA-256/HMAC, livepatch::ecdsa_p256 for 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::atomic types with explicit Ordering annotations. 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

FeatureSecureBalancedPerformance
W^X EnforcementStrict (panic on violation)Warn onlyDisabled
NX EnforcementYesYesYes
Spectre Mitigations (IBRS/IBPB/STIBP)FullFullDisabled
Kernel Pointer Obfuscation (kptr guard)YesYesDisabled
Identity Map CleanupRemoveWritableRemoveWritableRemoveWritable
RNG InitializationYesYesYes
Security Self-TestsYesNoNo
Audit Ring Capacity256 entries128 entries64 entries
Recommended UseProduction, high-securityDevelopment, general useBenchmarking, low-latency

Profile Parsing

The from_str method accepts case-insensitive aliases:

ProfileAccepted Strings
Securesecure, strict, hardened
Balancedbalanced, default, normal
Performanceperformance, 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).
  • Enabled and Failed are 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 to enable_fips_mode() after all KATs pass.
  • Disabled -> Failed: Call to enable_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

AlgorithmFIPS ApprovedStandard
SHA-256YesFIPS 180-4
SHA-384YesFIPS 180-4
SHA-512YesFIPS 180-4
Blake2bNoRFC 7693
HMAC-SHA256YesFIPS 198-1
ECDSA P-256YesFIPS 186-4
ECDSA P-384YesFIPS 186-4
Ed25519NoRFC 8032
AES-128-GCMYesFIPS 197 / SP 800-38D
AES-256-GCMYesFIPS 197 / SP 800-38D
ChaCha20NoRFC 8439
ChaCha20-Poly1305NoRFC 8439

Policy Enforcement Logic

The is_algorithm_permitted(algorithm) function implements a three-way decision:

  1. FIPS Disabled: All algorithms are permitted (returns true immediately).
  2. FIPS Enabled: Only FIPS-approved algorithms are permitted (checked via match table).
  3. FIPS Failed: No algorithms are permitted (returns false immediately). 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

TestStandardVectorsImplementation
SHA-256FIPS 180-4 / CAVPEmpty string, "abc", 1M x 'a'audit::crypto::sha256_digest / StreamingSha256
HMAC-SHA256NIST CSRC / RFC 423164-byte key, 32-byte key, 100-byte keyaudit::crypto::hmac_sha256_digest
ECDSA P-256RFC 6979 A.2.5Deterministic signature verificationlivepatch::ecdsa_p256::run_kat_if_needed

SHA-256 KAT Details

Three test vectors are validated:

  1. Empty message: SHA-256("") = e3b0c442... -- Tests the base case with no input data.
  2. Short message: SHA-256("abc") = ba7816bf... -- Tests a single-block input.
  3. 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:

  1. 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.
  2. Key < block length (32 bytes): Key 0x00..0x1f, message "Sample message for keylen<blocklen". Tests the zero-padding path.
  3. 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 from d.

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::Success or AuditOutcome::Error
  • Subject: AuditSubject::kernel() (the kernel itself is the actor)
  • Object: AuditObject::None (no target resource)
  • Args: [event_type, success_flag] where event_type = 1 (FIPS enable event) and success_flag is 1 (success) or 0 (failure)
  • Errno: 0 (no error code)
  • Timestamp: 0 (filled by the audit subsystem)

The audit event is emitted on two occasions:

  1. Successful FIPS activation (success = true, message = "enabled")
  2. 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:

FieldTypeDescription
phys_offsetVirtAddrVirtual address of physical memory mapping
cleanup_strategyIdentityCleanupStrategyHow to handle bootloader identity map (always RemoveWritable)
enforce_nxboolSet NX bit on data pages
validate_wxorxboolCheck for W^X violations
strict_wxorxboolPanic (vs. warn) on W^X violation
initialize_rngboolInitialize RDRAND/RDSEED + ChaCha20 CSPRNG
enable_kptr_guardboolObfuscate kernel pointers in logs
enable_spectre_mitigationsboolEnable IBRS/IBPB/STIBP
run_security_testsboolRun 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_PROFILE atomic)
  • Whether the profile is locked (read from PROFILE_LOCKED atomic)
  • The current FIPS state (read from FIPS_MODE atomic, with fail-closed semantics)

This is useful for runtime introspection, health checks, and compliance dashboards.


Key Data Structures

Global Atomics

StaticTypeInitial ValuePurpose
ACTIVE_PROFILEAtomicU81 (Balanced)Stores the active hardening profile
PROFILE_LOCKEDAtomicBoolfalseGuards against post-boot profile changes
FIPS_MODEAtomicU80 (Disabled)Stores the FIPS state
FIPS_ENABLINGAtomicBoolfalseSerializes concurrent FIPS enable attempts

Enums

EnumReprVariantsPurpose
HardeningProfileu8Secure(0), Balanced(1), Performance(2)Security level selection
FipsStateu8Disabled(0), Enabled(1), Failed(2)FIPS mode state machine
FipsError--AlreadyEnabled, EnableFailed, NotPermitted, SelfTestFailed, AccessDeniedError reporting
CryptoAlgorithm--12 variants (hashes, MACs, signatures, encryption)Algorithm identification

Structs

StructFieldsPurpose
ComplianceStatusprofile, profile_locked, fips_stateRuntime compliance snapshot

Concurrency and Safety

Memory Ordering

The module uses explicit atomic orderings throughout:

  • Ordering::Acquire on all loads: Ensures that reads of the atomic value see all writes that happened-before the corresponding Release store.

  • Ordering::Release on all stores: Ensures that the store is visible to subsequent Acquire loads on other cores.

  • Ordering::AcqRel on compare_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:

  1. Before acquiring the enable lock (fast-path rejection)
  2. 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 IDDescriptionImpact
R93-1Serialize FIPS enable attempts with FIPS_ENABLING spinlock and TOCTOU re-checkPrevents race conditions between concurrent enable_fips_mode() calls
R93-1Fail-closed on FIPS self-test failure in is_algorithm_permitted()FipsState::Failed blocks all algorithms instead of allowing them
R93-14Real 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-3Fail-closed on FIPS state corruption (_ => FipsState::Failed)Corrupted atomic values no longer fall back to Disabled
R94-4Fail-closed on profile corruption (_ => HardeningProfile::Secure)Corrupted atomic values no longer fall back to Balanced