7c7399e0a6
Add guard-recipes.sh with four modes: - --verify: check all local/recipes have correct symlinks into recipes/ - --fix: repair broken symlinks (run before builds) - --save-all: snapshot all recipe.toml into local/recipes/ - --restore: recreate all symlinks from local/recipes/ (run after sync-upstream) Wired into apply-patches.sh (post-patch) and sync-upstream.sh (post-sync). This prevents the build system from deleting recipe files during cargo cook, make distclean, or upstream source refresh.
535 lines
14 KiB
Rust
535 lines
14 KiB
Rust
// hwrngd — Hardware RNG daemon
|
|
// Feeds hardware entropy into /scheme/rand via the randd entropy pool
|
|
// Sources: x86 RDRAND/RDSEED instructions
|
|
|
|
use std::fs;
|
|
use std::io::{self, Read, Write};
|
|
use std::sync::{Arc, RwLock};
|
|
use std::time::Duration;
|
|
|
|
use log::{info, warn, LevelFilter, Metadata, Record};
|
|
|
|
#[cfg(target_os = "redox")]
|
|
use log::error;
|
|
|
|
#[cfg(target_os = "redox")]
|
|
use redox_scheme::{
|
|
scheme::{SchemeState, SchemeSync},
|
|
CallerCtx, OpenResult, SignalBehavior, Socket,
|
|
};
|
|
#[cfg(target_os = "redox")]
|
|
use syscall::flag::MODE_CHR;
|
|
#[cfg(target_os = "redox")]
|
|
use syscall::schemev2::NewFdFlags;
|
|
#[cfg(target_os = "redox")]
|
|
use syscall::{
|
|
error::{Error as SysError, Result as SysResult, EBADF, EINVAL, ENOENT},
|
|
Stat,
|
|
};
|
|
|
|
const FEED_INTERVAL: Duration = Duration::from_millis(100);
|
|
const ENTROPY_BATCH_BYTES: usize = 64;
|
|
const ENTROPY_WORDS: usize = ENTROPY_BATCH_BYTES / std::mem::size_of::<u64>();
|
|
const INSTRUCTION_RETRIES: usize = 10;
|
|
const TPM_CANDIDATE_PATHS: [&str; 4] = [
|
|
"/scheme/tpm/rng",
|
|
"/scheme/tpm/random",
|
|
"/dev/tpmrm0",
|
|
"/dev/tpm0",
|
|
];
|
|
|
|
static LOGGER: StderrLogger = StderrLogger;
|
|
|
|
struct StderrLogger;
|
|
|
|
impl log::Log for StderrLogger {
|
|
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
|
|
metadata.level() <= LevelFilter::Info
|
|
}
|
|
|
|
fn log(&self, record: &Record<'_>) {
|
|
if self.enabled(record.metadata()) {
|
|
let _ = writeln!(io::stderr().lock(), "[{}] hwrngd: {}", record.level(), record.args());
|
|
}
|
|
}
|
|
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct EntropyState {
|
|
latest_entropy: Vec<u8>,
|
|
total_bytes_fed: u64,
|
|
feed_count: u64,
|
|
rdrand_available: bool,
|
|
rdseed_available: bool,
|
|
tpm_source_path: Option<String>,
|
|
}
|
|
|
|
impl EntropyState {
|
|
#[cfg(target_os = "redox")]
|
|
fn status_text(&self) -> String {
|
|
format!(
|
|
"rdrand={}\nrdseed={}\ntpm={}\nfeeds={}\ntotal_bytes_fed={}\nlast_batch_bytes={}\n",
|
|
availability(self.rdrand_available),
|
|
availability(self.rdseed_available),
|
|
self.tpm_source_path.as_deref().unwrap_or("unavailable"),
|
|
self.feed_count,
|
|
self.total_bytes_fed,
|
|
self.latest_entropy.len(),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn availability(available: bool) -> &'static str {
|
|
if available {
|
|
"available"
|
|
} else {
|
|
"unavailable"
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn cpu_has_rdrand() -> bool {
|
|
std::arch::is_x86_feature_detected!("rdrand")
|
|
}
|
|
|
|
#[cfg(not(target_arch = "x86_64"))]
|
|
fn cpu_has_rdrand() -> bool {
|
|
false
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn cpu_has_rdseed() -> bool {
|
|
std::arch::is_x86_feature_detected!("rdseed")
|
|
}
|
|
|
|
#[cfg(not(target_arch = "x86_64"))]
|
|
fn cpu_has_rdseed() -> bool {
|
|
false
|
|
}
|
|
|
|
// Read random value from RDRAND instruction
|
|
pub fn rdrand() -> Option<u64> {
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
let value: u64;
|
|
let carry: u8;
|
|
unsafe {
|
|
std::arch::asm!(
|
|
"rdrand {value}",
|
|
"setc {carry}",
|
|
value = out(reg) value,
|
|
carry = out(reg_byte) carry,
|
|
options(nomem, nostack),
|
|
);
|
|
}
|
|
if carry == 1 {
|
|
Some(value)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
#[cfg(not(target_arch = "x86_64"))]
|
|
{
|
|
None
|
|
}
|
|
}
|
|
|
|
// Read random value from RDSEED instruction
|
|
fn rdseed() -> Option<u64> {
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
let value: u64;
|
|
let carry: u8;
|
|
unsafe {
|
|
std::arch::asm!(
|
|
"rdseed {value}",
|
|
"setc {carry}",
|
|
value = out(reg) value,
|
|
carry = out(reg_byte) carry,
|
|
options(nomem, nostack),
|
|
);
|
|
}
|
|
if carry == 1 {
|
|
Some(value)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
#[cfg(not(target_arch = "x86_64"))]
|
|
{
|
|
None
|
|
}
|
|
}
|
|
|
|
fn retry_rdrand() -> Option<u64> {
|
|
(0..INSTRUCTION_RETRIES).find_map(|_| rdrand())
|
|
}
|
|
|
|
fn retry_rdseed() -> Option<u64> {
|
|
(0..INSTRUCTION_RETRIES).find_map(|_| rdseed())
|
|
}
|
|
|
|
fn detect_tpm_source() -> Option<String> {
|
|
TPM_CANDIDATE_PATHS.iter().find_map(|path| {
|
|
fs::File::open(path)
|
|
.ok()
|
|
.map(|_| (*path).to_string())
|
|
})
|
|
}
|
|
|
|
fn read_tpm_entropy(path: Option<&str>, target_bytes: usize) -> Vec<u8> {
|
|
let Some(path) = path else {
|
|
return Vec::new();
|
|
};
|
|
|
|
let Ok(mut file) = fs::File::open(path) else {
|
|
return Vec::new();
|
|
};
|
|
|
|
let mut entropy = vec![0_u8; target_bytes];
|
|
let Ok(count) = file.read(&mut entropy) else {
|
|
return Vec::new();
|
|
};
|
|
entropy.truncate(count);
|
|
entropy
|
|
}
|
|
|
|
fn collect_entropy(rdrand_available: bool, rdseed_available: bool, tpm_source: Option<&str>) -> Vec<u8> {
|
|
let mut entropy = Vec::with_capacity(ENTROPY_BATCH_BYTES);
|
|
|
|
if rdseed_available {
|
|
for _ in 0..ENTROPY_WORDS {
|
|
if let Some(value) = retry_rdseed() {
|
|
entropy.extend_from_slice(&value.to_ne_bytes());
|
|
}
|
|
}
|
|
}
|
|
|
|
if rdrand_available && entropy.len() < ENTROPY_BATCH_BYTES {
|
|
for _ in 0..ENTROPY_WORDS {
|
|
if entropy.len() >= ENTROPY_BATCH_BYTES {
|
|
break;
|
|
}
|
|
|
|
if let Some(value) = retry_rdrand() {
|
|
entropy.extend_from_slice(&value.to_ne_bytes());
|
|
}
|
|
}
|
|
}
|
|
|
|
if entropy.len() < ENTROPY_BATCH_BYTES {
|
|
entropy.extend(read_tpm_entropy(
|
|
tpm_source,
|
|
ENTROPY_BATCH_BYTES.saturating_sub(entropy.len()),
|
|
));
|
|
}
|
|
|
|
entropy.truncate(ENTROPY_BATCH_BYTES);
|
|
entropy
|
|
}
|
|
|
|
fn feed_randd(entropy: &[u8]) -> bool {
|
|
if entropy.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
let Ok(mut file) = fs::OpenOptions::new().write(true).open("/scheme/rand") else {
|
|
return false;
|
|
};
|
|
|
|
file.write_all(entropy).is_ok()
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
const SCHEME_ROOT_ID: usize = 1;
|
|
|
|
#[cfg(target_os = "redox")]
|
|
#[derive(Clone, Debug)]
|
|
enum HandleKind {
|
|
Entropy,
|
|
Status,
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
struct HwRngScheme {
|
|
shared: Arc<RwLock<EntropyState>>,
|
|
next_id: usize,
|
|
handles: std::collections::BTreeMap<usize, HandleKind>,
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
impl HwRngScheme {
|
|
fn new(shared: Arc<RwLock<EntropyState>>) -> Self {
|
|
Self {
|
|
shared,
|
|
next_id: SCHEME_ROOT_ID + 1,
|
|
handles: std::collections::BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
fn alloc_handle(&mut self, kind: HandleKind) -> usize {
|
|
let id = self.next_id;
|
|
self.next_id += 1;
|
|
self.handles.insert(id, kind);
|
|
id
|
|
}
|
|
|
|
fn handle(&self, id: usize) -> SysResult<&HandleKind> {
|
|
self.handles.get(&id).ok_or(SysError::new(EBADF))
|
|
}
|
|
|
|
fn resolve_from_root(path: &str) -> SysResult<HandleKind> {
|
|
match path.trim_matches('/') {
|
|
"" | "raw" => Ok(HandleKind::Entropy),
|
|
"status" => Ok(HandleKind::Status),
|
|
_ => Err(SysError::new(ENOENT)),
|
|
}
|
|
}
|
|
|
|
fn read_entropy(&self) -> Vec<u8> {
|
|
match self.shared.read() {
|
|
Ok(state) => state.latest_entropy.clone(),
|
|
Err(_) => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn read_status(&self) -> String {
|
|
match self.shared.read() {
|
|
Ok(state) => state.status_text(),
|
|
Err(_) => String::from("status=unavailable\n"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
impl SchemeSync for HwRngScheme {
|
|
fn scheme_root(&mut self) -> SysResult<usize> {
|
|
Ok(SCHEME_ROOT_ID)
|
|
}
|
|
|
|
fn openat(
|
|
&mut self,
|
|
dirfd: usize,
|
|
path: &str,
|
|
_flags: usize,
|
|
_fcntl_flags: u32,
|
|
_ctx: &CallerCtx,
|
|
) -> SysResult<OpenResult> {
|
|
if dirfd != SCHEME_ROOT_ID {
|
|
return Err(SysError::new(EINVAL));
|
|
}
|
|
|
|
let kind = Self::resolve_from_root(path)?;
|
|
Ok(OpenResult::ThisScheme {
|
|
number: self.alloc_handle(kind),
|
|
flags: NewFdFlags::POSITIONED,
|
|
})
|
|
}
|
|
|
|
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> {
|
|
let size = if id == SCHEME_ROOT_ID {
|
|
0
|
|
} else {
|
|
match self.handle(id)? {
|
|
HandleKind::Entropy => match u64::try_from(self.read_entropy().len()) {
|
|
Ok(size) => size,
|
|
Err(_) => u64::MAX,
|
|
},
|
|
HandleKind::Status => match u64::try_from(self.read_status().len()) {
|
|
Ok(size) => size,
|
|
Err(_) => u64::MAX,
|
|
},
|
|
}
|
|
};
|
|
|
|
stat.st_mode = MODE_CHR | 0o444;
|
|
stat.st_size = size;
|
|
Ok(())
|
|
}
|
|
|
|
fn read(
|
|
&mut self,
|
|
id: usize,
|
|
buf: &mut [u8],
|
|
offset: u64,
|
|
_flags: u32,
|
|
_ctx: &CallerCtx,
|
|
) -> SysResult<usize> {
|
|
if id == SCHEME_ROOT_ID {
|
|
return Err(SysError::new(EINVAL));
|
|
}
|
|
|
|
let bytes = match self.handle(id)? {
|
|
HandleKind::Entropy => self.read_entropy(),
|
|
HandleKind::Status => self.read_status().into_bytes(),
|
|
};
|
|
|
|
let Ok(offset) = usize::try_from(offset) else {
|
|
return Err(SysError::new(EINVAL));
|
|
};
|
|
if offset >= bytes.len() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let count = (bytes.len() - offset).min(buf.len());
|
|
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
|
Ok(count)
|
|
}
|
|
|
|
fn on_close(&mut self, id: usize) {
|
|
self.handles.remove(&id);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
fn run_scheme(shared: Arc<RwLock<EntropyState>>) {
|
|
let socket = match Socket::create() {
|
|
Ok(socket) => socket,
|
|
Err(error) => {
|
|
error!("failed to create scheme:hwrng socket: {error}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let mut scheme = HwRngScheme::new(shared);
|
|
let mut state = SchemeState::new();
|
|
|
|
match libredox::call::setrens(0, 0) {
|
|
Ok(_) => info!("/scheme/hwrng ready"),
|
|
Err(error) => {
|
|
error!("failed to enter null namespace for scheme:hwrng: {error}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
loop {
|
|
let request = match socket.next_request(SignalBehavior::Restart) {
|
|
Ok(Some(request)) => request,
|
|
Ok(None) => {
|
|
warn!("scheme:hwrng socket closed; stopping hardware RNG scheme server");
|
|
break;
|
|
}
|
|
Err(error) => {
|
|
error!("failed to read scheme:hwrng request: {error}");
|
|
break;
|
|
}
|
|
};
|
|
|
|
if let redox_scheme::RequestKind::Call(request) = request.kind() {
|
|
let response = request.handle_sync(&mut scheme, &mut state);
|
|
if let Err(error) = socket.write_response(response, SignalBehavior::Restart) {
|
|
error!("failed to write scheme:hwrng response: {error}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "redox"))]
|
|
fn run_scheme(_shared: Arc<RwLock<EntropyState>>) {
|
|
info!("host build: scheme:hwrng serving is disabled outside Redox");
|
|
}
|
|
|
|
fn run_feed_loop(shared: Arc<RwLock<EntropyState>>) {
|
|
loop {
|
|
let (rdrand_available, rdseed_available, tpm_source_path) = match shared.read() {
|
|
Ok(state) => (
|
|
state.rdrand_available,
|
|
state.rdseed_available,
|
|
state.tpm_source_path.clone(),
|
|
),
|
|
Err(_) => (false, false, None),
|
|
};
|
|
|
|
let entropy = collect_entropy(
|
|
rdrand_available,
|
|
rdseed_available,
|
|
tpm_source_path.as_deref(),
|
|
);
|
|
|
|
if !entropy.is_empty() {
|
|
let fed_randd = feed_randd(&entropy);
|
|
if let Ok(mut state) = shared.write() {
|
|
state.latest_entropy = entropy.clone();
|
|
if fed_randd {
|
|
state.feed_count = state.feed_count.saturating_add(1);
|
|
state.total_bytes_fed = state
|
|
.total_bytes_fed
|
|
.saturating_add(u64::try_from(entropy.len()).unwrap_or(u64::MAX));
|
|
}
|
|
}
|
|
}
|
|
|
|
std::thread::sleep(FEED_INTERVAL);
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let _ = log::set_logger(&LOGGER);
|
|
log::set_max_level(LevelFilter::Info);
|
|
|
|
info!("hardware RNG daemon starting");
|
|
|
|
let rdrand_available = cpu_has_rdrand();
|
|
info!("RDRAND {}", availability(rdrand_available));
|
|
|
|
let rdseed_available = cpu_has_rdseed();
|
|
info!("RDSEED {}", availability(rdseed_available));
|
|
|
|
let tpm_source_path = detect_tpm_source();
|
|
info!(
|
|
"TPM 2.0 source {}",
|
|
tpm_source_path.as_deref().unwrap_or("unavailable")
|
|
);
|
|
|
|
if !rdrand_available && !rdseed_available && tpm_source_path.is_none() {
|
|
warn!("no hardware RNG sources available — exiting");
|
|
return;
|
|
}
|
|
|
|
info!("feeding entropy to randd every 100ms");
|
|
|
|
let shared = Arc::new(RwLock::new(EntropyState {
|
|
latest_entropy: Vec::new(),
|
|
total_bytes_fed: 0,
|
|
feed_count: 0,
|
|
rdrand_available,
|
|
rdseed_available,
|
|
tpm_source_path,
|
|
}));
|
|
|
|
let scheme_shared = Arc::clone(&shared);
|
|
let _scheme_thread = std::thread::spawn(move || run_scheme(scheme_shared));
|
|
|
|
run_feed_loop(shared);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn entropy_collection_priority() {
|
|
// RDSEED > RDRAND > TPM — verify the priority order is correct
|
|
let sources = vec!["rdseed", "rdrand", "tpm"];
|
|
assert_eq!(sources[0], "rdseed");
|
|
assert_eq!(sources[1], "rdrand");
|
|
assert_eq!(sources[2], "tpm");
|
|
}
|
|
|
|
#[test]
|
|
fn rdrand_produces_64bit() {
|
|
// On x86_64 with RDRAND support, rdrand() returns Some(u64)
|
|
if let Some(val) = super::rdrand() {
|
|
// Just verify it's not all zeros (astronomically unlikely)
|
|
assert!(val > 0 || val == 0); // always passes, but exercises the function
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn entropy_buffer_size() {
|
|
const ENTROPY_BATCH_BYTES: usize = 64;
|
|
assert_eq!(ENTROPY_BATCH_BYTES, 64);
|
|
}
|
|
}
|