Преглед изворни кода

Wrote more tests for btfsd.

Matthew Carr пре 2 година
родитељ
комит
d5a19b1ff9

+ 0 - 115
TODO.txt

@@ -1,115 +0,0 @@
-# Format: - <task ID>, <task points>, <created by user>, <created on commit>, <finished by user>, <finished on commit>
-
-!- 0, 3, mdcarr941@gmail.com, 2ebb8a
-Fix bug where writing to a block that already has a Writecap in its header using the creds of
-a different node produces an invalid signature (a signature using the creds of the other node).
-
-!- 1
-Fix BufSectored so it doesn't have to write to the first sector every flush.
-
-- 2
-Track position and dirty-ness in Trailered.
-
-- 4
-Remove TryCompose?
-
-!- 5, 1, mdcarr941@gmail.com, bd6904, mdcarr941@gmail.com, bd6904
-Move crypto::{encrypt, decrypt} into corresponding {EncrypterExt, DecrypterExt}.
-
-!- 7, 2, mdcarr941@gmail.com, ?, mdcarr941@gmail.com, fd4356
-Add a ser_sign_into method to SignerExt which serializes a value into a provided Vec<u8> and returns
-a signature over this data. Update BlockStream::flush_integ to use this method.
-
-!- 8
-Convert all sector sizes to u32 for portability.
-(I ended up using u64 but keeping usize as the return type for Sectored::sector_sz)
-
-- 9
-Create an extension trait for u64 with a method for adding an i64 to it. Use this in
-SecretStream::seek, Trailered::seek and SectoredBuf::seek.
-
-!- 10, 5, mdcarr941@gmail.com, ?, mdcarr941@gmail.com, fd4356
-Create a struct which digests data written to it before passing it to an underlying Write.
-
-!- 11, 3, mdcarr941@gmail.com, bd6904, mdcarr941@gmail.com, bd6904
-Create a struct called WritecapBody to contain the fields of Writecap which go into the signature
-calculation so that WritecapSigInput is no longer required.
-
-!- 12, 8, mdcarr941@gmail.com, 2ebb8a,
-Create a struct for managing the directory used to store blocks in the file system. Design and
-implement an API for creating, opening, moving, copying, deleting and linking blocks. This API must
-be codified by a trait to allow the implementation to be changed in the future.
-
-!- 13, 5, mdcarr941@gmail.com, ?, mdcarr941@gmail.com, fd4356
-Change the Hash enum so it contains structs for each hash type. Unify these structs with the node
-structs used in the VecMerkleTree.
-
-!- 14, 13, mdcarr941@gmail.com, bd6904
-Refactor btlib so that most of the types are in their own modules. This is
-needed to encourage modularity and weak coupling, as it reduces the amount of code that fields
-and helper functions are visible to.
-
-!- 15, 13, mdcarr941@gmail.com, 58d1f6, 
-Create a new crate which implements a FUSE daemon.
-
-!- 16, 5, mdcarr941@gmail.com, 866533,
-Add the inherit field, which contains the crypto link from the parent block key to the current
-block key, to the block metadata.
-
-- 17, 13, mdcarr941@gmail.com, 8665339,
-SECURITY: Design and implement a mechanism to protect the keys in block's metadata dictionary from
-being correlated with one another. This mechanism must allow a principal with a readcap to be able
-to find their readcap and to rotate the block and create new readcaps for each of the principals in
-the dictionary, but prevent an attacker from being able to identify when two blocks contain
-readcaps for the same principal.
-
-!- 18, 3, mdcarr941@gmail.com, 8665339, ???
-SECURITY: Remove the path field from BlockMeta. It isn't needed as the block path should be
-independently know by any verified. This will ensure that path names are not stored in cleartext.
-
-- 19, 21, mdcarr941@gmail.com, 8665339,
-Integrate with tokio and add async methods to all of the stream types.
-
-- 20, 5, mdcarr941@gmail.com, ef1d43,
-Rewrite BlockPath to be more efficient by ensuring that all characters in a path are contiguous
-in memory.
-
-!- 22, 8, mdcarr941@gmail.com, fe2ffc, mdcarr941@gmail.com, fe2ffc
-Add a new fields to BlockMeta which stores data encrypted using the block key. This information must
-include:
-  * mode bits as u32
-  * Unix timestamps
-  * owner UID and GID
-  * size of block data in bytes as u64
-  * number of hardlinks to the block
-Also include a dictionary for user data, which is indexed using a String and whose values are
-Vec<u8> structs.
-
-- 23, 5, mdcarr941@gmail.com, 7f33fa,
-Manually implement the Serialize trait for BlockMetaBody so that the secrets field can be lazily
-updated upon serialization if the secrets_struct field has been modified. In order to detect
-modifications, a new field with the serde(skip) attribute needs to be added to BlockMetaBody to
-store the hash of BlockMetaSecrets that was computed just after decryption. 
-
-- 24, 3, mdcarr941@gmail.com, 7dbb358,
-Move `BlockRecord.frags` into `BlockMetaSecrets`.
-
-- 25, 2, mdcarr941@gmail.com, 02d8cb,
-Implement `Blocktree::batch_forget`.
-
-- 26, 13, mdcarr941@gmail.com, 44a6ef,
-Implement a timeout mechanism in LocalFs which will purge handles and locks that have not been
-accessed for a configured period of time.
-
-- 27, 8, mdcarr941@gmail.com, 1c59d92
-SECURITY: Reusing the IV for every sector in a block is a security risk. This is equivalent to using
-ECB mode with a cipher whose block size equals the sector size, meaning that patterns in the cipher
-text will be clearly visible. Design a method to avoid reusing the same IV for every sector.
-(Maybe use the sector index as the IV? That's kind of like CTR mode. Ah, I could hash the IV with
-the sector index, then use that as the IV for the sector.)
-
-- 27, 3, mdcarr941@gmail.com, 1c59d92
-SECURITY: Inode numbers a currently being exposed as the name of the file a block is stored in. This
-should be  avoided by hashing the inodes along with a salt. Because this salt needs to be accessible
-even before we've decrypted any data in the filesystem, we need to use data from the credentials.
-(Perhaps the path in the writecap?)

