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

6
Cargo.lock generated
View File

@@ -291,7 +291,13 @@ version = "0.1.0"
name = "gatekeeper"
version = "0.1.0"
dependencies = [
"driver-block-virtio",
"driver-rng-virtio",
"keystore",
"libkos",
"port-block-device",
"port-entropy",
"types",
"zeroize",
]

View File

@@ -55,11 +55,17 @@ build:
-p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls -p counter
-p terminal-server -p shell -p cat -p ls -p counter -p gatekeeper
cargo build --target $(RISCV_TARGET) --release -p adapter-riscv
# The keystore holds the owner's sealed keys: made once, never rebuilt.
KEYS_IMG ?= keys.img
QEMU_KEYS = -drive file=$(KEYS_IMG),if=none,format=raw,id=ks -device virtio-blk-device,drive=ks \
-device virtio-rng-device
image: build
scripts/make-disk-image.sh
[ -f $(KEYS_IMG) ] || truncate -s 64K $(KEYS_IMG)
# Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs
# the SMP self-test and halts. Passes only on "SMPTEST PASS".
@@ -75,13 +81,13 @@ run-disk: image
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0
-device virtio-blk-device,drive=hd0 $(QEMU_KEYS)
run-gui: image
qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
-kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-device virtio-blk-device,drive=hd0 $(QEMU_KEYS) \
-device virtio-gpu-device \
-device virtio-keyboard-device \
-serial stdio

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;

View File

@@ -91,6 +91,17 @@ and budget listed there. Storage, started before the log server, receives
its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint).
Nothing that touches user data starts before the owner is authenticated.
Init starts the log, input, display and terminal servers, then the
gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore
(a small disk of its own, told from the data disk by its size) and the
virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in,
then `KEY`, whose reply is the master key; init passes it to storage
(`SET_KEY`, from the unbadged holder only, each half once) and only then
starts the permission manager and the desktop. If the gatekeeper cannot
unlock, init stops there. The gatekeeper exits, and its capabilities go
with it.
A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is
calling, and `Delegate` lets the client send capabilities with its calls

View File

@@ -46,6 +46,7 @@ copy permission-manager permission
copy session-server session
copy terminal-server terminal
copy shell shell
copy gatekeeper gatekeeper
# Apps.
app cat
app ls

View File

@@ -1,20 +1,58 @@
#!/bin/bash
# Boots IMAGE headless and waits for the self-test (init's SMP checks, then
# spawntest) to report; the machine halts itself when done. Exit 0 only on
# "SMPTEST PASS".
# spawntest) to report; the machine halts itself when done. Twice, on one
# fresh keystore: the first boot sets a passcode up at the gatekeeper, the
# second gets it wrong once and then right. Exit 0 only if both unlock and
# both say "SMPTEST PASS".
set -uo pipefail
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
SMP="${SMP:-4}"
KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv"
LOG="$(mktemp -t kos-test-qemu.XXXXXX)"
KEYS="${LOG}.keys"
MON="${LOG}.mon"
truncate -s 64K "${KEYS}"
timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \
-kernel "${KERNEL}" \
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
-device virtio-gpu-device -device virtio-keyboard-device \
-display none -serial "file:${LOG}" -monitor none
# Types `lines` (one per argument) at the gatekeeper once it waits.
owner() {
for _ in $(seq 300); do grep -aq "gatekeeper: waiting" "${LOG}" 2>/dev/null && break; sleep 0.2; done
for line in "$@"; do
for ((i = 0; i < ${#line}; i++)); do
echo "sendkey ${line:i:1}" | socat - "unix-connect:${MON}" >/dev/null 2>&1; sleep 0.05
done
echo "sendkey ret" | socat - "unix-connect:${MON}" >/dev/null 2>&1; sleep 0.3
done
}
# One boot; `owner` types the arguments. Its serial output lands in LOG.
boot() {
: > "${LOG}"; rm -f "${MON}"
owner "$@" &
timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \
-kernel "${KERNEL}" \
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
-drive "file=${KEYS},if=none,format=raw,id=ks" -device virtio-blk-device,drive=ks \
-device virtio-rng-device -device virtio-gpu-device -device virtio-keyboard-device \
-display none -serial "file:${LOG}" -monitor "unix:${MON},server,nowait"
local s=$?
wait
return $s
}
# First boot: a new passcode (twice), no duress passcode (twice empty).
boot 1234 1234 "" ""
status=$?
first_ok=0
grep -aq "the owner is in" "${LOG}" && grep -aq "SMPTEST PASS" "${LOG}" && first_ok=1
echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok || echo FAIL)"
# Second boot: the keystore remembers; a wrong guess, then the passcode.
boot 9999 1234
[ ${status} -eq 0 ] && status=$?
second_ok=0
grep -aq "the owner is in" "${LOG}" && second_ok=1
echo "test-qemu: second boot: wrong passcode refused, right one unlocks: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
rm -f "${KEYS}" "${MON}"
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
# What k-os wrote must be ext4 that the real tools accept.
@@ -29,7 +67,7 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then
fi
rm -f "${LOG}.fsck"
fi
if [ "${disk_ok}" -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then
if [ "${disk_ok}" -eq 1 ] && [ ${first_ok} -eq 1 ] && [ ${second_ok} -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then
echo "test-qemu: pass (${SMP} harts)"
rm -f "${LOG}"
exit 0