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..]))
}