feat(storage): per-app/owner/system encryption live: gatekeeper hands seed + fresh flag, storage unlocks layer, init seeds areas from factory/ (empties on new key); rmdir op; test-qemu checks sealed home + reboot persistence
This commit is contained in:
@@ -125,10 +125,46 @@ fn revoke_stops_a_running_app(died: CapSlot) -> Result<(), &'static str> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The owner's area is sealed: seeded from the factory, and what is
|
||||||
|
/// written there one boot reads back the next.
|
||||||
|
fn encrypted_home(fs: &mut Storage) -> Result<(), &'static str> {
|
||||||
|
let mut home = [0u8; 512];
|
||||||
|
let mut factory = [0u8; 512];
|
||||||
|
let read = |fs: &mut Storage, path: &[u8], out: &mut [u8]| -> Result<usize, &'static str> {
|
||||||
|
let h = fs.open(path).map_err(|_| "cannot open")?;
|
||||||
|
let n = fs.read(h, out).map_err(|_| "cannot read (a seal broken?)")?;
|
||||||
|
let _ = fs.close(h);
|
||||||
|
Ok(n)
|
||||||
|
};
|
||||||
|
let n = read(fs, b"home/readme.txt", &mut home)?;
|
||||||
|
let m = read(fs, b"factory/home/readme.txt", &mut factory)?;
|
||||||
|
if home[..n] != factory[..m] {
|
||||||
|
return Err("the encrypted home does not hold the factory's readme");
|
||||||
|
}
|
||||||
|
libkos::println!("spawntest: home seeded from the factory, reads back through its seal: ok");
|
||||||
|
const NOTE: &[u8] = b"sealed across a reboot";
|
||||||
|
let mut back = [0u8; 64];
|
||||||
|
match read(fs, b"home/persist.txt", &mut back) {
|
||||||
|
Ok(n) if &back[..n] == NOTE => libkos::println!("spawntest: an encrypted file from the last boot reads back: ok"),
|
||||||
|
Ok(_) => return Err("an encrypted file from the last boot reads back wrong"),
|
||||||
|
Err(_) => {
|
||||||
|
let h = fs.create(b"home/persist.txt").map_err(|_| "cannot create in home")?;
|
||||||
|
let wrote = fs.write(h, NOTE) == Ok(NOTE.len());
|
||||||
|
let _ = fs.close(h);
|
||||||
|
if !wrote {
|
||||||
|
return Err("cannot write in home");
|
||||||
|
}
|
||||||
|
libkos::println!("spawntest: wrote an encrypted file for the next boot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn check() -> Result<(), &'static str> {
|
fn check() -> Result<(), &'static str> {
|
||||||
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
||||||
write_a_file(&mut fs)?;
|
write_a_file(&mut fs)?;
|
||||||
libkos::println!("spawntest: a file written to ext4 reads back: ok");
|
libkos::println!("spawntest: a file written to ext4 reads back: ok");
|
||||||
|
encrypted_home(&mut fs)?;
|
||||||
|
|
||||||
let died = syscall::create_notification().map_err(|_| "no notification")?;
|
let died = syscall::create_notification().map_err(|_| "no notification")?;
|
||||||
narrow_storage(&mut fs, died)?;
|
narrow_storage(&mut fs, died)?;
|
||||||
|
|||||||
137
crates/init/src/factory.rs
Normal file
137
crates/init/src/factory.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
//! The encrypted areas' starting point. The image ships starter content
|
||||||
|
//! (the owner's first files, the pre-approved decisions) in plaintext under
|
||||||
|
//! `factory/`; with a new master key (a fresh device, or one just wiped)
|
||||||
|
//! init empties the encrypted areas and seeds them from it, storage
|
||||||
|
//! sealing what is written. A device just wiped so looks factory-fresh,
|
||||||
|
//! not full of files no key opens. An area simply missing (a disk image
|
||||||
|
//! rebuilt) is seeded too.
|
||||||
|
|
||||||
|
use libkos::fs::Storage;
|
||||||
|
use types::storage::MAX_PATH;
|
||||||
|
|
||||||
|
const AREAS: [&str; 3] = ["home", "system", "data"];
|
||||||
|
/// Entries handled per listing; a folder with more is listed again.
|
||||||
|
const BATCH: usize = 32;
|
||||||
|
const NAME: usize = types::storage::DIR_NAME_SIZE;
|
||||||
|
|
||||||
|
/// A path being built, `a/b/c`.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Path {
|
||||||
|
bytes: [u8; MAX_PATH],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
fn new(s: &str) -> Self {
|
||||||
|
let mut p = Self { bytes: [0; MAX_PATH], len: 0 };
|
||||||
|
p.bytes[..s.len()].copy_from_slice(s.as_bytes());
|
||||||
|
p.len = s.len();
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join(&self, name: &[u8]) -> Option<Self> {
|
||||||
|
let mut p = *self;
|
||||||
|
let end = p.len + 1 + name.len();
|
||||||
|
let room = p.bytes.get_mut(p.len..end)?;
|
||||||
|
room[0] = b'/';
|
||||||
|
room[1..].copy_from_slice(name);
|
||||||
|
p.len = end;
|
||||||
|
Some(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.bytes[..self.len]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Up to `BATCH` entries of `dir`: names and whether each is a folder.
|
||||||
|
fn entries(fs: &mut Storage, dir: &Path) -> Option<([([u8; NAME], usize, bool); BATCH], usize)> {
|
||||||
|
let mut out = [([0u8; NAME], 0, false); BATCH];
|
||||||
|
let mut n = 0;
|
||||||
|
fs.list_dir(dir.as_bytes(), |e| {
|
||||||
|
let name = e.name();
|
||||||
|
if name != b"." && name != b".." && n < BATCH {
|
||||||
|
out[n].0[..name.len()].copy_from_slice(name);
|
||||||
|
out[n].1 = name.len();
|
||||||
|
out[n].2 = e.is_dir;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
Some((out, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes everything inside `dir`, keeping `dir`.
|
||||||
|
fn empty(fs: &mut Storage, dir: &Path) -> Result<(), &'static str> {
|
||||||
|
loop {
|
||||||
|
let (list, n) = entries(fs, dir).ok_or("cannot list an area to empty it")?;
|
||||||
|
if n == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
for (name, len, is_dir) in &list[..n] {
|
||||||
|
let child = dir.join(&name[..*len]).ok_or("path too long")?;
|
||||||
|
if *is_dir {
|
||||||
|
empty(fs, &child)?;
|
||||||
|
fs.rmdir(child.as_bytes()).map_err(|_| "cannot remove a folder")?;
|
||||||
|
} else {
|
||||||
|
fs.delete(child.as_bytes()).map_err(|_| "cannot remove a file")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the file `from` to a new file `to`.
|
||||||
|
fn copy_file(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||||
|
let src = fs.open(from.as_bytes()).map_err(|_| "cannot open a factory file")?;
|
||||||
|
let dst = fs.create(to.as_bytes()).map_err(|_| "cannot create a file")?;
|
||||||
|
let done = loop {
|
||||||
|
match fs.read(src, buf) {
|
||||||
|
Ok(0) => break Ok(()),
|
||||||
|
Ok(n) if fs.write(dst, &buf[..n]) == Ok(n) => {}
|
||||||
|
_ => break Err("cannot copy a factory file"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = fs.close(src);
|
||||||
|
let _ = fs.close(dst);
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies everything in `from` into `to` (which exists).
|
||||||
|
fn copy_tree(fs: &mut Storage, from: &Path, to: &Path, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||||
|
let (list, n) = match entries(fs, from) {
|
||||||
|
Some(l) => l,
|
||||||
|
// Nothing shipped for this area.
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
for (name, len, is_dir) in &list[..n] {
|
||||||
|
let (src, dst) = (from.join(&name[..*len]).ok_or("path too long")?, to.join(&name[..*len]).ok_or("path too long")?);
|
||||||
|
if *is_dir {
|
||||||
|
fs.mkdir(dst.as_bytes()).map_err(|_| "cannot make a folder")?;
|
||||||
|
copy_tree(fs, &src, &dst, buf)?;
|
||||||
|
} else {
|
||||||
|
copy_file(fs, &src, &dst, buf)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Readies the encrypted areas once storage has the key: `fresh` when the
|
||||||
|
/// master key is new.
|
||||||
|
pub(crate) fn prepare(fs: &mut Storage, fresh: bool, buf: &mut [u8]) -> Result<(), &'static str> {
|
||||||
|
for area in AREAS {
|
||||||
|
let path = Path::new(area);
|
||||||
|
let present = fs.list_dir(path.as_bytes(), |_| {}).is_ok();
|
||||||
|
if present && !fresh {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if present {
|
||||||
|
empty(fs, &path)?;
|
||||||
|
} else {
|
||||||
|
fs.mkdir(path.as_bytes()).map_err(|_| "cannot make an area")?;
|
||||||
|
}
|
||||||
|
let factory = Path::new("factory").join(area.as_bytes()).ok_or("path too long")?;
|
||||||
|
copy_tree(fs, &factory, &path, buf)?;
|
||||||
|
libkos::println!("init: {} {}", if fresh { "set up" } else { "seeded" }, area);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -114,8 +114,9 @@ impl<'a> Launcher<'a> {
|
|||||||
note = self.give(*device, ep);
|
note = self.give(*device, ep);
|
||||||
}
|
}
|
||||||
if p.unlocks {
|
if p.unlocks {
|
||||||
unlock(ep)?;
|
let fresh = unlock(ep)?;
|
||||||
note = ", the owner is in";
|
crate::factory::prepare(self.storage, fresh, self.scratch)?;
|
||||||
|
note = if fresh { ", the owner is in (a new key)" } else { ", the owner is in" };
|
||||||
}
|
}
|
||||||
if console.is_some() {
|
if console.is_some() {
|
||||||
hand_over_console(ep);
|
hand_over_console(ep);
|
||||||
@@ -154,20 +155,26 @@ impl<'a> Launcher<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
|
/// Waits for the gatekeeper behind `ep` to authenticate the owner, then
|
||||||
/// passes the master key on to storage, which alone keeps it.
|
/// passes the master key and a fresh seed on to storage, which alone keeps
|
||||||
fn unlock(ep: CapSlot) -> Result<(), &'static str> {
|
/// them. Whether the key is new.
|
||||||
use types::gatekeeper::{KEY, UNLOCK, reply};
|
fn unlock(ep: CapSlot) -> Result<bool, &'static str> {
|
||||||
match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
|
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
|
||||||
Ok([reply::OK, ..]) => {}
|
let fresh = match syscall::request(ep, [UNLOCK, 0, 0, 0]) {
|
||||||
|
Ok([reply::OK, fresh, ..]) => fresh != 0,
|
||||||
_ => return Err("the gatekeeper could not unlock"),
|
_ => return Err("the gatekeeper could not unlock"),
|
||||||
}
|
};
|
||||||
let mut key = syscall::request(ep, [KEY, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
|
for (ask, give) in [(KEY, types::storage::op::SET_KEY), (SEED, types::storage::op::SET_SEED)] {
|
||||||
|
let mut secret = syscall::request(ep, [ask, 0, 0, 0]).map_err(|_| "the gatekeeper gave no key")?;
|
||||||
let given = [0, 1].iter().all(|&half| {
|
let given = [0, 1].iter().all(|&half| {
|
||||||
let words = [types::storage::op::SET_KEY, half, key[half * 2], key[half * 2 + 1]];
|
let words = [give, half, secret[half * 2], secret[half * 2 + 1]];
|
||||||
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
|
syscall::request(crate::STORAGE_EP, words).is_ok_and(|r| r[0] == types::storage::reply::OK)
|
||||||
});
|
});
|
||||||
key.fill(0);
|
secret.fill(0);
|
||||||
if given { Ok(()) } else { Err("storage would not take the key") }
|
if !given {
|
||||||
|
return Err("storage would not take the key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From here on init prints through the log server, like everyone else,
|
/// From here on init prints through the log server, like everyone else,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
mod devices;
|
mod devices;
|
||||||
|
mod factory;
|
||||||
mod launch;
|
mod launch;
|
||||||
mod load;
|
mod load;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ impl Storage {
|
|||||||
self.with_path(op::MKDIR, path, reply::OK).map(|_| ())
|
self.with_path(op::MKDIR, path, reply::OK).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the empty directory at `path`.
|
||||||
|
pub fn rmdir(&mut self, path: &[u8]) -> Result<()> {
|
||||||
|
self.with_path(op::RMDIR, path, reply::OK).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close(&mut self, handle: FileHandle) -> Result<()> {
|
pub fn close(&mut self, handle: FileHandle) -> Result<()> {
|
||||||
self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ())
|
self.expect([op::CLOSE, handle.0 as usize, 0, 0], reply::OK).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,14 +151,21 @@ fn setup(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt
|
|||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The owner is in: the master key, and whether it is new (a fresh
|
||||||
|
/// device, or one just wiped), so nothing sealed under an old one exists.
|
||||||
|
pub struct Unlocked {
|
||||||
|
pub key: Key,
|
||||||
|
pub fresh: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Authenticates the owner and returns the master key: setting one up on
|
/// Authenticates the owner and returns the master key: setting one up on
|
||||||
/// a fresh device, wiping on duress or too many failures (the device then
|
/// a fresh device, wiping on duress or too many failures (the device then
|
||||||
/// looks fresh, and is set up anew).
|
/// looks fresh, and is set up anew).
|
||||||
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Key, Failure> {
|
pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prompt, params: KdfParams, memory: &mut [Block]) -> Result<Unlocked, Failure> {
|
||||||
loop {
|
loop {
|
||||||
let (record, next) = load(ks)?;
|
let (record, next) = load(ks)?;
|
||||||
let Some(record) = record else {
|
let Some(record) = record else {
|
||||||
return setup(ks, rng, prompt, params, memory);
|
return setup(ks, rng, prompt, params, memory).map(|key| Unlocked { key, fresh: true });
|
||||||
};
|
};
|
||||||
if record.must_wipe() {
|
if record.must_wipe() {
|
||||||
wipe(ks, &record, next, rng)?;
|
wipe(ks, &record, next, rng)?;
|
||||||
@@ -174,7 +181,7 @@ pub fn run(ks: &mut impl Keystore, rng: &mut impl Random, prompt: &mut impl Prom
|
|||||||
match record.unlock(typed.as_bytes(), memory) {
|
match record.unlock(typed.as_bytes(), memory) {
|
||||||
Outcome::Real(key) => {
|
Outcome::Real(key) => {
|
||||||
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
|
store(ks, 1 - next, &counted.next(|r| r.failures = 0))?;
|
||||||
return Ok(key);
|
return Ok(Unlocked { key, fresh: false });
|
||||||
}
|
}
|
||||||
Outcome::Duress => {
|
Outcome::Duress => {
|
||||||
// Recorded as worn out first: a wipe cut short resumes.
|
// Recorded as worn out first: a wipe cut short resumes.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use alloc::vec;
|
|||||||
use keystore::{Block, KdfParams};
|
use keystore::{Block, KdfParams};
|
||||||
use libkos::syscall::{self, Received};
|
use libkos::syscall::{self, Received};
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
use types::gatekeeper::{KEY, UNLOCK, reply};
|
use types::gatekeeper::{KEY, SEED, UNLOCK, reply};
|
||||||
|
|
||||||
/// Where init's manifest put them: its own endpoint (devices arrive on
|
/// Where init's manifest put them: its own endpoint (devices arrive on
|
||||||
/// it, then init's calls), then the terminal.
|
/// it, then init's calls), then the terminal.
|
||||||
@@ -53,9 +53,10 @@ pub fn run() -> ! {
|
|||||||
};
|
};
|
||||||
let mut memory = vec![Block::default(); KdfParams::DEFAULT.memory_kib as usize];
|
let mut memory = vec![Block::default(); KdfParams::DEFAULT.memory_kib as usize];
|
||||||
libkos::println!("gatekeeper: waiting for the owner");
|
libkos::println!("gatekeeper: waiting for the owner");
|
||||||
let key = flow::run(
|
let mut random = devices::Hardware(random);
|
||||||
|
let unlocked = flow::run(
|
||||||
&mut devices::Disk(keystore),
|
&mut devices::Disk(keystore),
|
||||||
&mut devices::Hardware(random),
|
&mut random,
|
||||||
&mut devices::Terminal(TERMINAL),
|
&mut devices::Terminal(TERMINAL),
|
||||||
KdfParams::DEFAULT,
|
KdfParams::DEFAULT,
|
||||||
&mut memory,
|
&mut memory,
|
||||||
@@ -65,14 +66,19 @@ pub fn run() -> ! {
|
|||||||
*b = Block::default();
|
*b = Block::default();
|
||||||
}
|
}
|
||||||
drop(memory);
|
drop(memory);
|
||||||
match key {
|
match unlocked {
|
||||||
Ok(key) => {
|
Ok(flow::Unlocked { key, fresh }) => {
|
||||||
libkos::println!("gatekeeper: unlocked");
|
libkos::println!("gatekeeper: unlocked");
|
||||||
libkos::terminal::clear(TERMINAL);
|
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]));
|
let words = |b: &[u8; 32]| core::array::from_fn::<usize, 4, _>(|i| usize::from_le_bytes(b[i * 8..i * 8 + 8].try_into().unwrap_or([0; 8])));
|
||||||
if answer(UNLOCK, [reply::OK, 0, 0, 0]) {
|
let mut seed = [0u8; 32];
|
||||||
let _ = answer(KEY, [word(0), word(1), word(2), word(3)]);
|
let seeded = keystore::Random::fill(&mut random, &mut seed);
|
||||||
|
if seeded && answer(UNLOCK, [reply::OK, usize::from(fresh), 0, 0]) && answer(KEY, words(&key.0)) {
|
||||||
|
let _ = answer(SEED, words(&seed));
|
||||||
|
} else if !seeded {
|
||||||
|
let _ = answer(UNLOCK, [reply::FAILED, 0, 0, 0]);
|
||||||
}
|
}
|
||||||
|
seed.fill(0);
|
||||||
}
|
}
|
||||||
Err(why) => {
|
Err(why) => {
|
||||||
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
libkos::println!("gatekeeper: cannot go on: {:?}", why);
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ fn memory() -> Vec<Block> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<keystore::Key, Failure> {
|
fn run_with(dev: &mut Device, owner: &mut Owner) -> Result<keystore::Key, Failure> {
|
||||||
run(dev, &mut Counter(5), owner, FAST, &mut memory())
|
run(dev, &mut Counter(5), owner, FAST, &mut memory()).map(|u| u.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fresh(dev: &mut Device, owner: &mut Owner) -> bool {
|
||||||
|
run(dev, &mut Counter(5), owner, FAST, &mut memory()).unwrap().fresh
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A device set up with passcode 1234 and duress 0000; its master key.
|
/// A device set up with passcode 1234 and duress 0000; its master key.
|
||||||
@@ -219,3 +223,11 @@ fn a_duress_whose_wipe_is_torn_still_wipes_next_boot() {
|
|||||||
dev.cut_after = None;
|
dev.cut_after = None;
|
||||||
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
|
assert!(dev.record().unwrap().must_wipe(), "the duress was recorded before the wipe began");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_new_master_key_counts_as_fresh() {
|
||||||
|
let mut dev = Device::new();
|
||||||
|
assert!(fresh(&mut dev, &mut Owner::types(&["1234", "1234", "0000", "0000"])), "first boot");
|
||||||
|
assert!(!fresh(&mut dev, &mut Owner::types(&["1234"])), "an unlock");
|
||||||
|
assert!(fresh(&mut dev, &mut Owner::types(&["0000", "5678", "5678", "", ""])), "after a duress wipe");
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,6 @@ pub fn run() -> ! {
|
|||||||
libkos::println!("storage: no ext4 filesystem it can read, exiting");
|
libkos::println!("storage: no ext4 filesystem it can read, exiting");
|
||||||
syscall::exit();
|
syscall::exit();
|
||||||
};
|
};
|
||||||
let mut srv = server::StorageServer::new(ext4, Kernel);
|
let mut srv = server::StorageServer::new(encrypted::Encrypted::new(ext4), Kernel);
|
||||||
server_framework::serve(&mut srv, ep)
|
server_framework::serve(&mut srv, ep)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ pub enum StorageRequest {
|
|||||||
SetLog,
|
SetLog,
|
||||||
/// Half (`half`: 0 or 1) of the master key.
|
/// Half (`half`: 0 or 1) of the master key.
|
||||||
SetKey { half: usize, words: [usize; 2] },
|
SetKey { half: usize, words: [usize; 2] },
|
||||||
|
/// Half of the seed for randomness.
|
||||||
|
SetSeed { half: usize, words: [usize; 2] },
|
||||||
|
Rmdir { path_len: usize },
|
||||||
/// 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 {
|
||||||
@@ -77,6 +80,8 @@ impl Encode for StorageRequest {
|
|||||||
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]],
|
SetKey { half, words } => [op::SET_KEY, half, words[0], words[1]],
|
||||||
|
SetSeed { half, words } => [op::SET_SEED, half, words[0], words[1]],
|
||||||
|
Rmdir { path_len } => [op::RMDIR, path_len, 0, 0],
|
||||||
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],
|
||||||
}
|
}
|
||||||
@@ -99,6 +104,8 @@ impl Decode for StorageRequest {
|
|||||||
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::SET_KEY => SetKey { half: w[1], words: [w[2], w[3]] },
|
||||||
|
op::SET_SEED => SetSeed { half: w[1], words: [w[2], w[3]] },
|
||||||
|
op::RMDIR => Rmdir { path_len: w[1] },
|
||||||
op::GRANT => Grant {
|
op::GRANT => Grant {
|
||||||
path_len: w[1],
|
path_len: w[1],
|
||||||
write: w[2] != 0,
|
write: w[2] != 0,
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ 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
|
/// The master key's and the seed's halves, from the gatekeeper through
|
||||||
/// disk's encryption will be keyed with.
|
/// init; handed to the file system (and forgotten here) once all in.
|
||||||
key: [Option<[usize; 2]>; 2],
|
key: [Option<[usize; 2]>; 2],
|
||||||
|
seed: [Option<[usize; 2]>; 2],
|
||||||
|
unlocked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
||||||
@@ -65,6 +67,8 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
owners: [None; MAX_HANDLES],
|
owners: [None; MAX_HANDLES],
|
||||||
log: None,
|
log: None,
|
||||||
key: [None; 2],
|
key: [None; 2],
|
||||||
|
seed: [None; 2],
|
||||||
|
unlocked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,20 +92,29 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Only the unbadged holder (init), each half only once: nobody can
|
/// Only the unbadged holder (init), each half only once: nobody can
|
||||||
/// replace the key later.
|
/// replace the key or the seed later.
|
||||||
fn set_key(&mut self, badge: usize, half: usize, words: [usize; 2]) -> StorageResponse {
|
fn set_half(&mut self, badge: usize, seed: bool, half: usize, words: [usize; 2]) -> StorageResponse {
|
||||||
match (badge, self.key.get_mut(half)) {
|
let halves = if seed { &mut self.seed } else { &mut self.key };
|
||||||
(0, Some(slot @ None)) => {
|
match (badge, self.unlocked, halves.get_mut(half)) {
|
||||||
*slot = Some(words);
|
(0, false, Some(slot @ None)) => *slot = Some(words),
|
||||||
|
_ => return StorageResponse::Error,
|
||||||
|
}
|
||||||
|
if let ([Some(k0), Some(k1)], [Some(s0), Some(s1)]) = (self.key, self.seed) {
|
||||||
|
let bytes = |w: [usize; 4]| -> [u8; 32] { core::array::from_fn(|i| w[i / 8].to_le_bytes()[i % 8]) };
|
||||||
|
let (mut master, mut seed) = (bytes([k0[0], k0[1], k1[0], k1[1]]), bytes([s0[0], s0[1], s1[0], s1[1]]));
|
||||||
|
self.fs.unlock(&master, &seed);
|
||||||
|
master.fill(0);
|
||||||
|
seed.fill(0);
|
||||||
|
self.key = [None; 2];
|
||||||
|
self.seed = [None; 2];
|
||||||
|
self.unlocked = true;
|
||||||
|
}
|
||||||
StorageResponse::Ok
|
StorageResponse::Ok
|
||||||
}
|
}
|
||||||
_ => StorageResponse::Error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the whole master key has arrived.
|
/// Whether the encrypted areas are open: the key and seed are in.
|
||||||
pub fn unlocked(&self) -> bool {
|
pub fn unlocked(&self) -> bool {
|
||||||
self.key.iter().all(Option::is_some)
|
self.unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filesystem(&self) -> &F {
|
pub fn filesystem(&self) -> &F {
|
||||||
@@ -225,7 +238,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
Write { handle, .. } => self
|
Write { handle, .. } => self
|
||||||
.owner(handle)
|
.owner(handle)
|
||||||
.is_some_and(|o| o.badge == badge && o.write),
|
.is_some_and(|o| o.badge == badge && o.write),
|
||||||
Create { .. } | Delete { .. } | Mkdir { .. } => scope.write,
|
Create { .. } | Delete { .. } | Mkdir { .. } | Rmdir { .. } => scope.write,
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
if !allowed {
|
if !allowed {
|
||||||
@@ -233,7 +246,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
}
|
}
|
||||||
let mutates = matches!(
|
let mutates = matches!(
|
||||||
request,
|
request,
|
||||||
Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. }
|
Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. } | Rmdir { .. }
|
||||||
);
|
);
|
||||||
if mutates && !self.fs.writable() {
|
if mutates && !self.fs.writable() {
|
||||||
return (R::ReadOnly, None);
|
return (R::ReadOnly, None);
|
||||||
@@ -263,6 +276,7 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
| Create { path_len }
|
| Create { path_len }
|
||||||
| Delete { path_len }
|
| Delete { path_len }
|
||||||
| Mkdir { path_len }
|
| Mkdir { path_len }
|
||||||
|
| Rmdir { path_len }
|
||||||
| ListDir { path_len } => Some(path_len),
|
| ListDir { path_len } => Some(path_len),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -286,7 +300,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 | SetKey { .. } | Release => R::Error,
|
Attach | SetLog | SetKey { .. } | SetSeed { .. } | 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))) {
|
||||||
@@ -353,6 +367,13 @@ impl<F: FileSystem, H: Host> StorageServer<F, H> {
|
|||||||
R::Error
|
R::Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Rmdir { .. } => {
|
||||||
|
if fs.rmdir(path) {
|
||||||
|
R::Ok
|
||||||
|
} else {
|
||||||
|
R::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
(response, None)
|
(response, None)
|
||||||
}
|
}
|
||||||
@@ -389,7 +410,8 @@ 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),
|
Some(StorageRequest::SetKey { half, words }) => (self.set_half(badge, false, half, words).encode(), None),
|
||||||
|
Some(StorageRequest::SetSeed { half, words }) => (self.set_half(badge, true, 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),
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ fn every_request_roundtrips() {
|
|||||||
},
|
},
|
||||||
StorageRequest::Release,
|
StorageRequest::Release,
|
||||||
StorageRequest::SetKey { half: 1, words: [7, 9] },
|
StorageRequest::SetKey { half: 1, words: [7, 9] },
|
||||||
|
StorageRequest::SetSeed { half: 0, words: [1, 2] },
|
||||||
|
StorageRequest::Rmdir { path_len: 3 },
|
||||||
] {
|
] {
|
||||||
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
|
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,14 +474,19 @@ fn the_unbadged_holder_cannot_be_released() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_init_sets_the_key_and_only_once() {
|
fn only_init_unlocks_and_only_once() {
|
||||||
let mut s = server();
|
let mut s = server();
|
||||||
let set = |s: &mut Srv, badge, half| call(s, badge, StorageRequest::SetKey { half, words: [1, 2] }).0;
|
let set = |s: &mut Srv, badge, req| call(s, badge, req).0;
|
||||||
assert_eq!(set(&mut s, 5, 0), StorageResponse::Error, "a client cannot");
|
let key = |half| StorageRequest::SetKey { half, words: [1, 2] };
|
||||||
assert_eq!(set(&mut s, 0, 0), StorageResponse::Ok);
|
let seed = |half| StorageRequest::SetSeed { half, words: [3, 4] };
|
||||||
assert!(!s.unlocked());
|
assert_eq!(set(&mut s, 5, key(0)), StorageResponse::Error, "a client cannot");
|
||||||
assert_eq!(set(&mut s, 0, 0), StorageResponse::Error, "nor can it be replaced");
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Ok);
|
||||||
assert_eq!(set(&mut s, 0, 2), StorageResponse::Error, "two halves only");
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "nor can a half be replaced");
|
||||||
assert_eq!(set(&mut s, 0, 1), StorageResponse::Ok);
|
assert_eq!(set(&mut s, 0, key(2)), StorageResponse::Error, "two halves only");
|
||||||
|
assert_eq!(set(&mut s, 0, key(1)), StorageResponse::Ok);
|
||||||
|
assert_eq!(set(&mut s, 0, seed(0)), StorageResponse::Ok);
|
||||||
|
assert!(!s.unlocked(), "not without the whole seed");
|
||||||
|
assert_eq!(set(&mut s, 0, seed(1)), StorageResponse::Ok);
|
||||||
assert!(s.unlocked());
|
assert!(s.unlocked());
|
||||||
|
assert_eq!(set(&mut s, 0, key(0)), StorageResponse::Error, "once unlocked, never again");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
//! the same strict parser the permission manager uses:
|
//! the same strict parser the permission manager uses:
|
||||||
//!
|
//!
|
||||||
//! app-image install <root> <manifest.toml> <binary>
|
//! app-image install <root> <manifest.toml> <binary>
|
||||||
//! an installed, pre-approved app: /apps/<id>/, its decision granting
|
//! an installed, pre-approved app: /apps/<id>/, and (under factory/,
|
||||||
//! everything it asks for, and /data/<id>/ if that includes storage
|
//! which init seeds the encrypted areas from) its decision granting
|
||||||
|
//! everything it asks for, and data/<id>/ if that includes storage
|
||||||
//! app-image stage <folder> <manifest.toml> <binary>
|
//! app-image stage <folder> <manifest.toml> <binary>
|
||||||
//! an app folder the owner can install later (`install <folder>`)
|
//! an app folder the owner can install later (`install <folder>`)
|
||||||
|
|
||||||
@@ -36,11 +37,11 @@ fn install(root: &Path, manifest: &Path, binary: &Path) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
let mut out = [0u8; MAX_DECISION_TEXT];
|
let mut out = [0u8; MAX_DECISION_TEXT];
|
||||||
let n = decision.write(&mut out);
|
let n = decision.write(&mut out);
|
||||||
let decisions = root.join("system/permissions");
|
let decisions = root.join("factory/system/permissions");
|
||||||
fs::create_dir_all(&decisions).map_err(|e| e.to_string())?;
|
fs::create_dir_all(&decisions).map_err(|e| e.to_string())?;
|
||||||
fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
|
fs::write(decisions.join(m.id.as_str()), &out[..n]).map_err(|e| e.to_string())?;
|
||||||
if decision.granted.contains(Capability::Storage) {
|
if decision.granted.contains(Capability::Storage) {
|
||||||
fs::create_dir_all(root.join("data").join(m.id.as_str())).map_err(|e| e.to_string())?;
|
fs::create_dir_all(root.join("factory/data").join(m.id.as_str())).map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
//! Init and the gatekeeper: after handing it the keystore and randomness
|
//! Init and the gatekeeper: after handing it the keystore and randomness
|
||||||
//! (device setup calls), init calls `UNLOCK`, which returns once the owner
|
//! (device setup calls), init calls `UNLOCK`, which returns once the owner
|
||||||
//! is authenticated (`OK`) or the gatekeeper cannot go on (`FAILED`); then
|
//! is authenticated (`OK`, word 1 non-zero if the master key is new: a
|
||||||
//! `KEY`, whose reply is the 32-byte master key in its four words. The
|
//! fresh device or one just wiped) or the gatekeeper cannot go on
|
||||||
//! gatekeeper then exits. Only the unbadged holder (init) is answered.
|
//! (`FAILED`); then `KEY`, whose reply is the 32-byte master key in its four
|
||||||
|
//! words, and `SEED`, 32 fresh random bytes from the hardware for storage's
|
||||||
|
//! randomness this boot. The gatekeeper then exits. Only the unbadged
|
||||||
|
//! holder (init) is answered.
|
||||||
|
|
||||||
pub const UNLOCK: usize = 1;
|
pub const UNLOCK: usize = 1;
|
||||||
pub const KEY: usize = 2;
|
pub const KEY: usize = 2;
|
||||||
|
pub const SEED: usize = 3;
|
||||||
|
|
||||||
pub mod reply {
|
pub mod reply {
|
||||||
pub const OK: usize = 0;
|
pub const OK: usize = 0;
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ pub mod op {
|
|||||||
/// Half the master key (word 1: which half, words 2..4: it), from the
|
/// Half the master key (word 1: which half, words 2..4: it), from the
|
||||||
/// unbadged holder (init) only, each half once.
|
/// unbadged holder (init) only, each half once.
|
||||||
pub const SET_KEY: usize = 13;
|
pub const SET_KEY: usize = 13;
|
||||||
|
/// Half the seed for storage's randomness this boot, as `SET_KEY`. The
|
||||||
|
/// encrypted areas open once the whole key and seed are in.
|
||||||
|
pub const SET_SEED: usize = 14;
|
||||||
|
/// Removes the (empty) directory at the path in the buffer.
|
||||||
|
pub const RMDIR: usize = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reply tags (word 0).
|
/// Reply tags (word 0).
|
||||||
|
|||||||
13
docs/apps.md
13
docs/apps.md
@@ -31,11 +31,13 @@ declares `display`. Nothing gets anything "just in case".
|
|||||||
## Disk layout
|
## Disk layout
|
||||||
|
|
||||||
```
|
```
|
||||||
/home/ the owner's files; the shell's `/`
|
/home/ the owner's files; the shell's `/` (encrypted)
|
||||||
/apps/<id>/ an installed app: manifest.toml and its binary
|
/apps/<id>/ an installed app: manifest.toml and its binary
|
||||||
/data/<id>/ the app's private folder (its `storage` capability)
|
/data/<id>/ the app's private folder (its `storage`) (encrypted, its own key)
|
||||||
/system/permissions/ the owner's decisions, one file per app id
|
/system/permissions/ the owner's decisions, one file per app id (encrypted)
|
||||||
/bin/ system servers and the shell (init starts these)
|
/bin/ system servers and the shell (init starts these)
|
||||||
|
/factory/ plaintext starter content the encrypted areas are
|
||||||
|
seeded from with a new master key
|
||||||
```
|
```
|
||||||
|
|
||||||
Only the permission manager reaches `/apps`, `/data` and `/system`. The
|
Only the permission manager reaches `/apps`, `/data` and `/system`. The
|
||||||
@@ -89,8 +91,9 @@ granted = ["display"]
|
|||||||
```
|
```
|
||||||
|
|
||||||
Written by the permission manager when the owner decides; apps on the
|
Written by the permission manager when the owner decides; apps on the
|
||||||
system image ship with their decisions already recorded (pre-approved by
|
system image ship with their decisions already recorded under
|
||||||
whoever built the image). Changing one later is the owner's call, through
|
`factory/system/` (pre-approved by whoever built the image), and init
|
||||||
|
seeds them into the encrypted `system/` on a fresh setup. Changing one later is the owner's call, through
|
||||||
the permission manager.
|
the permission manager.
|
||||||
|
|
||||||
## Permission manager
|
## Permission manager
|
||||||
|
|||||||
10
docs/ipc.md
10
docs/ipc.md
@@ -97,8 +97,14 @@ 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
|
(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,
|
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
|
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
|
(`SET_KEY`, from the unbadged holder only, each half once), and a fresh
|
||||||
starts the permission manager and the desktop. If the gatekeeper cannot
|
32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot.
|
||||||
|
Storage's encrypted areas (`home/`, `system/`, each app's `data/<id>/`;
|
||||||
|
crate `filecrypt`, layer `storage_server::encrypted`) open once both are
|
||||||
|
in. With a new key (a fresh device, or one just wiped: `UNLOCK`'s word 1)
|
||||||
|
init empties those areas and seeds them from the plaintext `factory/`;
|
||||||
|
an area that is missing is seeded too. Only then does init start the
|
||||||
|
permission manager and the desktop. If the gatekeeper cannot
|
||||||
unlock, init stops there. The gatekeeper exits, and its capabilities go
|
unlock, init stops there. The gatekeeper exits, and its capabilities go
|
||||||
with it.
|
with it.
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ app() {
|
|||||||
staged() {
|
staged() {
|
||||||
local crate="$1" folder="$2"
|
local crate="$1" folder="$2"
|
||||||
if [ -f "${BUILD_DIR}/${crate}" ]; then
|
if [ -f "${BUILD_DIR}/${crate}" ]; then
|
||||||
"${APP_IMAGE[@]}" stage "${ROOT}/home/${folder}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}"
|
"${APP_IMAGE[@]}" stage "${ROOT}/factory/home/${folder}" "crates/apps/${crate}/manifest.toml" "${BUILD_DIR}/${crate}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/data" "${ROOT}/system/permissions" "${ROOT}/home"
|
# home/, system/ and data/ are encrypted, so not made here: init seeds them
|
||||||
|
# from factory/ (plaintext) once the gatekeeper has the key.
|
||||||
|
mkdir -p "${ROOT}/bin" "${ROOT}/apps" "${ROOT}/factory/home" "${ROOT}/factory/system/permissions" "${ROOT}/factory/data"
|
||||||
# System servers and the shell: init starts these.
|
# System servers and the shell: init starts these.
|
||||||
copy display-qemu display
|
copy display-qemu display
|
||||||
copy input-qemu input
|
copy input-qemu input
|
||||||
@@ -53,8 +55,8 @@ app ls
|
|||||||
app sleep
|
app sleep
|
||||||
app snake
|
app snake
|
||||||
staged counter inbox/counter
|
staged counter inbox/counter
|
||||||
# The owner's files.
|
# The owner's first files.
|
||||||
cp -r disk/home/. "${ROOT}/home/"
|
cp -r disk/home/. "${ROOT}/factory/home/"
|
||||||
# Only the test image: its presence makes init run the self-test.
|
# Only the test image: its presence makes init run the self-test.
|
||||||
if [ -n "${SMPTEST:-}" ]; then
|
if [ -n "${SMPTEST:-}" ]; then
|
||||||
copy smptest smptest
|
copy smptest smptest
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ echo "test-qemu: first boot: passcode set up: $([ ${first_ok} -eq 1 ] && echo ok
|
|||||||
boot 9999 1234
|
boot 9999 1234
|
||||||
[ ${status} -eq 0 ] && status=$?
|
[ ${status} -eq 0 ] && status=$?
|
||||||
second_ok=0
|
second_ok=0
|
||||||
grep -aq "the owner is in" "${LOG}" && second_ok=1
|
grep -aq "the owner is in" "${LOG}" && grep -aq "from the last boot reads back: ok" "${LOG}" && second_ok=1
|
||||||
echo "test-qemu: second boot: wrong passcode refused, right one unlocks: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
|
echo "test-qemu: second boot: wrong passcode refused, right one unlocks, sealed file kept: $([ ${second_ok} -eq 1 ] && echo ok || echo FAIL)"
|
||||||
rm -f "${KEYS}" "${MON}"
|
rm -f "${KEYS}" "${MON}"
|
||||||
|
|
||||||
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
|
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
|
||||||
@@ -62,8 +62,12 @@ if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then
|
|||||||
echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0
|
echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0
|
||||||
elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then
|
elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then
|
||||||
echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0
|
echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0
|
||||||
|
elif debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | grep -aq "Welcome"; then
|
||||||
|
echo "test-qemu: /home/readme.txt is plaintext on the disk"; disk_ok=0
|
||||||
|
elif ! debugfs -R 'cat /home/readme.txt' "${IMAGE}" 2>/dev/null | head -c 8 | grep -aq "KOSFILE1"; then
|
||||||
|
echo "test-qemu: /home/readme.txt is not a sealed file"; disk_ok=0
|
||||||
else
|
else
|
||||||
echo "test-qemu: e2fsck clean, file on disk"
|
echo "test-qemu: e2fsck clean, file on disk, home sealed"
|
||||||
fi
|
fi
|
||||||
rm -f "${LOG}.fsck"
|
rm -f "${LOG}.fsck"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user