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:
2026-10-27 23:43:47 +01:00
parent b7ef67ffd8
commit 3b7d004285
36 changed files with 455 additions and 74 deletions

View File

@@ -57,6 +57,8 @@ fn run_hello(fs: &mut Storage, died: CapSlot, grant: Option<CapSlot>) -> Result<
/// A file written through storage, which the host then checks with e2fsck.
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> {
// Left by an earlier boot of the same disk.
let _ = fs.delete(b"selftest.txt");
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
if fs.write(h, NOTE) != Ok(NOTE.len()) {
return Err("short write");

View File

@@ -8,9 +8,11 @@ use port_block_device::{BlockDevice, SectorId};
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000);
const REQ_HDR_VA: VirtualAddress = VirtualAddress::from_raw(0x23000);
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_0000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_1000);
const REQ_HDR_VA: VirtualAddress = VirtualAddress::from_raw(0x01D0_3000);
const SECTOR_SIZE: usize = 512;
pub struct VirtioBlockDevice {

View File

@@ -10,9 +10,11 @@ use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
use types::display::{Color, Dimension, ScreenCoord};
const GPU_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x20000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x21000);
const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x23000);
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const GPU_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_0000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_1000);
const CMDBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D2_3000);
const FB_VA: VirtualAddress = VirtualAddress::from_raw(0x100000);
const FB_BPP: usize = 4;
/// Pixels converted and stored per run.

View File

@@ -5,7 +5,9 @@ use libkos::Window;
use port_input_device::{InputDevice, InputEvent, Key};
use types::address::{ByteOffset, VirtualAddress};
const UART_VA: VirtualAddress = VirtualAddress::from_raw(0x20000);
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const UART_VA: VirtualAddress = VirtualAddress::from_raw(0x01D4_0000);
const UART_RBR: ByteOffset = ByteOffset::new(0);
const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1);
const UART_LSR_OFFSET: ByteOffset = ByteOffset::new(5);

View File

@@ -10,9 +10,11 @@ use port_input_device::{InputDevice, InputEvent};
use types::address::{ByteOffset, PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x25000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x26000);
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x28000);
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_0000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_1000);
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x01D1_3000);
const QUEUE_SIZE: usize = 32;
const EVENT_SIZE: usize = 8;

View File

@@ -10,9 +10,11 @@ use port_entropy::Entropy;
use types::address::{ByteOffset, VirtualAddress};
use types::bounded::PageCount;
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x2A000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x2B000);
const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x2D000);
/// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
/// each), clear of program images and heaps.
const MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_0000);
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_1000);
const BUFFER_VA: VirtualAddress = VirtualAddress::from_raw(0x01D3_3000);
/// Asked for at most this much at a time.
const CHUNK: usize = 256;

View File

