Ver Fonte

- Fixed bug where writing using different credentials didn't replace writecap.
- Fixed bug where hash of BlockMeta was non-deterministic.

Matthew Carr há 2 anos atrás
pai
commit
d1c53f7676

+ 21 - 3
crates/btlib/TODO.txt

@@ -1,28 +1,46 @@
+# Format: - <task ID>, <task points>, <created by user>, <created on commit>, <finished by user>, <finished on commit>
+- 1
 Fix BufSectored so it doesn't have to write to the first sector every flush.
 
+- 2
 Track position and dirty-ness in Trailered.
 
+- 3
 Implement a stream which is both Read and Write and which can transparently compress and decompress
 data written to and read from it.
 
+- 4
 Remove TryCompose?
 
+- 5
 Move crypto::{encrypt, decrypt} into corresponding {EncrypterExt, DecrypterExt}.
 
+- 6
 Create an enum to eliminate the use of Block trait objects?
 
+- 7
 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 u64 for portability.
 
+- 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
 Create a struct which digests data written to it before passing it to an underlying Write.
 
-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).
-
+- 11
 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
+Change the Hash enum so it contains structs for each hash type. Unify these structs with the node
+structs used in the VecMerkleTree.

+ 112 - 63
crates/btlib/src/crypto/mod.rs

@@ -3,9 +3,9 @@
 pub mod tpm;
 
 use crate::{
-    fmt, io, BigArray, Block, BlockMetaBody, BoxInIoErr, Decompose, Deserialize, Display, Epoch,
-    Formatter, Hashable, MetaAccess, Path, Principal, Principaled, Read, Sectored, Seek, Serialize,
-    Trailered, TryCompose, Write, WriteInteg, Writecap, SECTOR_SZ_DEFAULT,
+    fmt, io, BigArray, Block, BlockMeta, BlockMetaBody, BoxInIoErr, Decompose, Deserialize,
+    Display, Epoch, Formatter, Hashable, MetaAccess, Path, Principal, Principaled, Read, Sectored,
+    Seek, Serialize, Trailered, TryCompose, Write, WriteInteg, Writecap, SECTOR_SZ_DEFAULT,
 };
 
 use btserde::{self, from_vec, to_vec, write_to};
@@ -74,12 +74,28 @@ pub enum Error {
     BlockNotEncrypted,
     InvalidHashFormat,
     InvalidSignature,
-    IncorrectSize { expected: usize, actual: usize },
-    IndexOutOfBounds { index: usize, limit: usize },
-    IndivisibleSize { divisor: usize, actual: usize },
-    InvalidOffset { actual: usize, limit: usize },
+    IncorrectSize {
+        expected: usize,
+        actual: usize,
+    },
+    IndexOutOfBounds {
+        index: usize,
+        limit: usize,
+    },
+    IndivisibleSize {
+        divisor: usize,
+        actual: usize,
+    },
+    InvalidOffset {
+        actual: usize,
+        limit: usize,
+    },
     HashCmpFailure,
     RootHashNotVerified,
+    SignatureMismatch {
+        actual: Principal,
+        expected: Principal,
+    },
     WritecapAuthzErr(WritecapAuthzErr),
     Serde(btserde::Error),
     Io(std::io::Error),
@@ -122,6 +138,10 @@ impl Display for Error {
             ),
             Error::HashCmpFailure => write!(f, "hash data are not equal"),
             Error::RootHashNotVerified => write!(f, "root hash is not verified"),
+            Error::SignatureMismatch { actual, expected } => write!(
+                f,
+                "expected a signature from {expected} but found one from {actual}"
+            ),
             Error::WritecapAuthzErr(err) => err.fmt(f),
             Error::Serde(err) => err.fmt(f),
             Error::Io(err) => err.fmt(f),
@@ -191,7 +211,18 @@ fn rand_vec(len: usize) -> Result<Vec<u8>> {
 }
 
 /// A cryptographic hash.
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hashable, Clone, EnumDiscriminants)]
+#[derive(
+    Debug,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    Hashable,
+    Clone,
+    EnumDiscriminants,
+    PartialOrd,
+    Ord,
+)]
 #[strum_discriminants(derive(EnumString, Display, Serialize, Deserialize))]
 #[strum_discriminants(name(HashKind))]
 pub enum Hash {
@@ -2121,13 +2152,26 @@ pub(crate) fn sign_header<K: Signer>(header: &BlockMetaBody, signer: &K) -> Resu
     signer.sign(std::iter::once(header.as_slice()))
 }
 
