ajhahn.de
← FlashOS
Flash 198 lines
// hwrng: kernel entropy source for salt generation.
//
// Ships the FALLBACK path only: the generic-timer counter
// (CNTPCT_EL0, via get_sys_count) mixed through SplitMix64. This is
// deliberately WEAK — timer-derived bits are low-entropy at boot — and
// is acceptable only because the CI targets authenticate against fixed
// initramfs fixtures. The boot announce is loud about it so the weak
// path can never run silently.
//
// FIXME: the BCM2711 hardware RNG (the RNG200 block) driver is not here
// yet. QEMU's raspi4b machine does not back that MMIO region, and an EL1
// read of an unbacked device address raises a synchronous external abort
// (the kernel hangs in err_hang before reaching the shell), so a real-HW
// driver cannot be exercised by either CI target — there is no safe
// "probe by reading" for an absent block. The driver lands together with
// its on-bench hardware validation; from then on, falling back on real
// hardware becomes a hard failure instead of an announce-and-continue.
//
// Concurrency: single-core kernel; hwrng_init() runs once during
// bring-up before PID 1 exists, fill() is called from syscall context
// afterwards. No locking until the SMP pass (same posture as klog_ring).

// ---- Pure mixer (host-tested) ----

// SplitMix64 finalizer (Steele/Lea/Flood; Vigna's reference
// implementation). Avalanches a 64-bit input into a 64-bit output.
pub fn splitmix64(x u64) u64 {
    var z = x
    z = (z ^ (z >> 30)) *% 0xBF58476D1CE4E5B9
    z = (z ^ (z >> 27)) *% 0x94D049BB133111EB
    return z ^ (z >> 31)
}

// The SplitMix64 "golden gamma" increment.
const GAMMA u64 = 0x9E3779B97F4A7C15

// Deterministic core of the fallback generator: a SplitMix64 stream
// whose state additionally absorbs an entropy word on every draw. Pure
// (no externs) so host tests can drive it with known inputs; the
// gamma increment alone guarantees consecutive outputs differ even if
// the absorbed entropy word is stuck.
pub const Mixer = struct {
    state u64,

    pub fn init(seed u64) Mixer {
        return .{ .state = splitmix64(seed) }
    }

    pub fn next(self *mut Mixer, entropy u64) u64 {
        self.state +%= GAMMA
        self.state ^= entropy
        return splitmix64(self.state)
    }
}

// ---- Kernel glue (timer-backed, announce over the UART) ----

const MU i32 = 0

extern fn get_sys_count() u64
extern fn main_output(interface i32, str [*:0]u8) void
extern fn main_output_char(interface i32, ch u8) void

use console_ui

// console_ui Sink bound to the same Mini-UART boot console the kernel logs to
// (byte-at-a-time; see src/kernel.zig `bootSink` for the rationale).
fn bootSink(bytes []u8) void {
    for b in bytes { main_output_char(MU, b) }
}
const boot = console_ui.logger(&bootSink)

// Which entropy source produced the bytes. Only the weak fallback
// exists today; the hardware source joins with the RNG200 driver.
pub const Source = enum { fallback }

var mixer Mixer = .{ .state = 0 }

// Fill `buf` with generator output and report which source produced it.
// This is the salt-minting primitive for the authentication syscalls.
// Allocation-free: writes only into the caller's buffer.
pub fn fill(buf []mut u8) Source {
    var i usize = 0
    while (i < buf.len) {
        var word = mixer.next(get_sys_count())
        var k usize = 0
        while (k < 8 && i < buf.len) {
            buf[i] = #truncate(word)
            word >>= 8
            i += 1
            k += 1
        }
    }
    return .fallback
}

// Boot-time init: seed the mixer, self-test, announce the active source.
// Called once from kernel_main after the Mini-UART is up and before
// PID 1 is created, so the announce line sits in the kernel log ring by
// the time the EL0 harness scenario snapshots it. Allocates nothing —
// the free-page baseline emitted right after is unaffected.
export fn hwrng_init() void {
    mixer = Mixer.init(get_sys_count())

    // Self-test: two draws must differ. A stuck counter or a mixer
    // regression would mint the same salt for every credential — catch
    // that loudly at boot rather than silently weakening every hash.
    var a [16]u8 = undefined
    var b [16]u8 = undefined
    _ = fill(a[0..])
    _ = fill(b[0..])
    var same = true
    var i usize = 0
    while (i < 16) {
        if (a[i] != b[i]) { same = false }
        i += 1
    }
    if (same) {
        boot.warn("hwrng: self-test failed (constant output)")
        return
    }
    boot.ok("Initialized hwrng")
}

// ---- Host tests ----

const std = #import("std")
const testing = std.testing

test "SplitMix64 reference sequence from seed 0" {
    // First outputs of the SplitMix64 reference generator seeded with 0.
    // A transcription error in the multiplier constants or shift amounts
    // changes every value.
    var state u64 = 0
    const expected = [_]u64{ 0xE220A8397B1DCDAF, 0x6E789E6AA1B965F4, 0x06C45D188009454F }
    for want in expected {
        state +%= GAMMA
        try testing.expectEqual(want, splitmix64(state))
    }
}

test "differential: splitmix64 matches std.Random.SplitMix64" {
    var theirs = std.Random.SplitMix64.init(0)
    var state u64 = 0
    var i usize = 0
    while (i < 1000) {
        state +%= GAMMA
        try testing.expectEqual(theirs.next(), splitmix64(state))
        i += 1
    }
}

test "Mixer: outputs differ even with a stuck entropy input" {
    // The property the boot self-test relies on: even if CNTPCT were
    // stuck, the gamma increment alone changes every draw.
    var m = Mixer.init(0)
    const first = m.next(0xDEADBEEF)
    const second = m.next(0xDEADBEEF)
    const third = m.next(0xDEADBEEF)
    try testing.expect(first != second)
    try testing.expect(second != third)
    try testing.expect(first != third)
}

test "Mixer: same seed and entropy sequence reproduces the stream" {
    var m1 = Mixer.init(42)
    var m2 = Mixer.init(42)
    var i u64 = 0
    while (i < 100) {
        try testing.expectEqual(m1.next(i *% 7919), m2.next(i *% 7919))
        i += 1
    }
}

test "Mixer: different seeds diverge" {
    var m1 = Mixer.init(1)
    var m2 = Mixer.init(2)
    var collisions u32 = 0
    var i usize = 0
    while (i < 64) {
        if (m1.next(0) == m2.next(0)) { collisions += 1 }
        i += 1
    }
    try testing.expectEqual(#as(u32, 0), collisions)
}

test "fill + hwrng_init: end-to-end with the stubbed counter" {
    // tests/host_stubs.zig provides a ramping get_sys_count and a no-op
    // main_output, so the real boot path (seed → self-test → announce)
    // runs here exactly as in the kernel.
    hwrng_init()
    var a [23]u8 = undefined // odd length: exercises the partial last word
    var b [23]u8 = undefined
    try testing.expectEqual(Source.fallback, fill(a[0..]))
    try testing.expectEqual(Source.fallback, fill(b[0..]))
    try testing.expect(!std.mem.eql(u8, a[0..], b[0..]))
}