// SPDX-License-Identifier: AGPL-3.0-or-later mod config; use btfproto::{ local_fs::{LocalFs, ModeAuthorizer}, server::{new_fs_server, FsProvider}, }; use btlib::{ config_helpers::from_envvar, crypto::{tpm::TpmCredStore, CredStore, Creds}, }; use btmsg::Receiver; use config::{Config, ConfigRef, Envvars}; use std::{ net::{IpAddr, Ipv6Addr}, path::PathBuf, str::FromStr, sync::Arc, }; const ENVVARS: Envvars<'static> = Envvars { ip_addr: "BTFSD_IPADDR", tabrmd: "BTFSD_TABRMD", tpm_state_path: "BTFSD_TPMSTATE", block_dir: "BTFSD_BLOCKDIR", }; const DEFAULT_CONFIG: ConfigRef<'static> = ConfigRef { ip_addr: IpAddr::V6(Ipv6Addr::LOCALHOST), tabrmd: "bus_type=session", tpm_state_path: "./tpm_state", block_dir: "./bt", }; async fn provider( block_dir: PathBuf, creds: C, ) -> impl FsProvider { if block_dir.exists() { LocalFs::new_existing(block_dir, creds, ModeAuthorizer).unwrap() } else { std::fs::create_dir_all(&block_dir).unwrap(); LocalFs::new_empty(block_dir, 0, creds, ModeAuthorizer) .await .unwrap() } } async fn receiver(config: Config) -> impl Receiver { let cred_store = TpmCredStore::from_tabrmd(&config.tabrmd, config.tpm_state_path).unwrap(); let node_creds = cred_store.node_creds().unwrap(); let provider = Arc::new(provider(config.block_dir, node_creds.clone()).await); new_fs_server(config.ip_addr, Arc::new(node_creds), provider).unwrap() } #[tokio::main] async fn main() { let ip_addr = from_envvar(ENVVARS.ip_addr) .unwrap() .map(|txt| IpAddr::from_str(&txt).unwrap()); let tabrmd = from_envvar(ENVVARS.tabrmd).unwrap(); let tpm_state_path = from_envvar(ENVVARS.tpm_state_path) .unwrap() .map(PathBuf::from); let block_dir = from_envvar(ENVVARS.block_dir).unwrap().map(PathBuf::from); let config = Config::builder() .with_ip_addr(ip_addr) .with_tabrmd(tabrmd) .with_tpm_state_path(tpm_state_path) .with_block_dir(block_dir) .build(); let receiver = receiver(config).await; receiver.complete().unwrap().await.unwrap(); } #[cfg(test)] mod tests { use super::*; use btfproto::{client::FsClient, msg::*}; use btlib::{ crypto::{ConcreteCreds, CredsPriv, CredsPub}, log::BuilderExt, AuthzAttrs, BlockMetaSecrets, Epoch, IssuedProcRec, Principaled, ProcRec, }; use btlib_tests::TpmCredStoreHarness; use btmsg::{BlockAddr, Transmitter}; use btserde::from_slice; use std::{future::ready, net::Ipv4Addr, time::Duration}; use swtpm_harness::SwtpmHarness; use tempdir::TempDir; const LOG_LEVEL: &str = "warn"; #[ctor::ctor] fn ctor() { std::env::set_var("RUST_LOG", LOG_LEVEL); env_logger::Builder::from_default_env().btformat().init(); } struct TestCase { client: FsClient, rx: R, harness: TpmCredStoreHarness, _dir: TempDir, } const ROOT_PASSWD: &str = "existential_threat"; const LOCALHOST: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST); const BT_DIR: &str = "bt"; async fn test_case( dir: TempDir, harness: TpmCredStoreHarness, ip_addr: IpAddr, ) -> TestCase { let config = Config { ip_addr, tabrmd: harness.swtpm().tabrmd_config().to_owned(), tpm_state_path: harness.swtpm().state_path().to_owned(), block_dir: dir.path().join(BT_DIR), }; let rx = receiver(config).await; let tx = rx.transmitter(rx.addr().clone()).await.unwrap(); let client = FsClient::new(tx); TestCase { _dir: dir, harness, rx, client, } } async fn new_case() -> TestCase { let dir = TempDir::new("btfsd").unwrap(); let harness = TpmCredStoreHarness::new(ROOT_PASSWD.to_owned()).unwrap(); test_case(dir, harness, LOCALHOST).await } async fn existing_case( case: TestCase, ) -> TestCase { case.rx.stop().await.unwrap(); case.rx.complete().unwrap().await.unwrap(); let TestCase { _dir, harness: _harness, .. } = case; test_case(_dir, _harness, IpAddr::V4(Ipv4Addr::LOCALHOST)).await } #[allow(dead_code)] async fn manual_test() { let case = new_case().await; case.rx.complete().unwrap().await.unwrap(); } #[tokio::test] async fn create_write_read() { const FILENAME: &str = "file.txt"; const EXPECTED: &[u8] = b"potato"; let case = new_case().await; let client = case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { let mut buf = Vec::with_capacity(EXPECTED.len()); buf.extend_from_slice(reply.data); ready(buf) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn read_from_different_instance() { const FILENAME: &str = "file.txt"; const EXPECTED: &[u8] = b"potato"; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); client.flush(inode, handle).await.unwrap(); let case = existing_case(case).await; let client = &case.client; let LookupReply { inode, .. } = client .lookup(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { let mut buf = Vec::with_capacity(EXPECTED.len()); buf.extend_from_slice(reply.data); ready(buf) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn create_lookup() { const FILENAME: &str = "file.txt"; let case = new_case().await; let client = case.client; let CreateReply { inode: expected, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadOnly.into(), 0o644, 0, ) .await .unwrap(); let LookupReply { inode: actual, .. } = client .lookup(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn open_existing() { const FILENAME: &str = "file.txt"; let case = new_case().await; let client = case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadOnly.into(), 0o644, 0, ) .await .unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; assert!(result.is_ok()); } #[tokio::test] async fn write_flush_close_read() { const FILENAME: &str = "lyrics.txt"; const EXPECTED: &[u8] = b"Fate, or something better"; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); let WriteReply { written, .. } = client.write(inode, handle, 0, EXPECTED).await.unwrap(); assert_eq!(EXPECTED.len() as u64, written); client.flush(inode, handle).await.unwrap(); client.close(inode, handle).await.unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(reply.data.to_owned()) }) .await .unwrap(); assert_eq!(EXPECTED, &actual); } #[tokio::test] async fn link() { const FIRSTNAME: &str = "Jean-Luc"; const LASTNAME: &str = "Picard"; let case = new_case().await; let client = &case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FIRSTNAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .link(inode, SpecInodes::RootDir.into(), LASTNAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(filenames.contains(&FIRSTNAME)); assert!(filenames.contains(&LASTNAME)); } #[tokio::test] async fn unlink() { const FIRSTNAME: &str = "Jean-Luc"; const LASTNAME: &str = "Picard"; let case = new_case().await; let client = &case.client; let CreateReply { inode, .. } = client .create( SpecInodes::RootDir.into(), FIRSTNAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .link(inode, SpecInodes::RootDir.into(), LASTNAME) .await .unwrap(); client .unlink(SpecInodes::RootDir.into(), FIRSTNAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(filenames.contains(&LASTNAME)); assert!(!filenames.contains(&FIRSTNAME)); } #[tokio::test] async fn delete() { const FILENAME: &str = "MANIFESTO.tex"; let case = new_case().await; let client = &case.client; client .create( SpecInodes::RootDir.into(), FILENAME, Flags::default(), 0o644, 0, ) .await .unwrap(); client .unlink(SpecInodes::RootDir.into(), FILENAME) .await .unwrap(); let OpenReply { handle: root_handle, .. } = client .open( SpecInodes::RootDir.into(), FlagValue::ReadOnly | FlagValue::Directory, ) .await .unwrap(); let ReadDirReply { entries, .. } = client .read_dir(SpecInodes::RootDir.into(), root_handle, 0, 0) .await .unwrap(); let filenames: Vec<_> = entries.iter().map(|e| e.0.as_str()).collect(); assert!(!filenames.contains(&FILENAME)); } #[tokio::test] async fn read_meta() { const FILENAME: &str = "kibosh.txt"; const EXPECTED: u32 = 0o600; let case = new_case().await; let client = &case.client; let before_create = Epoch::now(); let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), EXPECTED, 0, ) .await .unwrap(); let before_read = Epoch::now(); let ReadMetaReply { attrs, .. } = client.read_meta(inode, Some(handle)).await.unwrap(); let actual = attrs.mode; assert_eq!(FileType::Reg | EXPECTED, actual); assert!(before_create <= attrs.ctime && attrs.ctime <= before_read); assert!(before_create <= attrs.mtime && attrs.mtime <= before_read); assert!(before_create <= attrs.atime && attrs.atime <= before_read); assert_eq!(attrs.block_id.inode, inode); assert_eq!(attrs.size, 0); assert!(attrs.tags.is_empty()); } #[tokio::test] async fn write_meta() { fn assert_eq(expected: &Attrs, actual: &BlockMetaSecrets) { assert_eq!(expected.mode, actual.mode); assert_eq!(expected.uid, actual.uid); assert_eq!(expected.gid, actual.gid); assert_eq!(expected.atime, actual.atime); assert_eq!(expected.mtime, actual.mtime); assert_eq!(expected.ctime, actual.ctime); assert_eq!(expected.tags.len(), actual.tags.len()); for (key, expected_value) in expected.tags.iter() { let actual_value = actual.tags.get(key).unwrap(); assert_eq!(expected_value, actual_value); } } const FILENAME: &str = "word_salad.docx"; let expected = Attrs { mode: 0o600, uid: 9290, gid: 2190, atime: 23193.into(), mtime: 53432.into(), ctime: 87239.into(), tags: Vec::new(), }; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), expected.mode | 0o011, 0, ) .await .unwrap(); let WriteMetaReply { attrs, .. } = client .write_meta(inode, Some(handle), expected.clone(), AttrsSet::ALL) .await .unwrap(); assert_eq(&expected, &attrs); let ReadMetaReply { attrs, .. } = client.read_meta(inode, Some(handle)).await.unwrap(); assert_eq(&expected, &attrs); } #[tokio::test] async fn allocate_when_empty() { const FILENAME: &str = "output.dat"; const EXPECTED: &[u8] = &[0u8; 8]; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client .allocate(inode, handle, EXPECTED.len() as u64) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(Vec::from(reply.data)) }) .await .unwrap(); assert_eq!(EXPECTED, actual); } #[tokio::test] async fn allocate_with_data_present() { const FILENAME: &str = "output.dat"; const EXPECTED: &[u8] = &[1, 1, 1, 1, 0, 0, 0, 0]; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client.write(inode, handle, 0, &[1, 1, 1, 1]).await.unwrap(); client .allocate(inode, handle, EXPECTED.len() as u64) .await .unwrap(); let actual = client .read(inode, handle, 0, EXPECTED.len() as u64, |reply| { ready(Vec::from(reply.data)) }) .await .unwrap(); assert_eq!(EXPECTED, actual); } #[tokio::test] async fn forget() { const FILENAME: &str = "seed.dat"; let case = new_case().await; let client = &case.client; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite.into(), 0o644, 0, ) .await .unwrap(); client.close(inode, handle).await.unwrap(); let result = client.forget(inode, 1).await; assert!(result.is_ok()); } #[tokio::test] async fn add_readcap() { const FILENAME: &str = "net"; let case = new_case().await; let client = &case.client; let creds = ConcreteCreds::generate().unwrap(); let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), FILENAME, FlagValue::ReadWrite | FlagValue::Directory, 0o755, 0, ) .await .unwrap(); client .add_readcap(inode, handle, creds.principal(), creds.concrete_pub().enc) .await .unwrap(); } #[tokio::test] async fn grant_access_to_root() { let case = new_case().await; let client = &case.client; let mut creds = ConcreteCreds::generate().unwrap(); let root_creds = case.harness.root_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), vec![], Epoch::now() + Duration::from_secs(3600), ) .unwrap(); creds.set_writecap(writecap); let expected = IssuedProcRec { addr: IpAddr::V6(Ipv6Addr::LOCALHOST), pub_creds: creds.concrete_pub(), writecap: creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; client .grant_access(SpecInodes::RootDir.into(), expected.clone()) .await .unwrap(); let LookupReply { inode, entry, .. } = client .lookup(SpecInodes::RootDir.into(), &creds.principal().to_string()) .await .unwrap(); let OpenReply { handle, .. } = client .open(inode, FlagValue::ReadOnly.into()) .await .unwrap(); let record = client .read(inode, handle, 0, entry.attr.size, |reply| { ready(from_slice::(reply.data)) }) .await .unwrap() .unwrap(); let actual = record.validate().unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn grant_access_to_non_root_dir() { const DIRNAME: &str = "var"; let case = new_case().await; let client = &case.client; let mut creds = ConcreteCreds::generate().unwrap(); let root_creds = case.harness.root_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), vec![DIRNAME.to_owned()], Epoch::now() + Duration::from_secs(3600), ) .unwrap(); creds.set_writecap(writecap); let expected = IssuedProcRec { addr: IpAddr::V6(Ipv6Addr::LOCALHOST), pub_creds: creds.concrete_pub(), writecap: creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; let CreateReply { inode, handle, .. } = client .create( SpecInodes::RootDir.into(), DIRNAME, FlagValue::Directory | FlagValue::ReadWrite, 0o755, 0, ) .await .unwrap(); client.close(inode, handle).await.unwrap(); client.grant_access(inode, expected.clone()).await.unwrap(); let LookupReply { inode: record_inode, entry, .. } = client .lookup(inode, &creds.principal().to_string()) .await .unwrap(); let OpenReply { handle: record_handle, .. } = client .open(record_inode, FlagValue::ReadOnly.into()) .await .unwrap(); let record = client .read(record_inode, record_handle, 0, entry.attr.size, |reply| { ready(from_slice::(reply.data)) }) .await .unwrap() .unwrap(); let actual = record.validate().unwrap(); assert_eq!(expected, actual); } #[tokio::test] async fn grant_access_non_root_user() { const ROOT_FILE: &str = "root.txt"; const USER_FILE: &str = "user.txt"; let user_tpm = SwtpmHarness::new().unwrap(); let case = new_case().await; let client = &case.client; let root_creds = case.harness.root_creds().unwrap(); let user_creds = { let cred_store = TpmCredStore::from_context( user_tpm.context().unwrap(), user_tpm.state_path().to_owned(), ) .unwrap(); let mut creds = cred_store.node_creds().unwrap(); let writecap = root_creds .issue_writecap( creds.principal(), vec![], Epoch::now() + Duration::from_secs(3600), ) .unwrap(); cred_store .assign_node_writecap(&mut creds, writecap) .unwrap(); creds }; let expected = IssuedProcRec { addr: IpAddr::V6(Ipv6Addr::LOCALHOST), pub_creds: user_creds.concrete_pub(), writecap: user_creds.writecap().unwrap().to_owned(), authz_attrs: AuthzAttrs { uid: 9001, gid: 9001, supp_gids: vec![12, 41, 19], }, }; let root_inode = SpecInodes::RootDir.into(); client .grant_access(root_inode, expected.clone()) .await .unwrap(); // Note that non-root users do not have permission to write to ROOT_FILE, but // UID 9001 has permission to write to USER_FILE. client .create(root_inode, ROOT_FILE, FlagValue::ReadWrite.into(), 0o644, 0) .await .unwrap(); let CreateReply { inode, handle, .. } = client .create(root_inode, USER_FILE, FlagValue::ReadWrite.into(), 0o644, 0) .await .unwrap(); let mut attrs = Attrs::default(); attrs.uid = expected.authz_attrs.uid; attrs.gid = expected.authz_attrs.gid; client .write_meta(inode, Some(handle), attrs, AttrsSet::UID | AttrsSet::GID) .await .unwrap(); let node_creds = case.harness.cred_store().node_creds().unwrap(); let bind_path = node_creds.writecap().unwrap().bind_path(); let block_addr = Arc::new(BlockAddr::new(LOCALHOST, Arc::new(bind_path))); let tx = btmsg::transmitter(block_addr, Arc::new(user_creds)) .await .unwrap(); let client = FsClient::new(tx); let root_dir = SpecInodes::RootDir.value(); let LookupReply { inode, .. } = client.lookup(root_dir, USER_FILE).await.unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; assert!(result.is_ok()); let LookupReply { inode, .. } = client.lookup(root_dir, ROOT_FILE).await.unwrap(); let result = client.open(inode, FlagValue::ReadWrite.into()).await; let err = result.err().unwrap().to_string(); assert_eq!("write access denied", err); } }