Browse Source

* Split out all the mutating methods in `CredStore` into their own trait.
This was done in order to allow the same credential store to be used by
multiple processes, by ensuring only one of them can mutate it.
* Added `btlib::atomic_file` to provide additional guards against corrupting
a credential store which is being concurrently accessed by multiple processes.

Matthew Carr 1 year ago
parent
commit
feca36ea96

+ 142 - 6
Cargo.lock

@@ -84,7 +84,7 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi",
 ]
@@ -288,6 +288,7 @@ dependencies = [
  "criterion",
  "ctor",
  "env_logger",
+ "fd-lock",
  "foreign-types",
  "fuse-backend-rs",
  "lazy_static",
@@ -773,6 +774,38 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fd-lock"
+version = "3.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642"
+dependencies = [
+ "cfg-if",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
@@ -972,6 +1005,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -1024,6 +1063,17 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "io-uring"
 version = "0.5.7"
@@ -1083,9 +1133,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
 [[package]]
 name = "libc"
-version = "0.2.137"
+version = "0.2.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
 
 [[package]]
 name = "libdbus-sys"
@@ -1121,6 +1171,12 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
+
 [[package]]
 name = "log"
 version = "0.4.17"
@@ -1264,7 +1320,7 @@ version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
 ]
 
@@ -1793,6 +1849,20 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.37.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "rustls"
 version = "0.20.7"
@@ -2592,21 +2662,51 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
- "windows_aarch64_gnullvm",
+ "windows_aarch64_gnullvm 0.42.0",
  "windows_aarch64_msvc 0.42.0",
  "windows_i686_gnu 0.42.0",
  "windows_i686_msvc 0.42.0",
  "windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm",
+ "windows_x86_64_gnullvm 0.42.0",
  "windows_x86_64_msvc 0.42.0",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.36.1"
@@ -2619,6 +2719,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.36.1"
@@ -2631,6 +2737,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.36.1"
@@ -2643,6 +2755,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.36.1"
@@ -2655,12 +2773,24 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.36.1"
@@ -2673,6 +2803,12 @@ version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
 [[package]]
 name = "x509-certificate"
 version = "0.18.0"

+ 26 - 15
crates/btconfig/src/lib.rs

@@ -1,6 +1,6 @@
 use btlib::{
     self,
-    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore, Creds},
+    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore, CredStoreMut, Creds},
     error::BtErr,
     Result,
 };
@@ -107,30 +107,41 @@ impl CredStoreCfg {
     }
 }
 
+/// Trait for types which can consume a [CredStore] implementation.
 pub trait CredStoreConsumer {
     type Output;
     fn consume<C: CredStore>(self, cred_store: C) -> Self::Output;
 }
 
