Prechádzať zdrojové kódy

Started integration testing Block.

Matthew Carr 2 rokov pred
rodič
commit
18c67a827b

+ 20 - 0
crates/btlib/TODO.txt

@@ -0,0 +1,20 @@
+Write more tests which exercise the creation, writing and reading of blocks, including those
+persisted to the filesystem.
+
+Fix BufSectored so it doesn't have to write to the first sector every flush.
+
+Track position and dirty-ness in Trailered.
+
+Implement a stream which is both Read and Write and which can transparently compress and decompress
+data written to and read from it.
+
+Remove TryCompose?
+
+Move crypto::{encrypt, decrypt} into corresponding {EncrypterExt, DecrypterExt}.
+
+Create an enum to eliminate the use of Block trait objects?
+
+Add a ser_sign_into method to SignerExt which serializes a value into a provided Vec<u8> and returns
+a signature over this data.
+
+Convert all sector sizes to u64 for portability.

+ 45 - 17
crates/btlib/src/crypto/mod.rs

@@ -1,5 +1,5 @@
 /// Functions for performing cryptographic operations on the main data structures.
-mod tpm;
+pub mod tpm;
 
 use crate::{Block, BoxInIoErr, Decompose, HeaderAccess, Trailered, WriteInteg, SECTOR_SZ_DEFAULT};
 
@@ -71,7 +71,7 @@ pub enum Error {
 }
 
 impl Error {
-    fn custom<E: std::fmt::Debug + Send + Sync + 'static>(err: E) -> Self {
+    pub(crate) fn custom<E: std::fmt::Debug + Send + Sync + 'static>(err: E) -> Self {
         Error::Custom(Box::new(err))
     }
 }
@@ -535,6 +535,12 @@ impl Decrypter for SymKey {
     }
 }
 