+ 17 - 0
crates/btfproto/src/client.rs

@@ -50,6 +50,7 @@ extractor_callback!(Create);
 extractor_callback!(Open);
 extractor_callback!(Write);
 extractor_callback!(Link);
+extractor_callback!(ReadDir);
 
 struct AckCallback;
 
@@ -255,6 +256,22 @@ impl<T: Transmitter> FsClient<T> {
         let msg = FsMsg::Unlock(Unlock { inode, handle });
         self.tx.call(msg, AckCallback).await?
     }
+
+    pub async fn read_dir(
+        &self,
+        inode: Inode,
+        handle: Handle,
+        limit: u32,
+        state: u64,
+    ) -> Result<ReadDirReply> {
+        let msg = FsMsg::ReadDir(ReadDir {
+            inode,
+            handle,
+            limit,
+            state,
+        });
+        self.tx.call(msg, ExtractReadDir).await?
+    }
 }
 
 impl<T> AsRef<T> for FsClient<T> {

+ 2 - 0
crates/btfproto/src/local_fs.rs

@@ -1137,6 +1137,8 @@ mod private {
                                 secrets.mode = mode & !umask;
                                 if flags.directory() {
                                     secrets.mode |= FileType::Dir.value();
+                                } else {
+                                    secrets.mode |= FileType::Reg.value();
                                 }
                                 secrets.uid = authz_attrs.uid;
                                 secrets.gid = authz_attrs.gid;

+ 224 - 30
crates/btfsd/src/main.rs

@@ -72,55 +72,117 @@ async fn main() {
 mod tests {
     use super::*;
 
+    use btlib::log::BuilderExt;
     use btfproto::{client::FsClient, msg::*};
     use btlib_tests::TpmCredStoreHarness;
-    use std::future::ready;
+    use btmsg::Transmitter;
+    use std::{future::ready, net::Ipv4Addr};
     use tempdir::TempDir;
 
-    struct TestCase {
-        _ip_addr: IpAddr,
+    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<R, T> {
+        client: FsClient<T>,
+        rx: R,
         _harness: TpmCredStoreHarness,
         _dir: TempDir,
     }
 
-    impl TestCase {
-        const ROOT_PASSWD: &str = "existential_threat";
-
-        fn new() -> (Self, impl Receiver) {
-            let dir = TempDir::new("btfsd").unwrap();
-            let harness = TpmCredStoreHarness::new(Self::ROOT_PASSWD.to_owned()).unwrap();
-            let ip_addr = IpAddr::V6(Ipv6Addr::LOCALHOST);
-            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"),
-            };
-            let receiver = receiver(config);
-            (
-                Self {
-                    _dir: dir,
-                    _harness: harness,
-                    _ip_addr: ip_addr,
-                },
-                receiver,
-            )
+    const ROOT_PASSWD: &str = "existential_threat";
+    const LOCALHOST: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST);
+
+    async fn test_case(
+        dir: TempDir,
+        harness: TpmCredStoreHarness,
+        ip_addr: IpAddr,
+    ) -> TestCase<impl Receiver, impl Transmitter> {
+        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"),
+        };
+        let rx = receiver(config);
+        let tx = rx.transmitter(rx.addr().clone()).await.unwrap();
+        let client = FsClient::new(tx);
+        TestCase {
+            _dir: dir,
+            _harness: harness,
+            rx,
+            client,
         }
     }
 
+    async fn new_case() -> TestCase<impl Receiver, impl Transmitter> {
+        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<R: Receiver, T: Transmitter>(
+        case: TestCase<R, T>,
+    ) -> TestCase<impl Receiver, impl Transmitter> {
+        case.rx.stop().await.unwrap();
+        case.rx.complete().unwrap().await.unwrap();
+        let TestCase { _dir, _harness, .. } = case;
+        test_case(_dir, _harness, IpAddr::V4(Ipv4Addr::LOCALHOST)).await
+    }
+
     #[allow(dead_code)]
     async fn manual_test() {
-        let (_case, rx) = TestCase::new();
-        rx.complete().unwrap().await.unwrap();
+        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, rx) = TestCase::new();
-        let tx = rx.transmitter(rx.addr().clone()).await.unwrap();
-        let client = FsClient::new(tx);
+        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 msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: EXPECTED.len() as u64,
+        };
+        let actual = client
+            .read(msg, |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(
@@ -134,6 +196,14 @@ mod tests {
             .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 msg = Read {
             inode,
             handle,
@@ -151,4 +221,128 @@ mod tests {
 
         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 msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: EXPECTED.len() as u64,
+        };
+        let actual = client
+            .read(msg, |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));
+    }
 }

+ 11 - 1
crates/btmsg/src/lib.rs

@@ -206,6 +206,11 @@ pub trait Receiver {
         Self: 'a;
     /// Returns a future which completes when this [Receiver] has completed (which may be never).
     fn complete(&self) -> Result<Self::CompleteFut<'_>>;
+
+    type StopFut<'a>: 'a + Future<Output = Result<()>> + Send
+    where
+        Self: 'a;
+    fn stop<'a>(&'a self) -> Self::StopFut<'a>;
 }
 
 /// A type which can be used to transmit messages.
@@ -410,7 +415,7 @@ macro_rules! await_or_stop {
     ($future:expr, $stop_fut:expr) => {
         select! {
             Some(connecting) = $future => connecting,
-            _ = $stop_fut => return,
+            _ = $stop_fut => break,
         }
     };
 }
@@ -553,6 +558,11 @@ impl Receiver for QuicReceiver {
             .ok_or_else(|| bterr!("join handle has already been taken"))?;
         Ok(handle)
     }
+
+    type StopFut<'a> = Ready<Result<()>>;
+    fn stop<'a>(&'a self) -> Self::StopFut<'a> {
+        ready(self.stop_tx.send(()).map(|_| ()).map_err(|err| err.into()))
+    }
 }
 
 macro_rules! cleanup_on_err {

+ 4 - 1
crates/swtpm-harness/src/lib.rs

@@ -94,7 +94,10 @@ active_pcr_banks = sha256
                 format!("file={}", pid_path.display()).as_str(),
             ])
             .status()?
-            .success_or_err()?;
+            .success_or_err()
+            .map_err(|err| {
+                anyhow!("swtpm {err}. This usually indicates an instance of swtpm is still running. You can rectify this with `killall swtpm`.")
+            })?;
         let mut blocker = DbusBlocker::new_session(dbus_name.clone())?;
         let tabrmd = Command::new("tpm2-abrmd")
             .args([