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" name = "gatekeeper"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"driver-block-virtio",
"driver-rng-virtio",
"keystore", "keystore",
"libkos",
"port-block-device",
"port-entropy",
"types",
"zeroize", "zeroize",
] ]

View File

@@ -55,11 +55,17 @@ build:
-p init -p echo -p sleep -p snake \ -p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \ -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 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 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 image: build
scripts/make-disk-image.sh 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 # Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs
# the SMP self-test and halts. Passes only on "SMPTEST PASS". # 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 \ qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -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 run-gui: image
qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \ qemu-system-riscv64 -machine virt -smp $(SMP) -bios default \
-kernel $(KERNEL_BIN) \ -kernel $(KERNEL_BIN) \
-drive file=boot.img,if=none,format=raw,id=hd0 \ -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-gpu-device \
-device virtio-keyboard-device \ -device virtio-keyboard-device \
-serial stdio -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. /// A file written through storage, which the host then checks with e2fsck.
fn write_a_file(fs: &mut Storage) -> Result<(), &'static str> { 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")?; let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
if fs.write(h, NOTE) != Ok(NOTE.len()) { if fs.write(h, NOTE) != Ok(NOTE.len()) {
return Err("short write"); return Err("short write");

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ use libkos::Window;
use port_input_device::{InputDevice, InputEvent, Key}; use port_input_device::{InputDevice, InputEvent, Key};
use types::address::{ByteOffset, VirtualAddress}; 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_RBR: ByteOffset = ByteOffset::new(0);
const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1); const UART_IER_OFFSET: ByteOffset = ByteOffset::new(1);
const UART_LSR_OFFSET: ByteOffset = ByteOffset::new(5); 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::address::{ByteOffset, PhysicalAddress, VirtualAddress};
use types::bounded::PageCount; use types::bounded::PageCount;
const INPUT_MMIO_VA: VirtualAddress = VirtualAddress::from_raw(0x25000); /// Drivers map their devices in the device window (0x01D0_0000 up, 64 KiB
const VQUEUE_VA: VirtualAddress = VirtualAddress::from_raw(0x26000); /// each), clear of program images and heaps.
const EVBUF_VA: VirtualAddress = VirtualAddress::from_raw(0x28000); 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 QUEUE_SIZE: usize = 32;
const EVENT_SIZE: usize = 8; const EVENT_SIZE: usize = 8;

View File

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

View File

@@ -1,8 +1,9 @@
//! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard //! Finds the board's devices (`types::bootinfo`): disk, GPU, and keyboard
//! (or the UART when there is none). //! (or the UART when there is none).
use hw_virtio::block::CONFIG_CAPACITY;
use hw_virtio::mmio::{ 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::device::{self, Device};
use libkos::syscall; use libkos::syscall;
@@ -19,8 +20,14 @@ pub struct Devices {
pub disk: Device, pub disk: Device,
pub gpu: Device, pub gpu: Device,
pub input: 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> { fn slot(raw: u64) -> Option<CapSlot> {
if raw == NO_SLOT { if raw == NO_SLOT {
return None; return None;
@@ -28,21 +35,30 @@ fn slot(raw: u64) -> Option<CapSlot> {
CapSlot::new(u8::try_from(raw).ok()?) CapSlot::new(u8::try_from(raw).ok()?)
} }
/// The virtio device id behind `region`, 0 for an empty slot. Read through /// The virtio device id behind `region` (0 for an empty slot) and, for a
/// a copy of the capability, mapped only while looking and then dropped, /// block device, its size in sectors. Read through a copy of the
/// which unmaps it. /// capability, mapped only while looking and then dropped, which unmaps it.
fn virtio_id(region: CapSlot) -> u32 { fn virtio_id(region: CapSlot) -> (u32, u64) {
let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write); let perms = PermissionSet::empty().with(Permission::Read).with(Permission::Write);
let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else { let Ok(copy) = syscall::mint(region, Badge::NONE, perms) else {
return 0; return (0, 0);
}; };
let id = match device::map_mmio(copy, PROBE_VA) { let id = match device::map_mmio(copy, PROBE_VA) {
// The registers stay mapped until the drop below. // The registers stay mapped until the drop below.
Ok(regs) => match regs.read::<u32>(REG_MAGIC) == MAGIC { Ok(regs) => match regs.read::<u32>(REG_MAGIC) == MAGIC {
true => regs.read(REG_DEVICE_ID), true => {
false => 0, 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); syscall::drop_cap(copy);
id id
@@ -74,7 +90,13 @@ pub fn console() -> Option<CapSlot> {
/// Sorts the board's devices out. /// Sorts the board's devices out.
pub fn discover() -> Devices { pub fn discover() -> Devices {
let info = boot_info(); 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; let mut uart = Device::NONE;
for d in info.devices() { for d in info.devices() {
let Some(region) = slot(d.region_slot) else { let Some(region) = slot(d.region_slot) else {
@@ -84,9 +106,11 @@ pub fn discover() -> Devices {
let place = match d.kind { let place = match d.kind {
KIND_UART_16550 => &mut uart, KIND_UART_16550 => &mut uart,
KIND_VIRTIO => match virtio_id(region) { KIND_VIRTIO => match virtio_id(region) {
DEVICE_ID_BLOCK => &mut found.disk, (DEVICE_ID_BLOCK, size) if size <= KEYSTORE_MAX_SECTORS => &mut found.keystore,
DEVICE_ID_GPU => &mut found.gpu, (DEVICE_ID_BLOCK, _) => &mut found.disk,
DEVICE_ID_INPUT => &mut found.input, (DEVICE_ID_GPU, _) => &mut found.gpu,
(DEVICE_ID_INPUT, _) => &mut found.input,
(DEVICE_ID_RNG, _) => &mut found.random,
_ => continue, _ => continue,
}, },
_ => 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()) { if let Some(free) = self.served.iter_mut().find(|s| s.is_none()) {
*free = Some((p.name, ep)); *free = Some((p.name, ep));
} }
if let Some(device) = p.device { for device in p.devices {
note = self.give(device, ep); note = self.give(*device, ep);
}
if p.unlocks {
unlock(ep)?;
note = ", the owner is in";
} }
if console.is_some() { if console.is_some() {
hand_over_console(ep); hand_over_console(ep);
@@ -139,6 +143,8 @@ impl<'a> Launcher<'a> {
let (dev, given, none): (BoardDevice, _, _) = match device { let (dev, given, none): (BoardDevice, _, _) = match device {
Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"), Device::Gpu => (self.devices.gpu, ", gpu handed over", ", no gpu"),
Device::Input => (self.devices.input, ", input device handed over", ", no input device"), 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() { match libkos::device::give_device(ep, dev).is_ok() && dev.mmio.is_some() {
true => given, 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, /// From here on init prints through the log server, like everyone else,
/// and holds no console of its own. /// and holds no console of its own.
fn hand_over_console(log: CapSlot) { 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); libkos::print!("init: loading {}... ", p.name);
match launcher.launch(p) { match launcher.launch(p) {
Ok(note) => libkos::println!("ok{}", note), 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), Err(why) => libkos::println!("SKIP ({})", why),
} }
} }

View File

@@ -7,6 +7,8 @@ use types::budget::Limits;
pub enum Device { pub enum Device {
Gpu, Gpu,
Input, Input,
Keystore,
Random,
} }
/// A storage grant: a folder (or the whole disk, `b""`) and whether it may /// A storage grant: a folder (or the whole disk, `b""`) and whether it may
@@ -23,7 +25,11 @@ pub struct Program {
pub limits: Limits, pub limits: Limits,
/// Serves an endpoint, which it finds in slot 0. /// Serves an endpoint, which it finds in slot 0.
pub serves: bool, 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 /// 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. /// program's copy carries its own badge, so a server can tell them apart.
pub uses: &'static [&'static str], pub uses: &'static [&'static str],
@@ -51,7 +57,8 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
file, file,
limits, limits,
serves: true, serves: true,
device: None, devices: &[],
unlocks: false,
uses: &[], uses: &[],
storage: None, storage: None,
console: false, 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. /// Started on every boot. The log server comes first so the rest can print.
pub const SERVERS: &[Program] = &[ pub const SERVERS: &[Program] = &[
Program { console: true, ..server("log", b"bin/log", SMALL) }, 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. // The input server's only client: it passes input on to the app in focus.
Program { Program {
device: Some(Device::Gpu), devices: &[Device::Gpu],
uses: &["input"], uses: &["input"],
// The framebuffer alone is ~1000 pages at 1280x800. // The framebuffer alone is ~1000 pages at 1280x800.
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL }) ..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
}, },
// Text on the display, lines from the keyboard (it takes input focus). // Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) }, 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 // 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. // files and the owner's decisions, and runs apps through init.
Program { 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(), decode(SYS_REQUEST_PAGES, &[0, 0, 0, 0, 0, 0]).is_none(),
"zero pages" "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!( assert!(
decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(), decode(SYS_MAP_REGION, &[1, 0x30000, 3, 0, 0, 0]).is_none(),
"bad perms" "bad perms"

View File

@@ -3,7 +3,6 @@ use core::cell::UnsafeCell;
use core::ptr; use core::ptr;
const PAGE_SIZE: usize = 4096; const PAGE_SIZE: usize = 4096;
const MAX_PAGES: usize = 64;
struct FreeBlock { struct FreeBlock {
size: usize, size: usize,
@@ -14,10 +13,10 @@ pub struct KosAllocator {
inner: UnsafeCell<AllocatorInner>, 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 { struct AllocatorInner {
free_list: *mut FreeBlock, free_list: *mut FreeBlock,
pages: [usize; MAX_PAGES],
page_count: usize,
} }
unsafe impl Sync for KosAllocator {} unsafe impl Sync for KosAllocator {}
@@ -25,11 +24,7 @@ unsafe impl Sync for KosAllocator {}
impl KosAllocator { impl KosAllocator {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
inner: UnsafeCell::new(AllocatorInner { inner: UnsafeCell::new(AllocatorInner { free_list: ptr::null_mut() }),
free_list: ptr::null_mut(),
pages: [0; MAX_PAGES],
page_count: 0,
}),
} }
} }
@@ -41,19 +36,12 @@ impl KosAllocator {
impl AllocatorInner { impl AllocatorInner {
/// `n` pages in one piece, so an allocation larger than a page fits. /// `n` pages in one piece, so an allocation larger than a page fits.
fn request_pages(&mut self, n: usize) -> *mut u8 { 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 { let Some(count) = u16::try_from(n).ok().and_then(types::bounded::PageCount::new) else {
return ptr::null_mut(); return ptr::null_mut();
}; };
let Some(va) = crate::syscall::request_pages(count) else { let Some(va) = crate::syscall::request_pages(count) else {
return ptr::null_mut(); 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 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. /// Reads a typed line into `buf`, newline included if it fits; how much.
pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize { 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; let mut n = 0;
while n < buf.len() { 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; break;
}; };
let Some((bytes, len)) = types::log::text_of(words) else { let Some((bytes, len)) = types::log::text_of(words) else {

View File

@@ -6,3 +6,14 @@ edition = "2024"
[dependencies] [dependencies]
keystore = { workspace = true } keystore = { workspace = true }
zeroize = { 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. /// The owner, on the screen and keyboard.
pub trait Prompt { pub trait Prompt {
fn say(&mut self, text: &str); fn say(&mut self, text: &str);
/// Blanks the screen.
fn clear(&mut self);
/// Shows `question` and reads a passcode without showing it; false if /// Shows `question` and reads a passcode without showing it; false if
/// nothing could be read. /// nothing could be read.
fn secret(&mut self, question: &str, out: &mut Secret) -> bool; 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 /// A device never set up (or wiped): the owner chooses a passcode, and
/// may choose a duress one. Returns the new master key. /// 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> { 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"); prompt.say("This device has no passcode yet. Choose one; it protects everything on it.\n");
let mut real = Secret::new(); let mut real = Secret::new();
choose(prompt, "New passcode: ", false, &mut real)?; choose(prompt, "New passcode: ", false, &mut real)?;

View File

@@ -7,8 +7,78 @@
extern crate alloc; extern crate alloc;
pub mod devices;
pub mod flow; pub mod flow;
#[cfg(test)] #[cfg(test)]
#[path = "tests/mod.rs"] #[path = "tests/mod.rs"]
mod tests; 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. /// An owner typing from a script; what was shown is kept.
struct Owner { struct Owner {
typing: VecDeque<&'static str>, typing: VecDeque<&'static str>,
/// On the screen now.
shown: String, shown: String,
/// Ever shown, cleared or not.
said: String,
} }
impl Owner { impl Owner {
fn types(lines: &[&'static str]) -> Self { 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 { impl Prompt for Owner {
fn say(&mut self, text: &str) { fn say(&mut self, text: &str) {
self.shown.push_str(text); 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 { fn secret(&mut self, question: &str, out: &mut Secret) -> bool {
self.shown.push_str(question); 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", "", ""]); guesses.extend(["5678", "5678", "", ""]);
let mut owner = Owner::types(&guesses); let mut owner = Owner::types(&guesses);
let new = run_with(&mut dev, &mut owner).unwrap(); 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!(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"); assert_ne!(new.0, key, "the old master key is gone");
let mut old = Owner::types(&["1234"]); 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 mut owner = Owner::types(&["0000", "4321", "4321", "", ""]);
let new = run_with(&mut dev, &mut owner).unwrap(); let new = run_with(&mut dev, &mut owner).unwrap();
assert!(!owner.shown.contains("Wrong"), "nothing tells it was a duress passcode"); 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!(owner.shown.contains("no passcode yet"));
assert_ne!(new.0, key); assert_ne!(new.0, key);
} }

View File

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

View File

@@ -50,6 +50,9 @@ pub struct StorageServer<F: FileSystem, H: Host> {
clients: [Option<Client>; MAX_CLIENTS], clients: [Option<Client>; MAX_CLIENTS],
owners: [Option<Owner>; MAX_HANDLES], owners: [Option<Owner>; MAX_HANDLES],
log: Option<CapSlot>, 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> { 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], clients: [const { None }; MAX_CLIENTS],
owners: [None; MAX_HANDLES], owners: [None; MAX_HANDLES],
log: None, 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 { pub fn filesystem(&self) -> &F {
&self.fs &self.fs
} }
@@ -265,7 +286,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
None => R::NotFound, None => R::NotFound,
}; };
let response = match request { let response = match request {
Attach | SetLog | Release => R::Error, Attach | SetLog | SetKey { .. } | Release => R::Error,
Grant { path_len, write } => { Grant { path_len, write } => {
let narrower = scope.narrow(&buf[sent(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))) { 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>) { ) -> ([usize; 4], Option<Give>) {
match StorageRequest::decode(words) { match StorageRequest::decode(words) {
Some(StorageRequest::SetLog) => (self.set_log(badge, cap).encode(), None), 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. // A badge storage never issued, or one it has taken back.
Some(request) => match self.grants.scope(badge) { Some(request) => match self.grants.scope(badge) {
None => (StorageResponse::Denied.encode(), None), None => (StorageResponse::Denied.encode(), None),

View File

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

View File

@@ -472,3 +472,16 @@ fn the_unbadged_holder_cannot_be_released() {
StorageResponse::Error 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 /// Edits a line from the keyboard, echoing it (as `*` if `secret`),
/// drops it and reads as an empty line. /// until Enter. Ctrl+C drops it and reads as an empty line.
fn read_line(&mut self) { fn read_line(&mut self, secret: bool) {
self.console.flush(); self.console.flush();
// Flushed now; a flush timer still running is harmless. // Flushed now; a flush timer still running is harmless.
self.flush_pending = false; 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) => { b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
self.line[self.line_len] = b; self.line[self.line_len] = b;
self.line_len += 1; self.line_len += 1;
self.grid.put(b); self.grid.put(if secret { b'*' } else { b });
} }
_ => continue, _ => continue,
} }
@@ -202,14 +202,18 @@ impl<C: Console> TerminalServer<C> {
} }
/// The next chunk of the typed line, reading one first if none is left. /// 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 { 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 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; 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>) { fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
let reply = match words[0] { 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 => { types::terminal::REDRAW => {
// Taking focus back clears the screen. // Taking focus back clears the screen.
self.console.take_focus(); 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. // Cleared: the old five cells and the cursor after them are erased.
assert!(t.console().rows_drawn.contains(&(0, 6)), "{:?}", t.console().rows_drawn); 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. // Kani runs the real kernel with tiny tables; see kernel/src/proofs.
bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 }); bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 });
bounded!(Priority, u8, 0, 31); 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 { impl Priority {
pub const DEFAULT: Self = Self(16); 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 bounded;
pub mod budget; pub mod budget;
pub mod display; pub mod display;
pub mod gatekeeper;
pub mod generation; pub mod generation;
pub mod id; pub mod id;
pub mod input; pub mod input;

View File

@@ -33,6 +33,9 @@ pub mod op {
/// Takes back the grant this is called through, and everything /// Takes back the grant this is called through, and everything
/// granted from it, from every holder. /// granted from it, from every holder.
pub const RELEASE: usize = 12; 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). /// Reply tags (word 0).

View File

@@ -5,6 +5,9 @@
/// the line's last chunk ends with `\n`. Blocks until a line is entered. /// the line's last chunk ends with `\n`. Blocks until a line is entered.
pub const READ: usize = 10; pub const READ: usize = 10;
pub const CLEAR: usize = 11; 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 /// Repaints the whole screen and takes input focus back, after a program
/// that drew on the screen itself. /// that drew on the screen itself.
pub const REDRAW: usize = 12; 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 its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint). 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 A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is `Delegate`: the badge (unique per copy) tells the server which client is
calling, and `Delegate` lets the client send capabilities with its calls 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 session-server session
copy terminal-server terminal copy terminal-server terminal
copy shell shell copy shell shell
copy gatekeeper gatekeeper
# Apps. # Apps.
app cat app cat
app ls app ls

View File

@@ -1,20 +1,58 @@
#!/bin/bash #!/bin/bash
# Boots IMAGE headless and waits for the self-test (init's SMP checks, then # 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 # spawntest) to report; the machine halts itself when done. Twice, on one
# "SMPTEST PASS". # 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 set -uo pipefail
IMAGE="${1:?usage: test-qemu.sh IMAGE}" IMAGE="${1:?usage: test-qemu.sh IMAGE}"
SMP="${SMP:-4}" SMP="${SMP:-4}"
KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv" KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv"
LOG="$(mktemp -t kos-test-qemu.XXXXXX)" 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 \ # 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}" \ -kernel "${KERNEL}" \
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \ -drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
-device virtio-gpu-device -device virtio-keyboard-device \ -drive "file=${KEYS},if=none,format=raw,id=ks" -device virtio-blk-device,drive=ks \
-display none -serial "file:${LOG}" -monitor none -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=$? 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}" grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
# What k-os wrote must be ext4 that the real tools accept. # 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 fi
rm -f "${LOG}.fsck" rm -f "${LOG}.fsck"
fi 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)" echo "test-qemu: pass (${SMP} harts)"
rm -f "${LOG}" rm -f "${LOG}"
exit 0 exit 0