-impl CredStoreCfg {
-    /// Calls `consumer` with the [CredStore] created based on the configuration data in `self`.
-    pub fn consume<F: CredStoreConsumer>(self, consumer: F) -> Result<F::Output> {
-        match self {
-            CredStoreCfg::File { path } => {
-                let store = FileCredStore::new(path)?;
-                Ok(consumer.consume(store))
-            }
-            CredStoreCfg::Tpm {
-                path: tpm_state_path,
-                tabrmd,
-            } => {
-                let store = TpmCredStore::from_tabrmd(&tabrmd, tpm_state_path)?;
-                Ok(consumer.consume(store))
+/// Trait for types which can consume a [CredStoreMut] implementation.
+pub trait CredStoreMutConsumer {
+    type Output;
+    fn consume_mut<C: CredStoreMut>(self, cred_store: C) -> Self::Output;
+}
+
+macro_rules! impl_consume {
+    ($name:ident, $consumer:path) => {
+        #[doc = concat!("Calls `", stringify!($name), "` with the credential store created based on the configuration data in `self`")]
+        pub fn $name<F: $consumer>(self, consumer: F) -> Result<F::Output> {
+            match self {
+                CredStoreCfg::File { path } => {
+                    let store = FileCredStore::new(path)?;
+                    Ok(consumer.$name(store))
+                }
+                CredStoreCfg::Tpm { path, tabrmd } => {
+                    let store = TpmCredStore::from_tabrmd(&tabrmd, path)?;
+                    Ok(consumer.$name(store))
+                }
             }
         }
     }
 }
 
+impl CredStoreCfg {
+    impl_consume!(consume, CredStoreConsumer);
+    impl_consume!(consume_mut, CredStoreMutConsumer);
+}
+
 /// A [CredStoreConsumer] which gets the node creds from the [CredStore], then returns them
 /// as an `Arc<dyn Creds>`.
 pub struct NodeCredConsumer;

+ 2 - 2
crates/btfsd/src/main.rs

@@ -62,8 +62,8 @@ mod tests {
     use btfproto::{client::FsClient, msg::*};
     use btlib::{
         crypto::{
-            file_cred_store::FileCredStore, tpm::TpmCredStore, ConcreteCreds, CredStore, CredsPriv,
-            CredsPub,
+            file_cred_store::FileCredStore, tpm::TpmCredStore, ConcreteCreds, CredStore,
+            CredStoreMut, CredsPriv, CredsPub,
         },
         log::BuilderExt,
         AuthzAttrs, BlockMetaSecrets, Epoch, IssuedProcRec, Principaled, ProcRec,

+ 1 - 1
crates/btfuse/src/main.rs

@@ -156,7 +156,7 @@ mod test {
 
     use btfproto::{local_fs::ModeAuthorizer, server::new_fs_server};
     use btlib::{
-        crypto::{tpm::TpmCredStore, CredStore, Creds},
+        crypto::{tpm::TpmCredStore, CredStore, CredStoreMut, Creds},
         log::BuilderExt,
         Epoch, Principaled,
     };

+ 3 - 3
crates/btlib-tests/src/cred_store_testing_ext.rs

@@ -1,11 +1,11 @@
 use std::time::Duration;
 
 use btlib::{
-    crypto::{CredStore, Creds},
+    crypto::{CredStoreMut, Creds},
     Epoch, Principaled, Result,
 };
 
-pub trait CredStoreTestingExt: CredStore {
+pub trait CredStoreTestingExt: CredStoreMut {
     /// Generates new root credentials and issues the node credentials a writecap using them. The
     /// given password is used to secure the root credentials.
     fn provision(&self, root_password: &str) -> Result<()> {
@@ -17,4 +17,4 @@ pub trait CredStoreTestingExt: CredStore {
     }
 }
 
-impl<T: CredStore> CredStoreTestingExt for T {}
+impl<T: CredStoreMut> CredStoreTestingExt for T {}

+ 1 - 1
crates/btlib-tests/src/tpm_cred_store_harness.rs

@@ -4,7 +4,7 @@
 use btlib::{
     crypto::{
         tpm::{TpmCredStore, TpmCreds},
-        CredStore, Creds,
+        CredStore, CredStoreMut, Creds,
     },
     error::AnyhowErrorExt,
     Epoch, Principaled, Result,

+ 1 - 0
crates/btlib/Cargo.toml

@@ -31,6 +31,7 @@ x509-certificate = "0.18.0"
 bcder = "0.7.1"
 bytes = "1.3.0"
 safemem = "0.3.3"
+fd-lock = "3.0.12"
 
 [dev-dependencies]
 tempdir = { version = "0.3.7" }

+ 202 - 0
crates/btlib/src/atomic_file.rs

@@ -0,0 +1,202 @@
+//! This module exports the [AtomicFile] struct, which is used to synchronize access to a file.
+
+use crate::{error::DisplayErr, Result};
+
+use btserde::{read_from, write_to};
+use fd_lock::RwLock as FdLock;
+use serde::{de::DeserializeOwned, Serialize};
+use std::{
+    fs::{DirBuilder, File, OpenOptions},
+    io::{self, BufReader, BufWriter},
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+    os::unix::fs::PermissionsExt,
+    path::PathBuf,
+    sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
+};
+
+/// This struct provides access to a value of type `T` which can be persisted to a file in the
+/// local filesystem. Access to `T` is synchronized within the current process, and access to the
+/// file is synchronized between processes.
+///
+/// This struct is primarily intended to prevent the file storing `T` from becoming corrupted if
+/// multiple processes attempt to write to it at once.
+pub struct AtomicFile<T> {
+    file: SyncFile<T>,
+    value: RwLock<T>,
+}
+
+impl<T: TryDefault + Serialize + DeserializeOwned> AtomicFile<T> {
+    /// Creates a new [AtomicFile] stored at `path`.
+    ///
+    /// If there is already a file at `path` then it is
+    /// opened and a value of type `T` is deserialized from it. If it does not exist, then a new
+    /// value of `T` is created using the [TryDefault] trait which is then written to a new file at
+    /// `path`.
+    ///
+    /// If a new file is created then it's mode will be masked using `mask`. This means that if a
+    /// bit is set in `mask` then the corresponding mode bit will be unset in the file.
+    ///
+    /// This method will recursively create all parent directories of `path` if they do not exist.
+    pub fn new(path: PathBuf, mask: u32) -> Result<Self> {
+        let file = SyncFile::new(path, mask);
+        let value = RwLock::new(file.load_or_init()?);
+        Ok(AtomicFile { file, value })
+    }
+
+    /// Locks the value of `T` for reading in the current process and returns a struct which
+    /// dereferences to it.
+    pub fn read(&self) -> Result<AtomicFileReadGuard<'_, T>> {
+        Ok(AtomicFileReadGuard {
+            guard: self.value.read().display_err()?,
+        })
+    }
+
+    /// Locks the value of `T` for writing in the current process and returns a struct which
+    /// dereferences to it.
+    ///
+    /// This struct can be used to save the current value of `T` to the file.
+    pub fn write(&self) -> Result<AtomicFileWriteGuard<'_, T>> {
+        Ok(AtomicFileWriteGuard {
+            file: &self.file,
+            guard: self.value.write().display_err()?,
+        })
+    }
+}
+
+pub struct AtomicFileReadGuard<'a, T> {
+    guard: RwLockReadGuard<'a, T>,
+}
+
+impl<'a, T> Deref for AtomicFileReadGuard<'a, T> {
+    type Target = T;
+    fn deref(&self) -> &Self::Target {
+        self.guard.deref()
+    }
+}
+
+pub struct AtomicFileWriteGuard<'a, T> {
+    file: &'a SyncFile<T>,
+    guard: RwLockWriteGuard<'a, T>,
+}
+
+impl<'a, T: Serialize> AtomicFileWriteGuard<'a, T> {
+    /// Saves the current value of `T` to the file.
+    ///
+    /// If another process calls `save` concurrently, then one will block waiting for the other
+    /// to finish. Note that in this case one process will overwrite the value written
+    /// by the other, but whichever value is contained in the file after both have finished
+    /// is internally consistent.
+    pub fn save(&mut self) -> Result<()> {
+        self.file.save(&self.guard)
+    }
+}
+
+impl<'a, T> Deref for AtomicFileWriteGuard<'a, T> {
+    type Target = T;
+    fn deref(&self) -> &Self::Target {
+        self.guard.deref()
+    }
+}
+
+impl<'a, T> DerefMut for AtomicFileWriteGuard<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.guard.deref_mut()
+    }
+}
+
+/// This struct provides synchronized access to a file such that two different processes cannot
+/// concurrently write to the same path.
+pub struct SyncFile<T> {
+    path: PathBuf,
+    /// The bits which are set in this field will cause the corresponding mode bits to be set to
+    /// zero when the file is created.
+    mask: u32,
+    phantom_data: PhantomData<T>,
+}
+
+impl<T> SyncFile<T> {
+    /// Create a new [SyncFile] backed by `path`. If there is no file at `path`, then it will be
+    /// created and it's mode adjusted according to `mask`.
+    pub fn new(path: PathBuf, mask: u32) -> Self {
+        SyncFile {
+            path,
+            mask,
+            phantom_data: PhantomData,
+        }
+    }
+}
+
+impl<T: Serialize> SyncFile<T> {
+    /// Serialize `value` and save the resulting bytes in the file.
+    pub fn save(&self, value: &T) -> Result<()> {
+        let file = OpenOptions::new()
+            .write(true)
+            .create(false)
+            .open(&self.path)?;
+        let mut lock = FdLock::new(file);
+        let mut guard = lock.write()?;
+        let mut writer = BufWriter::new(guard.deref_mut());
+        write_to(value, &mut writer).map_err(|err| err.into())
+    }
+}
+
+impl<T: DeserializeOwned> SyncFile<T> {
+    fn load(file: File) -> Result<T> {
+        let lock = FdLock::new(file);
+        let guard = lock.read()?;
+        let mut reader = BufReader::new(guard.deref());
+        read_from(&mut reader).map_err(|err| err.into())
+    }
+}
+
+impl<T: TryDefault + Serialize + DeserializeOwned> SyncFile<T> {
+    fn init(&self) -> Result<T> {
+        if let Some(dir_path) = self.path.parent() {
+            DirBuilder::new().recursive(true).create(dir_path)?;
+        }
+        let file = OpenOptions::new()
+            .write(true)
+            .create_new(true)
+            .open(&self.path)?;
+        let metadata = file.metadata()?;
+        let mut permissions = metadata.permissions();
+        if permissions.mode() & self.mask != 0 {
+            permissions.set_mode(permissions.mode() & !self.mask);
+            file.set_permissions(permissions)?;
+        }
+
+        let state = T::try_default()?;
+        let mut writer = BufWriter::new(file);
+        write_to(&state, &mut writer)?;
+        Ok(state)
+    }
+
+    /// Load the current value from the file, or create a new value using the [TryDefault] trait,
+    /// save it to the file and return it.
+    pub fn load_or_init(&self) -> Result<T> {
+        match OpenOptions::new().read(true).create(false).open(&self.path) {
+            Ok(file) => Self::load(file),
+            Err(err) => {
+                if io::ErrorKind::NotFound != err.kind() {
+                    return Err(err.into());
+                }
+                self.init()
+            }
+        }
+    }
+}
+
+/// Trait for types which can produce a default value, but which may fail when doing so.
+pub trait TryDefault: Sized {
+    /// Attempt to produce a default value for this type.
+    fn try_default() -> Result<Self>;
+}
+
+/// Constants for setting file permissions.
+pub mod mask {
+    /// A mask which only allows the owner to access a file.
+    pub const OWNER_ONLY: u32 = 0o077;
+    /// A mask which only allows the owner or group owner to access a file.
+    pub const OWNER_AND_GROUP: u32 = 0o007;
+}

+ 46 - 38
crates/btlib/src/crypto.rs

@@ -2347,26 +2347,59 @@ pub trait CredStore {
     /// Returns the root credentials. If no root credentials have been generated, or the provided
     /// password is incorrect, then an error is returned.
     fn root_creds(&self, password: &str) -> Result<Self::CredHandle>;
-    /// Generates the root credentials and protects them using the given password. If the root
-    /// credentials have already been generated then an error is returned.
-    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle>;
     /// Returns a public key which can be used to encrypt data intended only to be accessed by this
-    /// node. The returned key can be given as the `new_parent` parameter to the [export_root_creds]
-    /// method.
+    /// node. The returned key can be given as the `new_parent` parameter to the
+    /// [CredStore::export_root_creds] method.
     fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>>;
     /// Exports the root credentials. These can be serialized and persisted external to the
-    /// application and later loaded and deserialized and passed to the [import_root_creds] method.
-    /// The `password` argument must match the value provided when the [root_creds] method was
-    /// called. The `new_parent` argument is the public key of the node that is to import the root
-    /// key, which can be obtained using the [gen_root_creds] method on the importing node.
+    /// application and later loaded and deserialized and passed to the
+    /// [CredStoreMut::import_root_creds] method.
+    /// The `password` argument must match the value provided when the [CredStore::root_creds]
+    /// method was called. The `new_parent` argument is the public key of the node that is to import
+    /// the root key, which can be obtained using the [CredStoreMut::gen_root_creds] method on the
+    /// importing node.
     fn export_root_creds(
         &self,
         root_creds: &Self::CredHandle,
         password: &str,
         new_parent: &AsymKeyPub<Encrypt>,
     ) -> Result<Self::ExportedCreds>;
-    /// Imports root credentials that were previously created with [export_root_creds]. The provided
-    /// password must match the value that given to that method.
+}
+
+impl<T: ?Sized + CredStore, P: Deref<Target = T>> CredStore for P {
+    type CredHandle = T::CredHandle;
+    type ExportedCreds = T::ExportedCreds;
+
+    fn node_creds(&self) -> Result<Self::CredHandle> {
+        self.deref().node_creds()
+    }
+
+    fn root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+        self.deref().root_creds(password)
+    }
+
+    fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
+        self.deref().storage_key()
+    }
+
+    fn export_root_creds(
+        &self,
+        root_creds: &Self::CredHandle,
+        password: &str,
+        new_parent: &AsymKeyPub<Encrypt>,
+    ) -> Result<Self::ExportedCreds> {
+        self.deref()
+            .export_root_creds(root_creds, password, new_parent)
+    }
+}
+
+/// An extension of [CredStore] which exposes additional methods for mutating the credential store.
+pub trait CredStoreMut: CredStore {
+    /// Generates the root credentials and protects them using the given password. If the root
+    /// credentials have already been generated then an error is returned.
+    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle>;
+    /// Imports root credentials that were previously created with [CredStore::export_root_creds].
+    /// The provided password must match the value that was given to that method.
     fn import_root_creds(
         &self,
         password: &str,
@@ -2406,36 +2439,11 @@ pub trait CredStore {
     }
 }
 
-impl<T: ?Sized + CredStore, P: Deref<Target = T>> CredStore for P {
-    type CredHandle = T::CredHandle;
-    type ExportedCreds = T::ExportedCreds;
-
-    fn node_creds(&self) -> Result<Self::CredHandle> {
-        self.deref().node_creds()
-    }
-
-    fn root_creds(&self, password: &str) -> Result<Self::CredHandle> {
-        self.deref().root_creds(password)
-    }
-
-    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+impl<T: ?Sized + CredStoreMut, P: Deref<Target = T>> CredStoreMut for P {
+    fn gen_root_creds(&self, password: &str) -> Result<T::CredHandle> {
         self.deref().gen_root_creds(password)
     }
 
-    fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
-        self.deref().storage_key()
-    }
-
-    fn export_root_creds(
-        &self,
-        root_creds: &Self::CredHandle,
-        password: &str,
-        new_parent: &AsymKeyPub<Encrypt>,
-    ) -> Result<Self::ExportedCreds> {
-        self.deref()
-            .export_root_creds(root_creds, password, new_parent)
-    }
-
     fn import_root_creds(
         &self,
         password: &str,

+ 36 - 102
crates/btlib/src/crypto/file_cred_store.rs

@@ -1,21 +1,15 @@
 use crate::{
+    atomic_file::{mask, AtomicFile, TryDefault},
     crypto::{
-        AsymKeyPair, AsymKeyPub, ConcreteCreds, CredStore, DerivationParams, Encrypt, Envelope,
-        Scheme, TaggedCiphertext,
+        AsymKeyPair, AsymKeyPub, ConcreteCreds, CredStore, CredStoreMut, DerivationParams, Encrypt,
+        Envelope, Scheme, TaggedCiphertext,
     },
     error::DisplayErr,
     Result,
 };
-use btserde::{read_from, write_to};
+use btserde::field_helpers;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use std::{
-    fmt::Display,
-    fs::{DirBuilder, File, Metadata, OpenOptions},
-    io::{self, BufReader, BufWriter},
-    os::unix::fs::PermissionsExt,
-    path::{Path, PathBuf},
-    sync::{Arc, RwLock},
-};
+use std::{fmt::Display, path::PathBuf, sync::Arc};
 use zeroize::{Zeroize, Zeroizing};
 
 pub use private::{Error, FileCredStore};
@@ -23,20 +17,6 @@ pub use private::{Error, FileCredStore};
 mod private {
     use super::*;
 
-    fn serialize_inner<S: Serializer, T: Serialize>(
-        value: &Arc<T>,
-        ser: S,
-    ) -> std::result::Result<S::Ok, S::Error> {
-        value.serialize(ser)
-    }
-
-    fn deserialize_inner<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
-        de: D,
-    ) -> std::result::Result<Arc<T>, D::Error> {
-        let inner: T = Deserialize::deserialize(de)?;
-        Ok(Arc::new(inner))
-    }
-
     fn serialize_optional_inner<S: Serializer, T: Serialize>(
         value: &Option<Arc<T>>,
         ser: S,
@@ -94,8 +74,7 @@ mod private {
 
     #[derive(Serialize, Deserialize)]
     struct State {
-        #[serde(serialize_with = "serialize_inner")]
-        #[serde(deserialize_with = "deserialize_inner")]
+        #[serde(with = "field_helpers::smart_ptr")]
         /// The credentials of this node.
         node_creds: Arc<ConcreteCreds>,
         #[serde(serialize_with = "serialize_optional_inner")]
@@ -125,46 +104,6 @@ mod private {
             })
         }
 
-        fn open_file(file_path: &Path) -> Result<(File, Metadata)> {
-            let result = OpenOptions::new()
-                .read(true)
-                .write(true)
-                .create(false)
-                .open(file_path);
-            let file = match result {
-                Ok(file) => file,
-                Err(err) => {
-                    if io::ErrorKind::NotFound == err.kind() {
-                        if let Some(dir_path) = file_path.parent() {
-                            DirBuilder::new().recursive(true).create(dir_path)?;
-                        }
-                        OpenOptions::new()
-                            .read(true)
-                            .write(true)
-                            .create_new(true)
-                            .open(file_path)?
-                    } else {
-                        return Err(err.into());
-                    }
-                }
-            };
-            let metadata = file.metadata()?;
-            let mut permissions = metadata.permissions();
-            const MASK: u32 = 0o077;
-            if permissions.mode() & MASK != 0 {
-                permissions.set_mode(permissions.mode() & !MASK);
-                file.set_permissions(permissions)?;
-            }
-            Ok((file, metadata))
-        }
-
-        fn save(&mut self, file_path: &Path) -> Result<()> {
-            let (file, ..) = Self::open_file(file_path)?;
-            let mut writer = BufWriter::new(file);
-            write_to(self, &mut writer)?;
-            Ok(())
-        }
-
         fn root_password_valid(&self, password: &str) -> Result<()> {
             let expected = if let Some(ref expected) = self.root_password_hash {
                 expected
@@ -185,6 +124,12 @@ mod private {
         }
     }
 
+    impl TryDefault for State {
+        fn try_default() -> Result<Self> {
+            State::new()
+        }
+    }
+
     /// An implementation of [CredStore] which uses a file to store credentials.
     ///
     /// This struct relies on the security of the underlying filesystem to protect the
@@ -194,8 +139,7 @@ mod private {
     /// are loaded into main memory as long as this struct is alive. Thus, this struct does not
     /// provide anywhere near the level of protection that [crate::crypto::tpm::TpmCredStore] does.
     pub struct FileCredStore {
-        state: RwLock<State>,
-        file_path: PathBuf,
+        state: AtomicFile<State>,
     }
 
     impl FileCredStore {
@@ -203,21 +147,9 @@ mod private {
         ///
         /// If no file is present at `file_path`, then a new one will be created containing
         /// freshly generated credentials.
-        pub fn new(file_path: PathBuf) -> Result<Self> {
-            let (file, metadata) = State::open_file(&file_path)?;
-            let state = if metadata.len() > 0 {
-                let mut reader = BufReader::new(file);
-                read_from::<State, _>(&mut reader)?
-            } else {
-                // If the file is zero length then we must have just created it.
-                let state = State::new()?;
-                let mut writer = BufWriter::new(file);
-                write_to(&state, &mut writer)?;
-                state
-            };
+        pub fn new(path: PathBuf) -> Result<Self> {
             Ok(FileCredStore {
-                state: RwLock::new(state),
-                file_path,
+                state: AtomicFile::new(path, mask::OWNER_ONLY)?,
             })
         }
     }
@@ -242,21 +174,6 @@ mod private {
             }
         }
 
-        fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
-            {
-                let state = self.state.read().display_err()?;
-                if let Some(ref root_creds) = state.root_creds {
-                    state.root_password_valid(password)?;
-                    return Ok(root_creds.clone());
-                }
-            }
-            let mut state = self.state.write().display_err()?;
-            let root_creds = Arc::new(ConcreteCreds::generate()?);
-            state.set_root_creds(root_creds.clone(), password)?;
-            state.save(&self.file_path)?;
-            Ok(root_creds)
-        }
-
         fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
             let guard = self.state.read().display_err()?;
             Ok(guard.storage_key.public.clone())
@@ -274,6 +191,23 @@ mod private {
             let export = aead_key.encrypt(params, &envelope)?;
             Ok(export)
         }
+    }
+
+    impl CredStoreMut for FileCredStore {
+        fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+            {
+                let state = self.state.read().display_err()?;
+                if let Some(ref root_creds) = state.root_creds {
+                    state.root_password_valid(password)?;
+                    return Ok(root_creds.clone());
+                }
+            }
+            let mut state = self.state.write().display_err()?;
+            let root_creds = Arc::new(ConcreteCreds::generate()?);
+            state.set_root_creds(root_creds.clone(), password)?;
+            state.save()?;
+            Ok(root_creds)
+        }
 
         fn import_root_creds(
             &self,
@@ -285,7 +219,7 @@ mod private {
             let mut state = self.state.write().display_err()?;
             let root_creds = Arc::new(envelope.open(&state.storage_key)?);
             state.set_root_creds(root_creds.clone(), password)?;
-            state.save(&self.file_path)?;
+            state.save()?;
             Ok(root_creds)
         }
 
@@ -298,7 +232,7 @@ mod private {
             node_creds.set_writecap(writecap)?;
             let mut state = self.state.write().display_err()?;
             state.node_creds = handle.clone();
-            state.save(&self.file_path)?;
+            state.save()?;
             Ok(())
         }
 
@@ -311,7 +245,7 @@ mod private {
             root_creds.set_writecap(writecap)?;
             let mut state = self.state.write().display_err()?;
             state.root_creds = Some(handle.clone());
-            state.save(&self.file_path)?;
+            state.save()?;
             Ok(())
         }
     }
@@ -327,7 +261,7 @@ mod test {
     use super::*;
 
     use btserde::to_vec;
-    use std::{ops::Deref, time::Duration};
+    use std::{ops::Deref, path::Path, time::Duration};
     use tempdir::TempDir;
 
     struct TestCase {

+ 99 - 136
crates/btlib/src/crypto/tpm.rs

@@ -1,9 +1,11 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
-use crate::error::{DisplayErr, StringError};
+use crate::{
+    atomic_file::{mask, SyncFile, TryDefault},
+    error::{DisplayErr, StringError},
+};
 
 use super::*;
 
-use btserde::read_from;
 use log::error;
 use openssl::{
     bn::{BigNum, BigNumRef},
@@ -12,14 +14,12 @@ use openssl::{
 use serde::ser;
 use std::{
     ffi::CStr,
-    fs::OpenOptions,
-    io::{BufReader, BufWriter, Read, Write},
     mem::size_of,
     ops::Deref,
-    os::{raw::c_char, unix::fs::PermissionsExt},
-    path::{Path, PathBuf},
+    os::raw::c_char,
+    path::PathBuf,
     str::FromStr,
-    sync::{Arc, RwLock, RwLockWriteGuard},
+    sync::{Arc, RwLock},
     time::Duration,
 };
 use tss_esapi::{
@@ -458,6 +458,30 @@ pub struct ExportedCreds {
     writecap: Option<Writecap>,
 }
 
+/// A public/private key pair.
+#[derive(Clone)]
+struct KeyPair<S: Scheme> {
+    public: AsymKeyPub<S>,
+    /// A rust struct which wraps an `ESYS_TR` from the ESAPI.
+    private: KeyHandle,
+}
+
+impl<S: Scheme> KeyPair<S> {
+    fn from_stored(context: &mut Context, stored: &StoredKeyPair<S>) -> Result<KeyPair<S>> {
+        let tpm_handle = TpmHandle::try_from(stored.private)?;
+        let key_handle = context.key_handle(tpm_handle)?;
+        Ok(KeyPair {
+            public: stored.public.clone(),
+            private: key_handle,
+        })
+    }
+
+    fn to_stored(&self, private: TPM2_HANDLE) -> StoredKeyPair<S> {
+        let public = self.public.clone();
+        StoredKeyPair { public, private }
+    }
+}
+
 /// A public/private key pair in a form which can be serialized and deserialized.
 #[derive(Serialize, Clone)]
 struct StoredKeyPair<S: Scheme> {
@@ -467,7 +491,7 @@ struct StoredKeyPair<S: Scheme> {
 
 impl<'de, S: Scheme> Deserialize<'de> for StoredKeyPair<S> {
     fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
-        const FIELDS: &[&str] = &["public", "private", "writecap"];
+        const FIELDS: &[&str] = &["public", "private"];
 
         struct StructVisitor<S: Scheme>(PhantomData<S>);
 
@@ -501,6 +525,13 @@ impl<'de, S: Scheme> Deserialize<'de> for StoredKeyPair<S> {
     }
 }
 
+/// Credentials which can be used for signing and encrypting.
+struct CredData {
+    sign: KeyPair<Sign>,
+    enc: KeyPair<Encrypt>,
+    writecap: Option<Writecap>,
+}
+
 /// Credentials in a form which can be serialized and deserialized.
 #[derive(Serialize, Deserialize, Clone)]
 struct StoredCredData {
@@ -538,46 +569,11 @@ impl Storage {
             storage_key: None,
         }
     }
+}
 
-    fn save<W: Write>(&self, to: &mut W) -> Result<()> {
-        Ok(write_to(self, to)?)
-    }
-
-    fn load<R: Read>(from: &mut R) -> Result<Storage> {
-        Ok(read_from(from)?)
-    }
-
-    fn init<P: AsRef<Path>>(&self, path: P) -> Result<()> {
-        let file = OpenOptions::new()
-            .write(true)
-            .create_new(true)
-            .open(&path)?;
-        // Only allow access from the current user.
-        let metadata = file.metadata()?;
-        let mut permissions = metadata.permissions();
-        permissions.set_mode(0o600);
-        file.set_permissions(permissions)?;
-        let mut reader = BufWriter::new(file);
-        self.save(&mut reader)?;
-        reader.flush()?;
-        Ok(())
-    }
-
-    fn load_or_init<P: AsRef<Path>>(path: P) -> Result<Storage> {
-        match OpenOptions::new().read(true).open(&path) {
-            Ok(file) => {
-                let mut reader = BufReader::new(file);
-                Self::load(&mut reader)
-            }
-            Err(error) => {
-                if std::io::ErrorKind::NotFound != error.kind() {
-                    return Err(error.into());
-                }
-                let state = Self::new(Cookie::random()?);
-                state.init(path)?;
-                Ok(state)
-            }
-        }
+impl TryDefault for Storage {
+    fn try_default() -> Result<Self> {
+        Ok(Storage::new(Cookie::random()?))
     }
 }
 
@@ -774,46 +770,18 @@ impl<'a> KeyBuilder<'a, Encrypt> {
     }
 }
 
-/// A public/private key pair.
-#[derive(Clone)]
-struct KeyPair<S: Scheme> {
-    public: AsymKeyPub<S>,
-    /// A rust struct which wraps an `ESYS_TR` from the ESAPI.
-    private: KeyHandle,
-}
-
-impl<S: Scheme> KeyPair<S> {
-    fn from_stored(context: &mut Context, stored: &StoredKeyPair<S>) -> Result<KeyPair<S>> {
-        let tpm_handle = TpmHandle::try_from(stored.private)?;
-        let key_handle = context.key_handle(tpm_handle)?;
-        Ok(KeyPair {
-            public: stored.public.clone(),
-            private: key_handle,
-        })
-    }
-
-    fn to_stored(&self, private: TPM2_HANDLE) -> StoredKeyPair<S> {
-        let public = self.public.clone();
-        StoredKeyPair { public, private }
-    }
-}
-
-/// Credentials which can be used for signing and encrypting.
-struct CredData {
-    sign: KeyPair<Sign>,
-    enc: KeyPair<Encrypt>,
-    writecap: Option<Writecap>,
-}
-
 struct State {
     context: Context,
     storage: Storage,
+    file: SyncFile<Storage>,
     node_creds: Option<TpmCreds>,
     storage_key: Option<KeyPair<Encrypt>>,
 }
 
 impl State {
-    fn new(mut context: Context, storage: Storage) -> Result<State> {
+    fn new(mut context: Context, path: PathBuf) -> Result<State> {
+        let file = SyncFile::new(path, mask::OWNER_ONLY);
+        let storage: Storage = file.load_or_init()?;
         let storage_key = match &storage.storage_key {
             Some(storage_key) => Some(KeyPair::from_stored(&mut context, storage_key)?),
             None => None,
@@ -821,12 +789,13 @@ impl State {
         Ok(State {
             context,
             storage,
+            file,
             node_creds: None,
             storage_key,
         })
     }
 
-    fn init_node_creds(&mut self, state: &Arc<RwLock<State>>) -> Result<()> {
+    fn init_node_creds(&mut self, state: Arc<RwLock<State>>) -> Result<()> {
         let tpm_handles = match &self.storage.node {
             Some(handles) => handles,
             None => return Ok(()),
@@ -840,11 +809,14 @@ impl State {
         self.node_creds = Some(TpmCreds::new(key_handles, state));
         Ok(())
     }
+
+    fn save(&mut self) -> Result<()> {
+        self.file.save(&self.storage)
+    }
 }
 
 pub struct TpmCredStore {
     state: Arc<RwLock<State>>,
-    storage_path: PathBuf,
     cookie: Cookie,
 }
 
@@ -861,28 +833,16 @@ impl TpmCredStore {
     }
 
     pub fn from_context(mut context: Context, state_path: PathBuf) -> Result<TpmCredStore> {
-        let storage = Storage::load_or_init(&state_path)?;
         let session = context.start_default_auth_session()?;
         context.set_sessions((Some(session), None, None));
-        let cookie = storage.cookie.clone();
-        let state = Arc::new(RwLock::new(State::new(context, storage)?));
+        let state = State::new(context, state_path)?;
+        let cookie = state.storage.cookie.clone();
+        let state = Arc::new(RwLock::new(state));
         {
             let mut guard = state.write().display_err()?;
-            guard.init_node_creds(&state)?;
+            guard.init_node_creds(state.clone())?;
         }
-        Ok(TpmCredStore {
-            state,
-            storage_path: state_path,
-            cookie,
-        })
-    }
-
-    fn save_storage(&self, guard: &mut RwLockWriteGuard<State>) -> Result<()> {
-        let file = OpenOptions::new().write(true).open(&self.storage_path)?;
-        let mut writer = BufWriter::new(file);
-        guard.storage.save(&mut writer)?;
-        writer.flush()?;
-        Ok(())
+        Ok(TpmCredStore { state, cookie })
     }
 
     fn persist<F: FnOnce(&mut State, StoredCredData)>(
@@ -907,7 +867,7 @@ impl TpmCredStore {
             writecap: creds.writecap.clone(),
         };
         update_storage(&mut guard, handles);
-        match self.save_storage(&mut guard) {
+        match guard.save() {
             Ok(_) => Ok(()),
             Err(error) => {
                 let result = guard
@@ -950,7 +910,7 @@ impl TpmCredStore {
         guard.storage_key = Some(storage_key.clone());
         let tpm_handle = guard.context.persist_key(storage_key.private)?;
         guard.storage.storage_key = Some(storage_key.to_stored(tpm_handle));
-        if let Err(err) = self.save_storage(&mut guard) {
+        if let Err(err) = guard.save() {
             guard.storage_key = None;
             guard.storage.storage_key = None;
             guard
@@ -990,7 +950,7 @@ impl TpmCredStore {
             enc,
             writecap: None,
         };
-        let creds = TpmCreds::new(cred_data, &self.state);
+        let creds = TpmCreds::new(cred_data, self.state.clone());
         self.persist(&creds, |state, handles| {
             state.storage.node = Some(handles);
             state.node_creds = Some(creds.clone());
@@ -1050,35 +1010,7 @@ impl CredStore for TpmCredStore {
         let mut guard = self.state.write().display_err()?;
         let key_handles = root_handles.to_cred_data(&mut guard.context)?;
         guard.context.set_auth(&key_handles, password)?;
-        Ok(TpmCreds::new(key_handles, &self.state))
-    }
-
-    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
-        {
-            let guard = self.state.read().display_err()?;
-            if guard.storage.root.is_some() {
-                return Err(bterr!("root creds have already been generated"));
-            }
-        }
-        let policy = {
-            let mut guard = self.state.write().display_err()?;
-            guard.context.dup_with_password_policy()?
-        };
-        let sign = self.gen_root_sign_key(password, policy.clone())?;
-        let enc = self.gen_root_enc_key(password, policy)?;
-        let cred_data = CredData {
-            sign,
-            enc,
-            writecap: None,
-        };
-        {
-            let mut guard = self.state.write().display_err()?;
-            guard.context.set_auth(&cred_data, password)?;
-        }
-        let mut creds = TpmCreds::new(cred_data, &self.state);
-        creds.init_root_writecap(Epoch::now() + Self::DEFAULT_WRITECAP_EXP)?;
-        self.persist(&creds, |state, handles| state.storage.root = Some(handles))?;
-        Ok(creds)
+        Ok(TpmCreds::new(key_handles, self.state.clone()))
     }
 
     fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
@@ -1126,7 +1058,9 @@ impl CredStore for TpmCredStore {
             writecap: root_creds.writecap.clone(),
         })
     }
+}
 
+impl CredStoreMut for TpmCredStore {
     fn import_root_creds(&self, password: &str, exported: ExportedCreds) -> Result<TpmCreds> {
         let aead_key = exported.params.derive_key(password)?;
         let auth = Auth::try_from(password.as_bytes())?;
@@ -1148,12 +1082,40 @@ impl CredStore for TpmCredStore {
                 enc,
                 writecap: exported.writecap,
             };
-            TpmCreds::new(cred_data, &self.state)
+            TpmCreds::new(cred_data, self.state.clone())
         };
         self.persist(&creds, |state, handles| state.storage.root = Some(handles))?;
         Ok(creds)
     }
 
+    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+        {
+            let guard = self.state.read().display_err()?;
+            if guard.storage.root.is_some() {
+                return Err(bterr!("root creds have already been generated"));
+            }
+        }
+        let policy = {
+            let mut guard = self.state.write().display_err()?;
+            guard.context.dup_with_password_policy()?
+        };
+        let sign = self.gen_root_sign_key(password, policy.clone())?;
+        let enc = self.gen_root_enc_key(password, policy)?;
+        let cred_data = CredData {
+            sign,
+            enc,
+            writecap: None,
+        };
+        {
+            let mut guard = self.state.write().display_err()?;
+            guard.context.set_auth(&cred_data, password)?;
+        }
+        let mut creds = TpmCreds::new(cred_data, self.state.clone());
+        creds.init_root_writecap(Epoch::now() + Self::DEFAULT_WRITECAP_EXP)?;
+        self.persist(&creds, |state, handles| state.storage.root = Some(handles))?;
+        Ok(creds)
+    }
+
     fn assign_node_writecap(
         &self,
         handle: &mut Self::CredHandle,
@@ -1167,7 +1129,7 @@ impl CredStore for TpmCredStore {
         if let Some(creds) = state.storage.node.as_mut() {
             creds.writecap = Some(writecap);
         }
-        self.save_storage(&mut state)
+        state.save()
     }
 
     fn assign_root_writecap(
@@ -1180,7 +1142,7 @@ impl CredStore for TpmCredStore {
         if let Some(creds) = state.storage.root.as_mut() {
             creds.writecap = Some(writecap);
         }
-        self.save_storage(&mut state)
+        state.save()
     }
 }
 
@@ -1310,11 +1272,11 @@ pub struct TpmCreds {
 }
 
 impl TpmCreds {
-    fn new(key_handles: CredData, state: &Arc<RwLock<State>>) -> TpmCreds {
+    fn new(key_handles: CredData, state: Arc<RwLock<State>>) -> TpmCreds {
         TpmCreds {
             sign: key_handles.sign,
             enc: key_handles.enc,
-            state: state.clone(),
+            state,
             writecap: key_handles.writecap,
         }
     }
@@ -1520,8 +1482,9 @@ mod test {
     use super::*;
 
     use crate::{error::AnyhowErrorExt, test_helpers::BtCursor};
+    use btserde::read_from;
     use ctor::ctor;
-    use std::{fs::File, io::SeekFrom};
+    use std::{fs::File, io::SeekFrom, os::unix::fs::PermissionsExt};
     use swtpm_harness::SwtpmHarness;
     use tss_esapi::{
         interface_types::ecc::EccCurve,

+ 1 - 0
crates/btlib/src/lib.rs

@@ -6,6 +6,7 @@
 //! [BlockMeta] struct is the type which implements block metadata.
 
 pub mod accessor;
+pub mod atomic_file;
 mod block_path;
 pub mod buf_reader;
 pub mod collections;

+ 14 - 20
crates/btprovision/src/main.rs

@@ -1,7 +1,10 @@
-use btconfig::{get_setting, get_settings, ConfigBuilderExt, CredStoreCfg, CredStoreConsumer};
+use btconfig::{
+    get_setting, get_settings, ConfigBuilderExt, CredStoreCfg, CredStoreConsumer,
+    CredStoreMutConsumer,
+};
 use btlib::{
     bterr,
-    crypto::{CredStore, Creds},
+    crypto::{CredStore, CredStoreMut, Creds},
     Decompose, Epoch, Principal, Principaled, RelBlockPath, Result, Writecap,
 };
 use btserde::{read_from, write_to};
@@ -63,9 +66,9 @@ struct RootProvisionConsumer<'a> {
     expires: Epoch,
 }
 
-impl<'a> CredStoreConsumer for RootProvisionConsumer<'a> {
+impl<'a> CredStoreMutConsumer for RootProvisionConsumer<'a> {
     type Output = Result<()>;
-    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+    fn consume_mut<C: CredStoreMut>(self, cred_store: C) -> Self::Output {
         cred_store.provision_root(self.password, self.expires)?;
         Ok(())
     }
@@ -83,25 +86,16 @@ fn gen_root_creds(config: AppConfig) -> Result<()> {
         password
     };
     let expires = get_setting!(config, writecapexpires);
-    config.credstore.consume(RootProvisionConsumer {
+    config.credstore.consume_mut(RootProvisionConsumer {
         password: &password,
         expires: Epoch::from_value(expires),
     })?
 }
 
-struct NodeProvisionConsumer;
-
-impl CredStoreConsumer for NodeProvisionConsumer {
-    type Output = Result<Principal>;
-    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
-        let node_creds = cred_store.node_creds()?;
-        Ok(node_creds.principal())
-    }
-}
-
 fn gen_node_creds(config: AppConfig) -> Result<()> {
-    let principal = config.credstore.consume(NodeProvisionConsumer)??;
-    println!("node principal: {}", principal);
+    let principal = config.credstore.consume(PrincipalConsumer)??;
+    eprint!("node principal: ");
+    println!("{principal}");
     Ok(())
 }
 
@@ -154,9 +148,9 @@ struct SaveNodeWritecapConsumer {
     writecap: Writecap,
 }
 
-impl CredStoreConsumer for SaveNodeWritecapConsumer {
+impl CredStoreMutConsumer for SaveNodeWritecapConsumer {
     type Output = Result<()>;
-    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+    fn consume_mut<C: CredStoreMut>(self, cred_store: C) -> Self::Output {
         let mut node_creds = cred_store.node_creds()?;
         cred_store.assign_node_writecap(&mut node_creds, self.writecap)?;
         Ok(())
@@ -176,7 +170,7 @@ fn save_node_writecap(config: AppConfig) -> Result<()> {
     };
     config
         .credstore
-        .consume(SaveNodeWritecapConsumer { writecap })?
+        .consume_mut(SaveNodeWritecapConsumer { writecap })?
 }
 
 struct PrincipalConsumer;

+ 1 - 1
crates/btserde/src/field_helpers.rs

@@ -3,7 +3,7 @@
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
 /// A helper for serializing and deserializing fields which are "smart pointers", types like
-/// [Arc], [Rc], and [Box].
+/// [std::sync::Arc], [std::rc::Rc], and [Box].
 pub mod smart_ptr {
     use super::*;
     use std::{ops::Deref, rc::Rc, sync::Arc};