feat(boot): gatekeeper gates boot (init→UI→gatekeeper→data); keystore disk + virtio-rng discovery; terminal READ_SECRET; storage SET_KEY; PageCount ≤8192, alloc cap gone; device window 0x01D0_0000; test-qemu types passcode over 2 boots
This commit is contained in:
@@ -6,3 +6,14 @@ edition = "2024"
|
||||
[dependencies]
|
||||
keystore = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
types = { workspace = true }
|
||||
libkos = { workspace = true }
|
||||
port-block-device = { workspace = true }
|
||||
port-entropy = { workspace = true }
|
||||
driver-block-virtio = { workspace = true }
|
||||
driver-rng-virtio = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "gatekeeper"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
55
crates/servers/gatekeeper/src/devices.rs
Normal file
55
crates/servers/gatekeeper/src/devices.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! The flow's ports on real devices: the keystore disk, virtio-rng, and
|
||||
//! the terminal.
|
||||
|
||||
use libkos::io::print_to;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
use port_entropy::Entropy;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
use crate::flow::{Keystore, MAX_SECRET, Prompt, Secret};
|
||||
use keystore::RECORD_SIZE;
|
||||
|
||||
/// The record's two copies are its first two sectors.
|
||||
pub struct Disk<D: BlockDevice>(pub D);
|
||||
|
||||
impl<D: BlockDevice> Keystore for Disk<D> {
|
||||
fn read(&mut self, copy: usize, out: &mut [u8; RECORD_SIZE]) -> bool {
|
||||
self.0.sector_size() == RECORD_SIZE && self.0.read_sector(SectorId::new(copy as u32), out)
|
||||
}
|
||||
|
||||
fn write(&mut self, copy: usize, data: &[u8; RECORD_SIZE]) -> bool {
|
||||
self.0.sector_size() == RECORD_SIZE && self.0.write_sector(SectorId::new(copy as u32), data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hardware<E: Entropy>(pub E);
|
||||
|
||||
impl<E: Entropy> keystore::Random for Hardware<E> {
|
||||
fn fill(&mut self, buf: &mut [u8]) -> bool {
|
||||
self.0.fill(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Terminal(pub CapSlot);
|
||||
|
||||
impl Prompt for Terminal {
|
||||
fn say(&mut self, text: &str) {
|
||||
print_to(self.0, format_args!("{}", text));
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
libkos::terminal::clear(self.0);
|
||||
}
|
||||
|
||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||
print_to(self.0, format_args!("{}", question));
|
||||
let mut line = Secret::new();
|
||||
let mut buf = [0u8; MAX_SECRET + 1];
|
||||
let n = libkos::terminal::read_secret(self.0, &mut buf);
|
||||
let typed = buf[..n].strip_suffix(b"\n").unwrap_or(&buf[..n]);
|
||||
line.set(typed);
|
||||
buf.fill(0);
|
||||
out.set(line.as_bytes());
|
||||
n > 0
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,8 @@ impl Drop for Secret {
|
||||
/// The owner, on the screen and keyboard.
|
||||
pub trait Prompt {
|
||||
fn say(&mut self, text: &str);
|
||||
/// Blanks the screen.
|
||||
fn clear(&mut self);
|
||||
/// Shows `question` and reads a passcode without showing it; false if
|
||||
/// nothing could be read.
|
||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool;
|
||||
@@ -128,6 +130,8 @@ fn choose(prompt: &mut impl Prompt, question: &str, optional: bool, out: &mut Se
|
||||
/// A device never set up (or wiped): the owner chooses a passcode, and
|
||||
/// may choose a duress one. Returns the new master key.
|
||||
fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
|
||||
// After a wipe nothing earlier may show: it must look like a new device.
|
||||
prompt.clear();
|
||||
prompt.say("This device has no passcode yet. Choose one; it protects everything on it.\n");
|
||||
let mut real = Secret::new();
|
||||
choose(prompt, "New passcode: ", false, &mut real)?;
|
||||
|
||||
@@ -7,8 +7,78 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod devices;
|
||||
pub mod flow;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/mod.rs"]
|
||||
mod tests;
|
||||
|
||||
use alloc::vec;
|
||||
use keystore::{Block, KdfParams};
|
||||
use libkos::syscall::{self, Received};
|
||||
use types::bounded::CapSlot;
|
||||
use types::gatekeeper::{KEY, UNLOCK, reply};
|
||||
|
||||
/// Where init's manifest put them: its own endpoint (devices arrive on
|
||||
/// it, then init's calls), then the terminal.
|
||||
const EP: CapSlot = CapSlot::const_new(0);
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(1);
|
||||
|
||||
/// Answers init's next call, if it is `op`, with `words`.
|
||||
fn answer(op: usize, words: [usize; 4]) -> bool {
|
||||
loop {
|
||||
match syscall::receive_msg(EP) {
|
||||
Ok(Received::Call { badge: 0, words: [w, ..], .. }) if w == op => {
|
||||
return syscall::reply(words, None).is_ok();
|
||||
}
|
||||
// Anyone else, or anything else: refused.
|
||||
Ok(Received::Call { .. }) => {
|
||||
let _ = syscall::reply([reply::FAILED, 0, 0, 0], None);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> ! {
|
||||
// Init hands over the keystore disk, then the randomness device.
|
||||
let keystore = libkos::device::receive_device(EP).mmio.and_then(driver_block_virtio::VirtioBlockDevice::new);
|
||||
let random = libkos::device::receive_device(EP).mmio.and_then(driver_rng_virtio::VirtioRng::new);
|
||||
let (Some(keystore), Some(random)) = (keystore, random) else {
|
||||
libkos::println!("gatekeeper: no keystore or no randomness, nothing to unlock with");
|
||||
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
|
||||
syscall::exit();
|
||||
};
|
||||
let mut memory = vec![Block::default(); KdfParams::DEFAULT.memory_kib as usize];
|
||||
libkos::println!("gatekeeper: waiting for the owner");
|
||||
let key = flow::run(
|
||||
&mut devices::Disk(keystore),
|
||||
&mut devices::Hardware(random),
|
||||
&mut devices::Terminal(TERMINAL),
|
||||
KdfParams::DEFAULT,
|
||||
&mut memory,
|
||||
);
|
||||
// The guesses' working memory goes before anything else happens.
|
||||
for b in memory.iter_mut() {
|
||||
*b = Block::default();
|
||||
}
|
||||
drop(memory);
|
||||
match key {
|
||||
Ok(key) => {
|
||||
libkos::println!("gatekeeper: unlocked");
|
||||
libkos::terminal::clear(TERMINAL);
|
||||
let word = |i: usize| usize::from_le_bytes(key.0[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8]));
|
||||
if answer(UNLOCK, [reply::OK, 0, 0, 0]) {
|
||||
let _ = answer(KEY, [word(0), word(1), word(2), word(3)]);
|
||||
}
|
||||
}
|
||||
Err(why) => {
|
||||
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
||||
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
// Its capabilities go with it.
|
||||
syscall::exit()
|
||||
}
|
||||
|
||||
11
crates/servers/gatekeeper/src/main.rs
Normal file
11
crates/servers/gatekeeper/src/main.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![no_main]
|
||||
|
||||
extern crate libkos;
|
||||
|
||||
libkos::entry!(main);
|
||||
|
||||
fn main() {
|
||||
gatekeeper::run();
|
||||
}
|
||||
@@ -67,18 +67,26 @@ impl Random for Counter {
|
||||
/// An owner typing from a script; what was shown is kept.
|
||||
struct Owner {
|
||||
typing: VecDeque<&'static str>,
|
||||
/// On the screen now.
|
||||
shown: String,
|
||||
/// Ever shown, cleared or not.
|
||||
said: String,
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
fn types(lines: &[&'static str]) -> Self {
|
||||
Self { typing: lines.iter().copied().collect(), shown: String::new() }
|
||||
Self { typing: lines.iter().copied().collect(), shown: String::new(), said: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Prompt for Owner {
|
||||
fn say(&mut self, text: &str) {
|
||||
self.shown.push_str(text);
|
||||
self.said.push_str(text);
|
||||
}
|
||||
/// What a fresh screen shows: nothing that came before.
|
||||
fn clear(&mut self) {
|
||||
self.shown.clear();
|
||||
}
|
||||
fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
|
||||
self.shown.push_str(question);
|
||||
@@ -158,7 +166,7 @@ fn too_many_wrong_passcodes_wipe_the_keys_and_the_device_starts_fresh() {
|
||||
guesses.extend(["5678", "5678", "", ""]);
|
||||
let mut owner = Owner::types(&guesses);
|
||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
||||
assert!(owner.shown.contains("One try left"));
|
||||
assert!(owner.said.contains("One try left"));
|
||||
assert!(owner.shown.ends_with("This device has no passcode yet. Choose one; it protects everything on it.\nNew passcode: Again: A duress passcode wipes the device when it is entered instead. Leave it empty for none.\nDuress passcode: Again: "));
|
||||
assert_ne!(new.0, key, "the old master key is gone");
|
||||
let mut old = Owner::types(&["1234"]);
|
||||
@@ -173,6 +181,7 @@ fn the_duress_passcode_wipes_silently_and_the_device_looks_fresh() {
|
||||
let mut owner = Owner::types(&["0000", "4321", "4321", "", ""]);
|
||||
let new = run_with(&mut dev, &mut owner).unwrap();
|
||||
assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode");
|
||||
assert!(owner.shown.starts_with("This device has no passcode yet"), "nor is the passcode prompt left showing");
|
||||
assert!(owner.shown.contains("no passcode yet"));
|
||||
assert_ne!(new.0, key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user