+impl Default for SymKeyKind {
+    fn default() -> Self {
+        SymKeyKind::Aes256Ctr
+    }
+}
+
 #[repr(u32)]
 #[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
 pub enum KeyLen {
@@ -1105,11 +1111,11 @@ impl CredsPriv for ConcreteCreds {}
 
 impl Creds for ConcreteCreds {}
 
-pub(crate) trait Encrypter {
+pub trait Encrypter {
     fn encrypt(&self, slice: &[u8]) -> Result<Vec<u8>>;
 }
 
-pub(crate) trait EncrypterExt: Encrypter {
+pub trait EncrypterExt: Encrypter {
     fn ser_encrypt<T: Serialize>(&self, value: &T) -> Result<Ciphertext<T>> {
         let data = to_vec(value)?;
         let data = self.encrypt(&data)?;
@@ -1218,7 +1224,7 @@ pub trait MerkleNode: Default + Serialize + for<'de> Deserialize<'de> {
 
     /// Returns `Ok(())` if self contains the given hash data, and `Err(Error::HashCmpFailure)`
     /// otherwise.
-    fn assert_contains(&self, hash_data: &[u8]) -> Result<()>;
+    fn assert_contains(&self, hash_data: Option<&[u8]>) -> Result<()>;
 
     /// Returns `Ok(())` if self contains the hash of the given data. Otherwise,
     /// `Err(Error::HashCmpFailure)` is returned.
@@ -1313,19 +1319,18 @@ impl MerkleNode for Sha2_256Node {
         })
     }
 
-    fn assert_contains(&self, hash_data: &[u8]) -> Result<()> {
-        if let Some(slice) = self.as_slice() {
-            if slice == hash_data {
-                return Ok(());
-            }
+    fn assert_contains(&self, hash_data: Option<&[u8]>) -> Result<()> {
+        if self.as_slice() == hash_data {
+            Ok(())
+        } else {
+            Err(Error::HashCmpFailure)
         }
-        Err(Error::HashCmpFailure)
     }
 
     fn assert_contains_hash_of<'a, I: Iterator<Item = &'a [u8]>>(&self, parts: I) -> Result<()> {
         let mut buf = [0u8; Self::KIND.len()];
         Self::digest(&mut buf, parts)?;
-        self.assert_contains(&buf)
+        self.assert_contains(Some(&buf))
     }
 
     fn assert_parent_of<'a, I: Iterator<Item = &'a [u8]>>(
@@ -1410,7 +1415,7 @@ impl BinTreeIndex {
 trait MerkleTree: Sectored {
     /// Checks that the root node contains the given hash data. If it does then `Ok(())` is
     /// returned. If it doesn't, then `Err(Error::HashCmpFailure)` is returned.
-    fn assert_root_contains(&mut self, hash_data: &[u8]) -> Result<()>;
+    fn assert_root_contains(&mut self, hash_data: Option<&[u8]>) -> Result<()>;
 
     /// Hashes the given data, adds a new node to the tree with its hash and updates the hashes
     /// of all parent nodes.
@@ -1545,14 +1550,21 @@ impl<T: MerkleNode> VecMerkleTree<T> {
 }
 
 impl<T: MerkleNode> MerkleTree for VecMerkleTree<T> {
-    fn assert_root_contains(&mut self, hash_data: &[u8]) -> Result<()> {
+    fn assert_root_contains(&mut self, hash_data: Option<&[u8]>) -> Result<()> {
         match self.hash_at(BinTreeIndex(0)) {
             Ok(root) => {
                 root.assert_contains(hash_data)?;
                 self.root_verified = true;
                 Ok(())
             }
-            Err(_) => Err(Error::HashCmpFailure),
+            Err(Error::IndexOutOfBounds { .. }) => {
+                if hash_data.is_none() {
+                    Ok(())
+                } else {
+                    Err(Error::HashCmpFailure)
+                }
+            }
+            Err(err) => Err(err),
         }
     }
 
@@ -1669,7 +1681,7 @@ impl Sectored for VariantMerkleTree {
 }
 
 impl MerkleTree for VariantMerkleTree {
-    fn assert_root_contains(&mut self, hash_data: &[u8]) -> Result<()> {
+    fn assert_root_contains(&mut self, hash_data: Option<&[u8]>) -> Result<()> {
         match self {
             Self::Sha2_256(tree) => tree.assert_root_contains(hash_data),
         }
@@ -1706,6 +1718,14 @@ pub struct MerkleStream<T> {
     pos: usize,
 }
 
+impl<T: HeaderAccess> MerkleStream<T> {
+    /// Asserts that the root merkle node contains the integrity value given by the inner stream.
+    pub fn assert_root_integrity(&mut self) -> Result<()> {
+        let hash_data = self.trailered.inner.integrity();
+        self.tree.assert_root_contains(hash_data)
+    }
+}
+
 impl<T: Read + Seek> MerkleStream<T> {
     /// Reads a `MerkleTree` from the end of the given stream and returns a stream which uses it.
     pub fn new(inner: T) -> Result<MerkleStream<T>> {
@@ -1797,6 +1817,10 @@ impl<T: HeaderAccess> HeaderAccess for MerkleStream<T> {
     fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> crate::Result<()> {
         self.trailered.inner.add_readcap_for(owner, key)
     }
+
+    fn integrity(&self) -> Option<&[u8]> {
+        self.trailered.inner.integrity()
+    }
 }
 
 // A stream which encrypts all data written to it and decrypts all data read from it.
@@ -1964,6 +1988,10 @@ impl<T: HeaderAccess> HeaderAccess for SecretStream<T> {
     fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> crate::Result<()> {
         self.inner.add_readcap_for(owner, key)
     }
+
+    fn integrity(&self) -> Option<&[u8]> {
+        self.inner.integrity()
+    }
 }
 
 impl<T: Read + Write + Seek + HeaderAccess> Block for SecretStream<T> {}
@@ -2609,7 +2637,7 @@ mod tests {
         let mut inner = SectoredBuf::new()
             .try_compose(
                 SecretStream::new(key)
-                    .try_compose(SectoredCursor::new([0u8; SECT_SZ * SECT_CT], SECT_SZ))
+                    .try_compose(SectoredCursor::new(Vec::new(), SECT_SZ))
                     .expect("compose for inner failed"),
             )
             .expect("compose with sectored buffer failed");

+ 5 - 114
crates/btlib/src/crypto/tpm.rs

@@ -1491,126 +1491,17 @@ impl HasResponseCode for TSS2_RC {
 
 #[cfg(test)]
 mod test {
+    use crate::test_helpers::SwtpmHarness;
+
     use super::*;
 
     use ctor::ctor;
-    use nix::{
-        sys::signal::{self, Signal},
-        unistd::Pid,
-    };
-    use std::{
-        fs::File,
-        process::{Command, ExitStatus, Stdio},
-        sync::atomic::{AtomicU16, Ordering},
-    };
-    use tempdir::TempDir;
+    use std::fs::File;
     use tss_esapi::{
         interface_types::ecc::EccCurve,
         structures::{EccPoint, EccScheme, KeyDerivationFunctionScheme, PublicEccParameters},
-        tcti_ldr::NetworkTPMConfig,
     };
 
-    trait ExitStatusExt {
-        fn success_or_err(&self) -> Result<()>;
-    }
-
-    impl ExitStatusExt for ExitStatus {
-        fn success_or_err(&self) -> Result<()> {
-            match self.success() {
-                true => Ok(()),
-                false => Err(Error::custom("ExitCode was not successful")),
-            }
-        }
-    }
-
-    struct SwtpmHarness {
-        dir: TempDir,
-        port: u16,
-        state_path: PathBuf,
-        pid_path: PathBuf,
-    }
-
-    impl SwtpmHarness {
-        const HOST: &'static str = "127.0.0.1";
-
-        fn new() -> Result<SwtpmHarness> {
-            static PORT: AtomicU16 = AtomicU16::new(21901);
-            let port = PORT.fetch_add(2, Ordering::SeqCst);
-            let ctrl_port = port + 1;
-            let dir = TempDir::new(format!("btnode.{port}").as_str())?;
-            let dir_path = dir.path();
-            let dir_path_display = dir_path.display();
-            let conf_path = dir_path.join("swtpm_setup.conf");
-            let state_path = dir_path.join("state.bt");
-            let pid_path = dir_path.join("swtpm.pid");
-            let addr = Self::HOST;
-            std::fs::write(
-                &conf_path,
-                r#"# Program invoked for creating certificates
-create_certs_tool= /usr/bin/swtpm_localca
-# Comma-separated list (no spaces) of PCR banks to activate by default
-active_pcr_banks = sha256
-"#,
-            )?;
-            Command::new("swtpm_setup")
-                .args([
-                    "--tpm2",
-                    "--config",
-                    conf_path.to_str().unwrap(),
-                    "--tpm-state",
-                    format!("dir://{dir_path_display}").as_str(),
-                ])
-                .stdout(Stdio::null())
-                .status()?
-                .success_or_err()?;
-            Command::new("swtpm")
-                .args([
-                    "socket",
-                    "--daemon",
-                    "--tpm2",
-                    "--server",
-                    format!("type=tcp,port={port},bindaddr={addr}").as_str(),
-                    "--ctrl",
-                    format!("type=tcp,port={ctrl_port},bindaddr={addr}").as_str(),
-                    "--log",
-                    format!("file={dir_path_display}/log.txt,level=5").as_str(),
-                    "--flags",
-                    "not-need-init,startup-clear",
-                    "--tpmstate",
-                    format!("dir={dir_path_display}").as_str(),
-                    "--pid",
-                    format!("file={}", pid_path.display()).as_str(),
-                ])
-                .status()?
-                .success_or_err()?;
-            Ok(SwtpmHarness {
-                dir,
-                port,
-                state_path,
-                pid_path,
-            })
-        }
-
-        fn context(&self) -> Result<Context> {
-            let config_string = format!("host={},port={}", Self::HOST, self.port);
-            let config = NetworkTPMConfig::from_str(config_string.as_str())?;
-            Ok(Context::new(TctiNameConf::Swtpm(config))?)
-        }
-
-        fn state_path(&self) -> &Path {
-            &self.state_path
-        }
-    }
-
-    impl Drop for SwtpmHarness {
-        fn drop(&mut self) {
-            let pid_str = std::fs::read_to_string(&self.pid_path).unwrap();
-            let pid_int = pid_str.parse::<i32>().unwrap();
-            let pid = Pid::from_raw(pid_int);
-            signal::kill(pid, Signal::SIGKILL).unwrap();
-        }
-    }
-
     #[ctor]
     fn ctor() {
         env_logger::init();
@@ -1691,7 +1582,7 @@ active_pcr_banks = sha256
     #[test]
     fn tpm_cred_store_new() -> Result<()> {
         let harness = SwtpmHarness::new()?;
-        let cookie_path = harness.dir.path().join("cookie.bin");
+        let cookie_path = harness.dir_path().join("cookie.bin");
         let store = TpmCredStore::new(harness.context()?, &cookie_path)?;
         let cookie = File::open(&cookie_path)?;
         let metadata = cookie.metadata()?;
@@ -1705,7 +1596,7 @@ active_pcr_banks = sha256
     #[test]
     fn gen_creds() -> Result<()> {
         let harness = SwtpmHarness::new()?;
-        let cookie_path = harness.dir.path().join("cookie.bin");
+        let cookie_path = harness.dir_path().join("cookie.bin");
         let store = TpmCredStore::new(harness.context()?, &cookie_path)?;
         store.gen_node_creds()?;
         Ok(())

+ 119 - 26
crates/btlib/src/lib.rs

@@ -19,11 +19,11 @@ use brotli::{CompressorWriter, Decompressor};
 use btserde::{self, read_from, write_to};
 mod crypto;
 use crypto::{
-    AsymKeyPub, Ciphertext, CredsPriv, Decrypter, Encrypter, EncrypterExt, Hash, HashKind,
-    MerkleStream, SecretStream, Sign, Signature, Signer, SymKey,
+    AsymKeyPub, Ciphertext, Creds, Decrypter, Encrypter, EncrypterExt, Hash, HashKind,
+    MerkleStream, SecretStream, Sign, Signature, Signer, SymKey, SymKeyKind,
 };
 
-use log::error;
+use log::{error, warn};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_big_array::BigArray;
 use std::{
@@ -40,7 +40,7 @@ use std::{
 };
 
 #[derive(Debug)]
-enum Error {
+pub enum Error {
     MissingWritecap,
     Io(std::io::Error),
     Serde(btserde::Error),
@@ -177,9 +177,11 @@ impl<T, U: Decompose<T>, S: TryCompose<T, U, Error = Infallible>> Compose<T, U>
     }
 }
 
-trait HeaderAccess {
+pub trait HeaderAccess {
     fn block_key(&self) -> Result<SymKey>;
     fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> Result<()>;
+    /// Returns the integrity value used to protect the contents of the block.
+    fn integrity(&self) -> Option<&[u8]>;
 }
 
 /// Extensions to the `Read` trait.
@@ -221,7 +223,7 @@ pub struct Header {
     readcaps: HashMap<Principal, Ciphertext<SymKey>>,
     writecap: Option<Writecap>,
     /// A hash which provides integrity for the contents of the block body.
-    integrity: Hash,
+    integrity: Option<Hash>,
 }
 
 #[derive(Serialize, Deserialize, Default)]
@@ -237,19 +239,35 @@ struct BlockStream<T, C> {
     creds: C,
 }
 
-impl<T: Read + Seek, C> BlockStream<T, C> {
+impl<T: Read + Seek, C: Creds> BlockStream<T, C> {
     fn new(inner: T, creds: C) -> Result<BlockStream<T, C>> {
-        let (trailered, trailer) = Trailered::new(inner)?;
+        let (trailered, trailer) = Trailered::<_, BlockTrailer>::new(inner)?;
+        let trailer = match trailer {
+            Some(trailer) => {
+                crypto::verify_header(&trailer.header, &trailer.sig)?;
+                trailer
+            }
+            None => {
+                let mut trailer = BlockTrailer::default();
+                let block_key = SymKey::generate(SymKeyKind::default())?;
+                trailer
+                    .header
+                    .readcaps
+                    .insert(creds.owner(), creds.ser_encrypt(&block_key)?);
+                // TODO: Insert writecap.
+                trailer
+            }
+        };
         Ok(BlockStream {
             trailered,
-            trailer: trailer.unwrap_or_default(),
+            trailer,
             header_buf: Vec::new(),
             creds,
         })
     }
 }
 
-impl<C> BlockStream<File, C> {
+impl<C: Creds> BlockStream<File, C> {
     fn open<P: AsRef<std::path::Path>>(path: P, creds: C) -> Result<BlockStream<File, C>> {
         let inner = OpenOptions::new().read(true).write(true).open(path)?;
         BlockStream::new(inner, creds)
@@ -272,7 +290,8 @@ impl<T: Write + Seek, C: Signer> Write for BlockStream<T, C> {
 impl<T: Write + Seek, C: Signer> WriteInteg for BlockStream<T, C> {
     fn flush_integ(&mut self, integrity: &[u8]) -> io::Result<()> {
         let header = &mut self.trailer.header;
-        header.integrity.as_mut().copy_from_slice(integrity);
+        let integ = header.integrity.get_or_insert_with(Hash::default);
+        integ.as_mut().copy_from_slice(integrity);
         self.header_buf.clear();
         write_to(&header, &mut self.header_buf).box_err()?;
         self.trailer.sig = self
@@ -314,6 +333,14 @@ impl<T, C: Decrypter + Owned> HeaderAccess for BlockStream<T, C> {
         self.trailer.header.readcaps.insert(owner, readcap);
         Ok(())
     }
+
+    fn integrity(&self) -> Option<&[u8]> {
+        self.trailer
+            .header
+            .integrity
+            .as_ref()
+            .map(|hash| hash.as_ref())
+    }
 }
 
 struct BlockOpenOptions<T, C> {
@@ -364,11 +391,12 @@ impl<T, C> BlockOpenOptions<T, C> {
     }
 }
 
-impl<T: Read + Write + Seek + 'static, C: CredsPriv + Owned + 'static> BlockOpenOptions<T, C> {
+impl<T: Read + Write + Seek + 'static, C: Creds + 'static> BlockOpenOptions<T, C> {
     fn open(self) -> Result<Box<dyn Block>> {
         let stream = BlockStream::new(self.inner, self.creds)?;
         let block_key = stream.block_key()?;
-        let stream = MerkleStream::new(stream)?;
+        let mut stream = MerkleStream::new(stream)?;
+        stream.assert_root_integrity()?;
         if self.encrypt {
             let stream = SecretStream::new(block_key).try_compose(stream)?;
             let stream = SectoredBuf::new().try_compose(stream)?;
@@ -619,19 +647,15 @@ impl<T> SectoredBuf<T> {
 impl<T: Read + Seek> SectoredBuf<T> {
     /// Fills the internal buffer by reading from the inner stream at the current position
     /// and updates `self.buf_start` with the position read from.
-    fn fill_internal_buf(&mut self) -> io::Result<usize> {
+    fn fill_internal_buf(&mut self) -> Result<usize> {
         self.buf_start = self.inner.stream_position()?.try_into().box_err()?;
         let read_bytes = if self.buf_start < self.len {
             let read_bytes = self.inner.fill_buf(&mut self.buf)?;
             if read_bytes < self.buf.len() {
-                return Err(io::Error::new(
-                    io::ErrorKind::Other,
-                    format!(
-                        "Failed to fill SectoredBuf.buf. Expected {} bytes, got {}.",
-                        self.buf.len(),
-                        read_bytes
-                    ),
-                ));
+                return Err(Error::IncorrectSize {
+                    expected: self.buf.len(),
+                    actual: read_bytes,
+                });
             }
             read_bytes
         } else {
@@ -670,10 +694,15 @@ impl<T: Sectored + Read + Seek> TryCompose<T, SectoredBuf<T>> for SectoredBuf<()
         sectored.buf.resize(sect_sz, 0);
         let len_stored = match sectored.fill_internal_buf() {
             Ok(bytes_read) => bytes_read >= Self::RESERVED,
-            Err(err) => {
-                error!("SectoredBuf::fill_internal_buf returned an error: {}", err);
+            Err(Error::IncorrectSize { actual, expected }) => {
+                if actual > 0 {
+                    return Err(Error::IncorrectSize { expected, actual });
+                }
+                // When the actual size was 0 that just means the inner stream was empty, which is
+                // not an error.
                 false
             }
+            Err(err) => return Err(err),
         };
         if len_stored {
             if let Ok(len) = read_from::<u64, _>(&mut sectored.buf.as_slice()) {
@@ -756,6 +785,7 @@ impl<T: Seek + Read + Write> Write for SectoredBuf<T> {
         write_to(&len, &mut self.buf.as_mut_slice()).box_err()?;
         self.inner.seek(SeekFrom::Start(0))?;
         self.inner.write_all(&self.buf)?;
+        self.inner.flush()?;
 
         // Seek to the next position.
         self.inner.seek(SeekFrom::Start(seek_to))?;
@@ -786,7 +816,7 @@ impl<T: Read + Seek> Read for SectoredBuf<T> {
                 let byte_ct = match self.fill_internal_buf() {
                     Ok(byte_ct) => byte_ct,
                     Err(err) => {
-                        error!("SectoredBuf::full_internal_buf returned an error: {}", err);
+                        warn!("SectoredBuf::full_internal_buf returned an error: {}", err);
                         break;
                     }
                 };
@@ -847,6 +877,10 @@ impl<T: HeaderAccess> HeaderAccess for SectoredBuf<T> {
     fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> Result<()> {
         self.inner.add_readcap_for(owner, key)
     }
+
+    fn integrity(&self) -> Option<&[u8]> {
+        self.inner.integrity()
+    }
 }
 
 impl<T: Read + Write + Seek + HeaderAccess> Block for SectoredBuf<T> {}
@@ -946,7 +980,7 @@ impl FragmentRecord {
 
 /// An identifier for a security principal, which is any entity that can be authenticated.
 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hashable, Clone, Default)]
-struct Principal(Hash);
+pub struct Principal(Hash);
 
 impl Principal {
     fn kind(&self) -> HashKind {
@@ -1153,7 +1187,10 @@ struct FragmentSerial(u32);
 mod tests {
     use std::io::Cursor;
 
+    use crate::crypto::{tpm::TpmCredStore, CredStore};
+
     use super::*;
+    use tempdir::TempDir;
     use test_helpers::*;
 
     fn path_from_str_test_case(
@@ -1603,4 +1640,60 @@ mod tests {
 
         assert_eq!(EXPECTED, actual);
     }
+
+    #[test]
+    fn block_can_create_empty() {
+        let harness = SwtpmHarness::new().expect("failed to start swtpm");
+        let context = harness.context().expect("failed to retrieve context");
+        let cred_store = TpmCredStore::new(context, harness.state_path())
+            .expect("failed to create TpmCredStore");
+        let creds = cred_store.node_creds().expect("failed to get node creds");
+        BlockOpenOptions::new()
+            .with_inner(Cursor::new(Vec::<u8>::new()))
+            .with_creds(creds)
+            .with_encrypt(true)
+            .open()
+            .expect("failed to open block");
+    }
+
+    #[test]
+    fn block_contents_persisted() {
+        const EXPECTED: &[u8] = b"Silly sordid sulking sultans.";
+        let temp_dir = TempDir::new("btlib").expect("failed to create temp dir");
+        let file_path = temp_dir.path().join("test.blk").to_owned();
+        let harness = SwtpmHarness::new().expect("failed to start swtpm");
+        let context = harness.context().expect("failed to retrieve context");
+        let cred_store = TpmCredStore::new(context, harness.state_path())
+            .expect("failed to create TpmCredStore");
+        let creds = cred_store.node_creds().expect("failed to get node creds");
+        {
+            let file = OpenOptions::new()
+                .create_new(true)
+                .write(true)
+                .read(true)
+                .open(&file_path)
+                .expect("failed to open file");
+            let mut block = BlockOpenOptions::new()
+                .with_inner(file)
+                .with_creds(creds.clone())
+                .with_encrypt(true)
+                .open()
+                .expect("failed to open block");
+            block.write(EXPECTED).expect("failed to write");
+            block.flush().expect("flush failed");
+        }
+        let file = OpenOptions::new()
+            .read(true)
+            .open(&file_path)
+            .expect("failed to reopen file");
+        let mut block = BlockOpenOptions::new()
+            .with_inner(file)
+            .with_creds(creds)
+            .with_encrypt(true)
+            .open()
+            .expect("failed to reopen block");
+        let mut actual = [0u8; EXPECTED.len()];
+        block.read(&mut actual).expect("read failed");
+        assert_eq!(EXPECTED, actual);
+    }
 }

+ 122 - 2
crates/btlib/src/test_helpers.rs

@@ -1,12 +1,25 @@
 /// Test data and functions to help with testing.
 use super::*;
 use btserde::{Error, Result};
-use crypto::*;
+use crypto::{self, *};
+use nix::{
+    sys::signal::{self, Signal},
+    unistd::Pid,
+};
 use std::{
     cell::RefCell,
     fmt::Write as FmtWrite,
     fs::File,
     io::{Cursor, Write},
+    path::PathBuf,
+    process::{Command, ExitStatus, Stdio},
+    str::FromStr,
+    sync::atomic::{AtomicU16, Ordering},
+};
+use tempdir::TempDir;
+use tss_esapi::{
+    tcti_ldr::{NetworkTPMConfig, TctiNameConf},
+    Context,
 };
 
 pub const PRINCIPAL: [u8; 32] = [
@@ -186,7 +199,7 @@ pub(crate) fn make_block_with(readcap: Readcap) -> Box<dyn Block> {
         path: make_path_with_owner(root_writecap.issued_to.clone(), vec!["apps", "verse"]),
         readcaps,
         writecap: Some(writecap),
-        integrity: Hash::Sha2_256([0u8; HashKind::Sha2_256.len()]),
+        integrity: Some(Hash::Sha2_256([0u8; HashKind::Sha2_256.len()])),
     };
     let sig = Signature::copy_from(Sign::RSA_PSS_3072_SHA_256, &SIGNATURE);
     let mut stream =
@@ -488,3 +501,110 @@ impl<T: FromVec> Seek for SectoredCursor<T> {
         self.cursor.seek(pos)
     }
 }
+
+trait ExitStatusExt {
+    fn success_or_err(&self) -> Result<()>;
+}
+
+impl ExitStatusExt for ExitStatus {
+    fn success_or_err(&self) -> btserde::Result<()> {
+        match self.success() {
+            true => Ok(()),
+            false => Err(btserde::Error::Message(
+                "ExitCode was not successful".to_string(),
+            )),
+        }
+    }
+}
+
+pub(crate) struct SwtpmHarness {
+    dir: TempDir,
+    port: u16,
+    state_path: PathBuf,
+    pid_path: PathBuf,
+}
+
+impl SwtpmHarness {
+    const HOST: &'static str = "127.0.0.1";
+
+    pub(crate) fn new() -> crypto::Result<SwtpmHarness> {
+        static PORT: AtomicU16 = AtomicU16::new(21901);
+        let port = PORT.fetch_add(2, Ordering::SeqCst);
+        let ctrl_port = port + 1;
+        let dir = TempDir::new(format!("btnode.{port}").as_str())?;
+        let dir_path = dir.path();
+        let dir_path_display = dir_path.display();
+        let conf_path = dir_path.join("swtpm_setup.conf");
+        let state_path = dir_path.join("state.bt");
+        let pid_path = dir_path.join("swtpm.pid");
+        let addr = Self::HOST;
+        std::fs::write(
+            &conf_path,
+            r#"# Program invoked for creating certificates
+create_certs_tool= /usr/bin/swtpm_localca
+# Comma-separated list (no spaces) of PCR banks to activate by default
+active_pcr_banks = sha256
+"#,
+        )?;
+        Command::new("swtpm_setup")
+            .args([
+                "--tpm2",
+                "--config",
+                conf_path.to_str().unwrap(),
+                "--tpm-state",
+                format!("dir://{dir_path_display}").as_str(),
+            ])
+            .stdout(Stdio::null())
+            .status()?
+            .success_or_err()?;
+        Command::new("swtpm")
+            .args([
+                "socket",
+                "--daemon",
+                "--tpm2",
+                "--server",
+                format!("type=tcp,port={port},bindaddr={addr}").as_str(),
+                "--ctrl",
+                format!("type=tcp,port={ctrl_port},bindaddr={addr}").as_str(),
+                "--log",
+                format!("file={dir_path_display}/log.txt,level=5").as_str(),
+                "--flags",
+                "not-need-init,startup-clear",
+                "--tpmstate",
+                format!("dir={dir_path_display}").as_str(),
+                "--pid",
+                format!("file={}", pid_path.display()).as_str(),
+            ])
+            .status()?
+            .success_or_err()?;
+        Ok(SwtpmHarness {
+            dir,
+            port,
+            state_path,
+            pid_path,
+        })
+    }
+
+    pub(crate) fn context(&self) -> crypto::Result<Context> {
+        let config_string = format!("host={},port={}", Self::HOST, self.port);
+        let config = NetworkTPMConfig::from_str(config_string.as_str())?;
+        Ok(Context::new(TctiNameConf::Swtpm(config))?)
+    }
+
+    pub(crate) fn dir_path(&self) -> &std::path::Path {
+        self.dir.path()
+    }
+
+    pub(crate) fn state_path(&self) -> &std::path::Path {
+        &self.state_path
+    }
+}
+
+impl Drop for SwtpmHarness {
+    fn drop(&mut self) {
+        let pid_str = std::fs::read_to_string(&self.pid_path).unwrap();
+        let pid_int = pid_str.parse::<i32>().unwrap();
+        let pid = Pid::from_raw(pid_int);
+        signal::kill(pid, Signal::SIGKILL).unwrap();
+    }
+}

+ 0 - 0
crates/btlib/README.md → crates/btnode/README.md