@@ -1,8 +1,9 @@
//! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard
//! (or the UART when there is none).
use hw_virtio::block::CONFIG_CAPACITY;
use hw_virtio::mmio::{
DEVICE_ID_BLOCK, DEVICE_ID_GPU, DEVICE_ID_INPUT, MAGIC, REG_DEVICE_ID, REG_MAGIC,
DEVICE_ID_BLOCK, DEVICE_ID_GPU, DEVICE_ID_INPUT, DEVICE_ID_RNG, MAGIC, REG_DEVICE_ID, REG_MAGIC,
};
use libkos::device::{self, Device};
use libkos::syscall;
@@ -19,8 +20,14 @@ pub struct Devices {
pub disk: Device,
pub gpu: Device,
pub input: Device,
/// The gatekeeper's: a small disk of its own for the key slots.
pub keystore: Device,
pub random: Device,
}
/// A block device this small (1 MiB) is the keystore, not the disk.
const KEYSTORE_MAX_SECTORS: u64 = 2048;
fn slot(raw: u64) -> Option<CapSlot> {
if raw == NO_SLOT {
return None;
@@ -28,21 +35,30 @@ fn slot(raw: u64) -> Option<CapSlot> {
CapSlot::new(u8::try_from(raw).ok()?)
}
/// The virtio device id behind `region`, 0 for an empty slot. Read through
/// a copy of the capability, mapped only while looking and then dropped,
/// which unmaps it.
fn virtio_id(region: CapSlot) -> u32 {
/// The virtio device id behind `region` (0 for an empty slot) and, for a
/// block device, its size in sectors. Read through a copy of the
/// capability, mapped only while looking and then dropped, which unmaps it.
fn virtio_id(region: CapSlot) -> (u32, u64) {
let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write);
let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else {
return 0;
return (0, 0);
};
let id = match device::map_mmio(copy, PROBE_VA) {
// The registers stay mapped until the drop below.
Ok(regs) => match regs.read::<u32>(REG_MAGIC) == MAGIC {
true => regs.read(REG_DEVICE_ID),
false => 0,
true => {
let id = regs.read(REG_DEVICE_ID);
let size = match id {
DEVICE_ID_BLOCK => {
regs.read::<u32>(CONFIG_CAPACITY) as u64 | (regs.read::<u32>(CONFIG_CAPACITY + 4) as u64) << 32
}
_ => 0,
};
(id, size)
}
false => (0, 0),
},
Err(_) => 0,
Err(_) => (0, 0),
};
syscall::drop_cap(copy);
id
@@ -74,7 +90,13 @@ pub fn console() -> Option<CapSlot> {
/// Sorts the board's devices out.
pub fn discover() -> Devices {
let info = boot_info();
let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE };
let mut found = Devices {
disk: Device::NONE,
gpu: Device::NONE,
input: Device::NONE,
keystore: Device::NONE,
random: Device::NONE,
};
let mut uart = Device::NONE;
for d in info.devices() {
let Some(region) = slot(d.region_slot) else {
@@ -84,9 +106,11 @@ pub fn discover() -> Devices {
let place = match d.kind {
KIND_UART_16550 => &mut uart,
KIND_VIRTIO => match virtio_id(region) {
DEVICE_ID_BLOCK => &mut found.disk,
DEVICE_ID_GPU => &mut found.gpu,
DEVICE_ID_INPUT => &mut found.input,
(DEVICE_ID_BLOCK, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore,
(DEVICE_ID_BLOCK, _) => &mut found.disk,
(DEVICE_ID_GPU, _) => &mut found.gpu,
(DEVICE_ID_INPUT, _) => &mut found.input,
(DEVICE_ID_RNG, _) => &mut found.random,
_ => continue,
},
_ => continue,

View File

@@ -110,8 +110,12 @@ impl<'a> Launcher<'a> {
if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
*free = Some((p.name, ep));
}
if let Some(device) = p.device {
note = self.give(device, ep);
for device in p.devices {
note = self.give(*device, ep);
}
if p.unlocks {
unlock(ep)?;
note = ", the owner is in";
}
if console.is_some() {
hand_over_console(ep);
@@ -139,6 +143,8 @@ impl<'a> Launcher<'a> {
let (dev, given, none): (BoardDevice, _, _) = match device {
Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"),
Device::Input => (self.devices.input, ", input device handed over", ", no input device"),
Device::Keystore => (self.devices.keystore, ", keystore handed over", ", no keystore"),
Device::Random => (self.devices.random, ", randomness handed over", ", no randomness"),
};
match libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some() {
true => given,
@@ -147,6 +153,23 @@ impl<'a> Launcher<'a> {
}
}
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
/// passes the master key on to storage, which alone keeps it.
fn unlock(ep: CapSlot) -> Result<(), &'static str> {
use types::gatekeeper::{KEY, UNLOCK, reply};
match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
Ok([reply::OK, ..]) => {}
_ => return Err("the gatekeeper could not unlock"),
}
let mut key = syscall::request(ep, [KEY, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
let given = [0, 1].iter().all(|&half| {
let words = [types::storage::op::SET_KEY, half, key[half * 2], key[half * 2 + 1]];
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
});
key.fill(0);
if given { Ok(()) } else { Err("storage would not take the key") }
}
/// From here on init prints through the log server, like everyone else,
/// and holds no console of its own.
fn hand_over_console(log: CapSlot) {

View File

@@ -80,6 +80,11 @@ fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {
libkos::print!("init: loading {}... ", p.name);
match launcher.launch(p) {
Ok(note) => libkos::println!("ok{}", note),
// Without the owner, nothing that touches user data may start.
Err(why) if p.unlocks => {
libkos::println!("STOP ({})", why);
idle();
}
Err(why) => libkos::println!("SKIP ({})", why),
}
}

View File

@@ -7,6 +7,8 @@ use types::budget::Limits;
pub enum Device {
Gpu,
Input,
Keystore,
Random,
}
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may
@@ -23,7 +25,11 @@ pub struct Program {
pub limits: Limits,
/// Serves an endpoint, which it finds in slot 0.
pub serves: bool,
pub device: Option<Device>,
/// Handed over in this order, after it starts.
pub devices: &'static [Device],
/// Authenticates the owner: init waits for the master key before
/// starting anything that touches user data.
pub unlocks: bool,
/// Earlier programs' endpoints it may call, in its next slots; each
/// program's copy carries its own badge, so a server can tell them apart.
pub uses: &'static [&'static str],
@@ -51,7 +57,8 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
file,
limits,
serves: true,
device: None,
devices: &[],
unlocks: false,
uses: &[],
storage: None,
console: false,
@@ -63,16 +70,26 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
/// Started on every boot. The log server comes first so the rest can print.
pub const SERVERS: &[Program] = &[
Program { console: true, ..server("log", b"bin/log", SMALL) },
Program { device: Some(Device::Input), ..server("input", b"bin/input", Limits { regions: 4, ..SMALL }) },
Program { devices: &[Device::Input], ..server("input", b"bin/input", Limits { regions: 4, ..SMALL }) },
// The input server's only client: it passes input on to the app in focus.
Program {
device: Some(Device::Gpu),
devices: &[Device::Gpu],
uses: &["input"],
// The framebuffer alone is ~1000 pages at 1280x800.
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
},
// Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
// Nothing that touches user data starts before the owner is in. It
// holds the screen (through the terminal), its keystore and randomness,
// and exits once it has handed over the key.
Program {
uses: &["terminal"],
devices: &[Device::Keystore, Device::Random],
unlocks: true,
// argon2's 16 MiB, its image and a little more.
..server("gatekeeper", b"bin/gatekeeper", Limits { pages: 4096 + 160, regions: 6, ..SMALL })
},
// The only one that equips apps (docs/apps.md): it reaches every app's
// files and the owner's decisions, and runs apps through init.
Program {

View File

@@ -70,7 +70,11 @@ fn out_of_range_arguments_do_not_decode() {
decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(),
"zero pages"
);
assert!(decode(SYS_REQUEST_PAGES, &[1025, 0, 0, 0, 0, 0]).is_none());
let too_many = types::bounded::PageCount::MAX as usize + 1;
assert!(
decode(SYS_REQUEST_PAGES, &[too_many, 0, 0, 0, 0, 0]).is_none(),
"more pages than one request may ask for"
);
assert!(
decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(),
"bad perms"

View File

@@ -3,7 +3,6 @@ use core::cell::UnsafeCell;
use core::ptr;
const PAGE_SIZE: usize = 4096;
const MAX_PAGES: usize = 64;
struct FreeBlock {
size: usize,
@@ -14,10 +13,10 @@ pub struct KosAllocator {
inner: UnsafeCell<AllocatorInner>,
}
/// How much heap a process gets is its page budget's business (the kernel
/// enforces it), not the allocator's.
struct AllocatorInner {
free_list: *mut FreeBlock,
pages: [usize; MAX_PAGES],
page_count: usize,
}
unsafe impl Sync for KosAllocator {}
@@ -25,11 +24,7 @@ unsafe impl Sync for KosAllocator {}
impl KosAllocator {
pub const fn new() -> Self {
Self {
inner: UnsafeCell::new(AllocatorInner {
free_list: ptr::null_mut(),
pages: [0; MAX_PAGES],
page_count: 0,
}),
inner: UnsafeCell::new(AllocatorInner { free_list: ptr::null_mut() }),
}
}
@@ -41,19 +36,12 @@ impl KosAllocator {
impl AllocatorInner {
/// `n` pages in one piece, so an allocation larger than a page fits.
fn request_pages(&mut self, n: usize) -> *mut u8 {
if self.page_count + n > MAX_PAGES {
return ptr::null_mut();
}
let Some(count) = u16::try_from(n).ok().and_then(types::bounded::PageCount::new) else {
return ptr::null_mut();
};
let Some(va) = crate::syscall::request_pages(count) else {
return ptr::null_mut();
};
if let Some(slot) = self.pages.get_mut(self.page_count) {
*slot = va.raw();
}
self.page_count += n;
va.raw() as *mut u8
}

View File

@@ -6,9 +6,18 @@ use types::bounded::CapSlot;
/// Reads a typed line into `buf`, newline included if it fits; how much.
pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize {
read(terminal, types::terminal::READ, buf)
}
/// Reads a typed line without showing it (`*` for each character).
pub fn read_secret(terminal: CapSlot, buf: &mut [u8]) -> usize {
read(terminal, types::terminal::READ_SECRET, buf)
}
fn read(terminal: CapSlot, op: usize, buf: &mut [u8]) -> usize {
let mut n = 0;
while n < buf.len() {
let Ok(words) = syscall::request(terminal, [types::terminal::READ, 0, 0, 0]) else {
let Ok(words) = syscall::request(terminal, [op, 0, 0, 0]) else {
break;
};
let Some((bytes, len)) = types::log::text_of(words) else {

View File

@@ -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

View 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
}
}

View File

@@ -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)?;

View File

@@ -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()
}

View File

@@ -0,0 +1,11 @@
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate libkos;
libkos::entry!(main);
fn main() {
gatekeeper::run();
}

View File

@@ -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);
}

View File

@@ -25,6 +25,8 @@ pub enum StorageRequest {
Mkdir { path_len: usize },
/// Carries the endpoint to print to.
SetLog,
/// Half (`half`: 0 or 1) of the master key.
SetKey { half: usize, words: [usize; 2] },
/// A capability to the path (`path_len` bytes of the buffer), inside
/// the caller's scope.
Grant {
@@ -74,6 +76,7 @@ impl Encode for StorageRequest {
Delete { path_len } => [op::DELETE, path_len, 0, 0],
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
SetLog => [op::SET_LOG, 0, 0, 0],
SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]],
Grant { path_len, write } => [op::GRANT, path_len, usize::from(write), 0],
Release => [op::RELEASE, 0, 0, 0],
}
@@ -95,6 +98,7 @@ impl Decode for StorageRequest {
op::DELETE => Delete { path_len: w[1] },
op::MKDIR => Mkdir { path_len: w[1] },
op::SET_LOG => SetLog,
op::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
op::GRANT => Grant {
path_len: w[1],
write: w[2] != 0,

View File

@@ -50,6 +50,9 @@ pub struct StorageServer<F: FileSystem, H: Host> {
clients: [Option<Client>; MAX_CLIENTS],
owners: [Option<Owner>; MAX_HANDLES],
log: Option<CapSlot>,
/// The master key's halves, from the gatekeeper through init: what the
/// disk's encryption will be keyed with.
key: [Option<[usize; 2]>; 2],
}
impl<F: FileSystem, H: Host> StorageServer<F, H> {
@@ -61,6 +64,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
clients: [const { None }; MAX_CLIENTS],
owners: [None; MAX_HANDLES],
log: None,
key: [None; 2],
}
}
@@ -83,6 +87,23 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
}
}
/// Only the unbadged holder (init), each half only once: nobody can
/// replace the key later.
fn set_key(&mut self, badge: usize, half: usize, words: [usize; 2]) -> StorageResponse {
match (badge, self.key.get_mut(half)) {
(0, Some(slot @ None)) => {
*slot = Some(words);
StorageResponse::Ok
}
_ => StorageResponse::Error,
}
}
/// Whether the whole master key has arrived.
pub fn unlocked(&self) -> bool {
self.key.iter().all(Option::is_some)
}
pub fn filesystem(&self) -> &F {
&self.fs
}
@@ -265,7 +286,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
None => R::NotFound,
};
let response = match request {
Attach | SetLog | Release => R::Error,
Attach | SetLog | SetKey { .. } | Release => R::Error,
Grant { path_len, write } => {
let narrower = scope.narrow(&buf[sent(path_len)], write);
return match narrower.and_then(|s| grants.add(badge, s, |b| host.mint(b))) {
@@ -368,6 +389,7 @@ impl<F: FileSystem, H: Host> Server for StorageServer<F, H> {
) -> ([usize; 4], Option<Give>) {
match StorageRequest::decode(words) {
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None),
Some(StorageRequest::SetKey { half, words }) => (self.set_key(badge, half, words).encode(), None),
// A badge storage never issued, or one it has taken back.
Some(request) => match self.grants.scope(badge) {
None => (StorageResponse::Denied.encode(), None),

View File

@@ -25,6 +25,7 @@ fn every_request_roundtrips() {
write: false,
},
StorageRequest::Release,
StorageRequest::SetKey { half: 1, words: [7, 9] },
] {
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}

View File

@@ -472,3 +472,16 @@ fn the_unbadged_holder_cannot_be_released() {
StorageResponse::Error
);
}
#[test]
fn only_init_sets_the_key_and_only_once() {
let mut s = server();
let set = |s: &mut Srv, badge, half| call(s, badge, StorageRequest::SetKey { half, words: [1, 2] }).0;
assert_eq!(set(&mut s, 5, 0), StorageResponse::Error, "a client cannot");
assert_eq!(set(&mut s, 0, 0), StorageResponse::Ok);
assert!(!s.unlocked());
assert_eq!(set(&mut s, 0, 0), StorageResponse::Error, "nor can it be replaced");
assert_eq!(set(&mut s, 0, 2), StorageResponse::Error, "two halves only");
assert_eq!(set(&mut s, 0, 1), StorageResponse::Ok);
assert!(s.unlocked());
}

View File

@@ -157,9 +157,9 @@ impl<C: Console> TerminalServer<C> {
}
}
/// Edits a line from the keyboard, echoing it, until Enter. Ctrl+C
/// drops it and reads as an empty line.
fn read_line(&mut self) {
/// Edits a line from the keyboard, echoing it (as `*` if `secret`),
/// until Enter. Ctrl+C drops it and reads as an empty line.
fn read_line(&mut self, secret: bool) {
self.console.flush();
// Flushed now; a flush timer still running is harmless.
self.flush_pending = false;
@@ -190,7 +190,7 @@ impl<C: Console> TerminalServer<C> {
b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
self.line[self.line_len] = b;
self.line_len += 1;
self.grid.put(b);
self.grid.put(if secret { b'*' } else { b });
}
_ => continue,
}
@@ -202,14 +202,18 @@ impl<C: Console> TerminalServer<C> {
}
/// The next chunk of the typed line, reading one first if none is left.
fn read(&mut self) -> [usize; 4] {
/// A secret line never reaches the screen, and is forgotten once read.
fn read(&mut self, secret: bool) -> [usize; 4] {
if self.line_read == self.line_len {
self.read_line();
self.read_line(secret);
}
let end = (self.line_read + TEXT_BYTES).min(self.line_len);
let chunk = &self.line[self.line_read..end];
let words = text_words(&self.line[self.line_read..end]);
self.line_read = end;
text_words(chunk)
if secret && self.line_read == self.line_len {
self.line.fill(0);
}
words
}
}
@@ -230,7 +234,8 @@ impl<C: Console> Server for TerminalServer<C> {
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
let reply = match words[0] {
types::terminal::READ => self.read(),
types::terminal::READ => self.read(false),
types::terminal::READ_SECRET => self.read(true),
types::terminal::REDRAW => {
// Taking focus back clears the screen.
self.console.take_focus();

View File

@@ -176,3 +176,13 @@ fn rows_are_drawn_only_as_wide_as_their_text_or_what_it_replaces() {
// Cleared: the old five cells and the cursor after them are erased.
assert!(t.console().rows_drawn.contains(&(0, 6)), "{:?}", t.console().rows_drawn);
}
#[test]
fn a_secret_line_shows_stars_and_is_forgotten() {
let mut t = term(b"hunter2\n");
let (words, _) = t.handle_call(1, [types::terminal::READ_SECRET, 0, 0, 0], None);
let (bytes, len) = text_of(words).unwrap();
assert_eq!(&bytes[..len], b"hunter2\n");
assert_eq!(&t.grid().row(0)[..8], b"*******\x20");
assert!(!t.grid().row(0).windows(7).any(|w| w == b"hunter2"));
}

View File

@@ -3,7 +3,9 @@ use crate::macros::bounded;
// Kani runs the real kernel with tiny tables; see kernel/src/proofs.
bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 });
bounded!(Priority, u8, 0, 31);
bounded!(PageCount, u16, 1, 1024);
// Up to a process's whole address window (32 MiB): one request may be big (a
// KDF's memory); budgets bound how much anyone gets.
bounded!(PageCount, u16, 1, 8192);
impl Priority {
pub const DEFAULT: Self = Self(16);

View File

@@ -0,0 +1,13 @@
//! Init and the gatekeeper: after handing it the keystore and randomness
//! (device setup calls), init calls `UNLOCK`, which returns once the owner
//! is authenticated (`OK`) or the gatekeeper cannot go on (`FAILED`); then
//! `KEY`, whose reply is the 32-byte master key in its four words. The
//! gatekeeper then exits. Only the unbadged holder (init) is answered.
pub const UNLOCK: usize = 1;
pub const KEY: usize = 2;
pub mod reply {
pub const OK: usize = 0;
pub const FAILED: usize = 1;
}

View File

@@ -8,6 +8,7 @@ pub mod bootinfo;
pub mod bounded;
pub mod budget;
pub mod display;
pub mod gatekeeper;
pub mod generation;
pub mod id;
pub mod input;

View File

@@ -33,6 +33,9 @@ pub mod op {
/// Takes back the grant this is called through, and everything
/// granted from it, from every holder.
pub const RELEASE: usize = 12;
/// Half the master key (word 1: which half, words 2..4: it), from the
/// unbadged holder (init) only, each half once.
pub const SET_KEY: usize = 13;
}
/// Reply tags (word 0).

View File

@@ -5,6 +5,9 @@
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
pub const READ: usize = 10;
pub const CLEAR: usize = 11;
/// As `READ`, but what is typed shows as `*` and is forgotten once read:
/// for passcodes.
pub const READ_SECRET: usize = 14;
/// Repaints the whole screen and takes input focus back, after a program
/// that drew on the screen itself.
pub const REDRAW: usize = 12;