-pub(crate) fn verify_header(header: &BlockMetaBody, sig: &Signature) -> Result<()> {
-    let writecap = header
-        .writecap
-        .as_ref()
-        .ok_or(crate::Error::MissingWritecap)?;
-    verify_writecap(writecap, &header.path)?;
-    header.signing_key.ser_verify(&header, sig.as_slice())
+impl BlockMeta {
+    /// Validates that this metadata struct contains a valid writecap, that this writecap is
+    /// permitted to write to the path of this block and that the signature in this metadata struct
+    /// is valid and matches the key the writecap was issued to.
+    pub fn assert_valid(&self) -> Result<()> {
+        let body = &self.body;
+        let writecap = body
+            .writecap
+            .as_ref()
+            .ok_or(crate::Error::MissingWritecap)?;
+        writecap.assert_valid_for(&body.path)?;
+        let signed_by = body.signing_key.principal();
+        if writecap.issued_to != signed_by {
+            return Err(Error::SignatureMismatch {
+                actual: signed_by,
+                expected: writecap.issued_to.clone(),
+            });
+        }
+        body.signing_key.ser_verify(&body, self.sig.as_slice())
+    }
 }
 
 #[derive(Serialize)]
@@ -2151,7 +2195,7 @@ impl<'a> From<&'a Writecap> for WritecapSigInput<'a> {
 
 pub(crate) fn sign_writecap<K: Signer>(writecap: &mut Writecap, priv_key: &K) -> Result<()> {
     let sig_input = to_vec(&WritecapSigInput::from(&*writecap))?;
-    writecap.signature = priv_key.sign([sig_input.as_slice()].into_iter())?;
+    writecap.signature = priv_key.sign(std::iter::once(sig_input.as_slice()))?;
     Ok(())
 }
 
@@ -2174,53 +2218,56 @@ pub enum WritecapAuthzErr {
     ChainTooLong(usize),
 }
 
-/// Verifies that the given `Writecap` actually grants permission to write to the given `Path`.
-pub(crate) fn verify_writecap(mut writecap: &Writecap, path: &Path) -> Result<()> {
-    const CHAIN_LEN_LIMIT: usize = 256;
-    let mut prev: Option<&Writecap> = None;
-    let mut sig_input_buf = Vec::new();
-    let now = Epoch::now();
-    for _ in 0..CHAIN_LEN_LIMIT {
-        if !writecap.path.contains(path) {
-            return Err(WritecapAuthzErr::UnauthorizedPath.into());
-        }
-        if writecap.expires <= now {
-            return Err(WritecapAuthzErr::Expired.into());
-        }
-        if let Some(prev) = &prev {
-            if prev
-                .signing_key
-                .principal_of_kind(writecap.issued_to.kind())
-                != writecap.issued_to
-            {
-                return Err(WritecapAuthzErr::NotChained.into());
+impl Writecap {
+    /// Verifies that the given `Writecap` actually grants permission to write to the given `Path`.
+    pub(crate) fn assert_valid_for(&self, path: &Path) -> Result<()> {
+        let mut writecap = self;
+        const CHAIN_LEN_LIMIT: usize = 256;
+        let mut prev: Option<&Writecap> = None;
+        let mut sig_input_buf = Vec::new();
+        let now = Epoch::now();
+        for _ in 0..CHAIN_LEN_LIMIT {
+            if !writecap.path.contains(path) {
+                return Err(WritecapAuthzErr::UnauthorizedPath.into());
             }
-        }
-        let sig_input = WritecapSigInput::from(writecap);
-        sig_input_buf.clear();
-        write_to(&sig_input, &mut sig_input_buf)
-            .map_err(|e| WritecapAuthzErr::Serde(e.to_string()))?;
-        writecap.signing_key.verify(
-            std::iter::once(sig_input_buf.as_slice()),
-            writecap.signature.as_slice(),
-        )?;
-        match &writecap.next {
-            Some(next) => {
-                prev = Some(writecap);
-                writecap = next;
+            if writecap.expires <= now {
+                return Err(WritecapAuthzErr::Expired.into());
             }
-            None => {
-                // We're at the root key. As long as the signer of this writecap is the owner of
-                // the path, then the writecap is valid.
-                if writecap.signing_key.principal_of_kind(path.root.kind()) == path.root {
-                    return Ok(());
-                } else {
-                    return Err(WritecapAuthzErr::RootDoesNotOwnPath.into());
+            if let Some(prev) = &prev {
+                if prev
+                    .signing_key
+                    .principal_of_kind(writecap.issued_to.kind())
+                    != writecap.issued_to
+                {
+                    return Err(WritecapAuthzErr::NotChained.into());
+                }
+            }
+            let sig_input = WritecapSigInput::from(writecap);
+            sig_input_buf.clear();
+            write_to(&sig_input, &mut sig_input_buf)
+                .map_err(|e| WritecapAuthzErr::Serde(e.to_string()))?;
+            writecap.signing_key.verify(
+                std::iter::once(sig_input_buf.as_slice()),
+                writecap.signature.as_slice(),
+            )?;
+            match &writecap.next {
+                Some(next) => {
+                    prev = Some(writecap);
+                    writecap = next;
+                }
+                None => {
+                    // We're at the root key. As long as the signer of this writecap is the owner of
+                    // the path, then the writecap is valid.
+                    if writecap.signing_key.principal_of_kind(path.root.kind()) == path.root {
+                        return Ok(());
+                    } else {
+                        return Err(WritecapAuthzErr::RootDoesNotOwnPath.into());
+                    }
                 }
             }
         }
+        Err(WritecapAuthzErr::ChainTooLong(CHAIN_LEN_LIMIT).into())
     }
-    Err(WritecapAuthzErr::ChainTooLong(CHAIN_LEN_LIMIT).into())
 }
 
 #[cfg(test)]
@@ -2272,14 +2319,16 @@ mod tests {
     #[test]
     fn verify_writecap_valid() {
         let writecap = make_writecap(vec!["apps", "verse"]);
-        verify_writecap(&writecap, &writecap.path).expect("failed to verify writecap");
+        writecap
+            .assert_valid_for(&writecap.path)
+            .expect("failed to verify writecap");
     }
 
     #[test]
     fn verify_writecap_invalid_signature() -> Result<()> {
         let mut writecap = make_writecap(vec!["apps", "verse"]);
         writecap.signature = Signature::empty(Sign::RSA_PSS_3072_SHA_256);
-        let result = verify_writecap(&writecap, &writecap.path);
+        let result = writecap.assert_valid_for(&writecap.path);
         if let Err(Error::InvalidSignature) = result {
             Ok(())
         } else {
@@ -2306,7 +2355,7 @@ mod tests {
         path.components.pop();
         // `path` is now a superpath of `writecap.path`, thus the writecap is not authorized to
         // write to it.
-        let result = verify_writecap(&writecap, &path);
+        let result = writecap.assert_valid_for(&path);
         assert_authz_err(WritecapAuthzErr::UnauthorizedPath, result)
     }
 
@@ -2314,7 +2363,7 @@ mod tests {
     fn verify_writecap_invalid_expired() -> Result<()> {
         let mut writecap = make_writecap(vec!["apps", "verse"]);
         writecap.expires = Epoch::now() - Duration::from_secs(1);
-        let result = verify_writecap(&writecap, &writecap.path);
+        let result = writecap.assert_valid_for(&writecap.path);
         assert_authz_err(WritecapAuthzErr::Expired, result)
     }
 
@@ -2330,7 +2379,7 @@ mod tests {
             node_principal,
             vec!["apps", "contacts"],
         );
-        let result = verify_writecap(&writecap, &writecap.path);
+        let result = writecap.assert_valid_for(&writecap.path);
         assert_authz_err(WritecapAuthzErr::NotChained, result)
     }
 
@@ -2347,7 +2396,7 @@ mod tests {
             node_principal,
             vec!["apps", "contacts"],
         );
-        let result = verify_writecap(&writecap, &writecap.path);
+        let result = writecap.assert_valid_for(&writecap.path);
         assert_authz_err(WritecapAuthzErr::RootDoesNotOwnPath, result)
     }
 

+ 6 - 2
crates/btlib/src/crypto/tpm.rs

@@ -1760,7 +1760,9 @@ mod test {
             root: root_creds.principal(),
             components: Vec::new(),
         };
-        verify_writecap(&writecap, &path).expect("failed to verify root writecap");
+        writecap
+            .assert_valid_for(&path)
+            .expect("failed to verify root writecap");
     }
 
     #[test]
@@ -1781,7 +1783,9 @@ mod test {
                 Epoch::now() + Duration::from_secs(3600),
             )
             .expect("failed to issue writecap");
-        verify_writecap(&writecap, &path).expect("failed to verify writecap");
+        writecap
+            .assert_valid_for(&path)
+            .expect("failed to verify writecap");
     }
 
     #[test]

+ 201 - 45
crates/btlib/src/lib.rs

@@ -25,7 +25,7 @@ use log::{error, warn};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_big_array::BigArray;
 use std::{
-    collections::HashMap,
+    collections::{BTreeMap, HashMap},
     convert::{Infallible, TryFrom},
     fmt::{self, Display, Formatter},
     hash::Hash as Hashable,
@@ -223,7 +223,7 @@ impl<T: Read> ReadExt for T {}
 #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
 pub struct BlockMetaBody {
     path: Path,
-    readcaps: HashMap<Principal, Ciphertext<SymKey>>,
+    readcaps: BTreeMap<Principal, Ciphertext<SymKey>>,
     writecap: Option<Writecap>,
     /// A hash which provides integrity for the contents of the block body.
     integrity: Option<Hash>,
@@ -235,7 +235,7 @@ impl BlockMetaBody {
     fn new<C: Creds>(creds: &C) -> BlockMetaBody {
         BlockMetaBody {
             path: Path::default(),
-            readcaps: HashMap::new(),
+            readcaps: BTreeMap::new(),
             writecap: creds.writecap().map(|e| e.to_owned()),
             integrity: None,
             signing_key: creds.public_sign().to_owned(),
@@ -269,8 +269,11 @@ impl<T: Read + Seek, C: Creds> BlockStream<T, C> {
     fn new(inner: T, creds: C) -> Result<BlockStream<T, C>> {
         let (trailered, trailer) = Trailered::<_, BlockMeta>::new(inner)?;
         let trailer = match trailer {
-            Some(trailer) => {
-                crypto::verify_header(&trailer.body, &trailer.sig)?;
+            Some(mut trailer) => {
+                trailer.assert_valid()?;
+                // We need to use the writecap and signing_key provided by the current credentials.
+                trailer.body.writecap = creds.writecap().map(|e| e.to_owned());
+                trailer.body.signing_key = creds.public_sign().to_owned();
                 trailer
             }
             None => {
@@ -1001,7 +1004,9 @@ 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)]
+#[derive(
+    Debug, PartialEq, Eq, Serialize, Deserialize, Hashable, Clone, Default, PartialOrd, Ord,
+)]
 pub struct Principal(Hash);
 
 impl Principal {
@@ -1010,6 +1015,12 @@ impl Principal {
     }
 }
 
+impl Display for Principal {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
 /// Trait for types which are owned by a `Principal`.
 trait Principaled {
     /// Returns the `Principal` that owns `self`, using the given hash algorithm.
@@ -1207,9 +1218,12 @@ struct FragmentSerial(u32);
 
 #[cfg(test)]
 mod tests {
-    use std::{fs::OpenOptions, io::Cursor};
+    use std::{fs::OpenOptions, io::Cursor, path::PathBuf};
 
-    use crate::crypto::{tpm::TpmCredStore, CredStore, CredsPriv};
+    use crate::crypto::{
+        tpm::{TpmCredStore, TpmCreds},
+        ConcreteCreds, CredStore, CredsPriv,
+    };
 
     use super::*;
     use tempdir::TempDir;
@@ -1678,57 +1692,199 @@ mod tests {
             .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 root_creds = cred_store
-            .gen_root_creds("(1337Prestidigitation7331)")
-            .expect("failed to get root creds");
-        let mut node_creds = cred_store.node_creds().expect("failed to get node creds");
-        let writecap = root_creds
-            .issue_writecap(
-                node_creds.principal(),
-                vec!["nodes".to_string(), "phone".to_string()],
-                Epoch::now() + Duration::from_secs(3600),
-            )
-            .expect("failed to issue writecap");
-        let path = writecap.path.clone();
-        node_creds.set_writecap(writecap);
-        {
+    struct BlockTestCase {
+        root_creds: TpmCreds,
+        node_creds: TpmCreds,
+        swtpm: SwtpmHarness,
+        temp_dir: TempDir,
+    }
+
+    impl BlockTestCase {
+        const ROOT_PASSWORD: &'static str = "(1337Prestidigitation7331)";
+
+        fn new() -> BlockTestCase {
+            let temp_dir = TempDir::new("block_test").expect("failed to create temp dir");
+            let swtpm = SwtpmHarness::new().expect("failed to start swtpm");
+            let context = swtpm.context().expect("failed to retrieve context");
+            let cred_store = TpmCredStore::new(context, swtpm.state_path())
+                .expect("failed to create TpmCredStore");
+            let root_creds = cred_store
+                .gen_root_creds(Self::ROOT_PASSWORD)
+                .expect("failed to get root creds");
+            let mut node_creds = cred_store.node_creds().expect("failed to get node creds");
+            let writecap = root_creds
+                .issue_writecap(
+                    node_creds.principal(),
+                    vec!["nodes".to_string(), "phone".to_string()],
+                    Epoch::now() + Duration::from_secs(3600),
+                )
+                .expect("failed to issue writecap");
+            node_creds.set_writecap(writecap);
+            BlockTestCase {
+                temp_dir,
+                swtpm,
+                node_creds,
+                root_creds,
+            }
+        }
+
+        fn fs_path(&self, path: &crate::Path) -> PathBuf {
+            let mut fs_path = self.temp_dir.path().to_owned();
+            fs_path.extend(path.components.iter());
+            fs_path
+        }
+
+        fn open_new(&mut self, path: &crate::Path) -> Box<dyn Block> {
             let file = OpenOptions::new()
                 .create_new(true)
-                .write(true)
                 .read(true)
-                .open(&file_path)
+                .write(true)
+                .open(&self.fs_path(path))
                 .expect("failed to open file");
             let mut block = BlockOpenOptions::new()
                 .with_inner(file)
-                .with_creds(node_creds.clone())
+                .with_creds(self.node_creds.clone())
                 .with_encrypt(true)
                 .open()
                 .expect("failed to open block");
-            block.set_path(path);
+            block.set_path(self.node_creds.writecap().unwrap().path.clone());
+            block
+        }
+
+        fn open_existing(&mut self, path: &crate::Path) -> Box<dyn Block> {
+            let file = OpenOptions::new()
+                .read(true)
+                .write(true)
+                .open(&self.fs_path(path))
+                .expect("failed to reopen file");
+            BlockOpenOptions::new()
+                .with_inner(file)
+                .with_creds(self.node_creds.clone())
+                .with_encrypt(true)
+                .open()
+                .expect("failed to reopen block")
+        }
+    }
+
+    #[test]
+    fn block_contents_persisted() {
+        const EXPECTED: &[u8] = b"Silly sordid sulking sultans.";
+        const BLOCK_NAME: &'static str = "test.blk";
+
+        let mut case = BlockTestCase::new();
+        let path = Path {
+            root: case.root_creds.principal(),
+            components: vec!["test.blk".to_string()],
+        };
+        {
+            let mut block = case.open_new(&path);
             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(node_creds)
-            .with_encrypt(true)
-            .open()
-            .expect("failed to reopen block");
+        let mut block = case.open_existing(&path);
         let mut actual = [0u8; EXPECTED.len()];
         block.read(&mut actual).expect("read failed");
         assert_eq!(EXPECTED, actual);
     }
+
+    #[test]
+    fn block_write_twice() {
+        const EXPECTED: &[u8] = b"Cool callous calamitous colonels.";
+        const MID: usize = EXPECTED.len() / 2;
+        const BLOCK_NAME: &'static str = "test.blk";
+
+        let mut case = BlockTestCase::new();
+        let path = Path {
+            root: case.root_creds.principal(),
+            components: vec!["test.blk".to_string()],
+        };
+        {
+            let mut block = case.open_new(&path);
+            block.write(&EXPECTED[..MID]).expect("first write failed");
+            block.flush().expect("first flush failed");
+        }
+        {
+            let mut block = case.open_existing(&path);
+            block
+                .seek(SeekFrom::Start(MID.try_into().unwrap()))
+                .expect("seek failed");
+            block.write(&EXPECTED[MID..]).expect("second write failed");
+            block.flush().expect("second flush failed");
+        }
+        {
+            let mut block = case.open_existing(&path);
+            let mut actual = [0u8; EXPECTED.len()];
+            block.read(&mut actual).expect("read failed");
+            assert_eq!(EXPECTED, actual);
+        }
+    }
+
+    #[test]
+    fn block_write_with_different_creds() {
+        const EXPECTED: &[u8] = b"Cool callous calamitous colonels.";
+        const MID: usize = EXPECTED.len() / 2;
+        const BLOCK_NAME: &'static str = "test.blk";
+
+        let mut case = BlockTestCase::new();
+        let path = Path {
+            root: case.root_creds.principal(),
+            components: vec!["test.blk".to_string()],
+        };
+        let app_creds = {
+            let mut app_creds = ConcreteCreds::generate().expect("failed to generate app creds");
+            let writecap = case
+                .root_creds
+                .issue_writecap(
+                    app_creds.principal(),
+                    path.components.clone(),
+                    Epoch::now() + Duration::from_secs(60),
+                )
+                .expect("failed to issue writecap");
+            app_creds.set_writecap(writecap);
+            app_creds
+        };
+        {
+            let mut block = case.open_new(&path);
+            block
+                .add_readcap_for(app_creds.principal(), &app_creds)
+                .expect("failed to add readcap");
+            block.write(&EXPECTED[..MID]).expect("first write failed");
+            block.flush().expect("first flush failed");
+        }
+        {
+            let file = OpenOptions::new()
+                .read(true)
+                .write(true)
+                .open(case.fs_path(&path))
+                .expect("failed to reopen file");
+            let mut block = BlockOpenOptions::new()
+                .with_inner(file)
+                // Note that this write is performed using app_creds.
+                .with_creds(app_creds)
+                .with_encrypt(true)
+                .open()
+                .expect("failed to reopen block");
+            block
+                .seek(SeekFrom::Start(MID.try_into().unwrap()))
+                .expect("seek failed");
+            block.write(&EXPECTED[MID..]).expect("second write failed");
+            block.flush().expect("second flush failed");
+        }
+        {
+            let file = OpenOptions::new()
+                .read(true)
+                .write(true)
+                .open(case.fs_path(&path))
+                .expect("failed to reopen file");
+            let mut block = BlockOpenOptions::new()
+                .with_inner(file)
+                .with_creds(case.node_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);
+        }
+    }
 }

+ 1 - 1
crates/btlib/src/test_helpers.rs

@@ -192,7 +192,7 @@ pub(crate) fn make_block() -> Box<dyn Block> {
 }
 
 pub(crate) fn make_block_with(readcap: Readcap) -> Box<dyn Block> {
-    let mut readcaps = HashMap::new();
+    let mut readcaps = BTreeMap::new();
     readcaps.insert(readcap.issued_to, readcap.key);
     // Notice that the writecap path contains the block path. If this were not the case, the block
     // would be invalid.