소스 검색

Added a crate for provisioning credential stores.

Matthew Carr 1 년 전
부모
커밋
f14464c21a

+ 278 - 2
Cargo.lock

@@ -17,6 +17,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "0.7.19"
@@ -164,6 +175,25 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "btconfig"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "btlib",
+ "config",
+ "serde",
+]
+
 [[package]]
 name = "btfproto"
 version = "0.1.0"
@@ -307,6 +337,20 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "btprovision"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "btconfig",
+ "btlib",
+ "btserde",
+ "config",
+ "serde",
+ "tempdir",
+ "termion",
+]
+
 [[package]]
 name = "btserde"
 version = "0.1.0"
@@ -454,6 +498,25 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "config"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
+dependencies = [
+ "async-trait",
+ "json5",
+ "lazy_static",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "yaml-rust",
+]
+
 [[package]]
 name = "const-oid"
 version = "0.9.1"
@@ -476,6 +539,15 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "criterion"
 version = "0.4.0"
@@ -555,6 +627,16 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "ctor"
 version = "0.1.23"
@@ -629,6 +711,22 @@ dependencies = [
  "const-oid",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
+
 [[package]]
 name = "either"
 version = "1.8.0"
@@ -804,6 +902,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.8"
@@ -838,6 +946,9 @@ name = "hashbrown"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
 
 [[package]]
 name = "heck"
@@ -940,6 +1051,17 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -986,6 +1108,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
 [[package]]
 name = "log"
 version = "0.4.17"
@@ -1133,6 +1261,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "numtoa"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
+
 [[package]]
 name = "object"
 version = "0.29.0"
@@ -1208,6 +1342,16 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "ordered-multimap"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
+dependencies = [
+ "dlv-list",
+ "hashbrown",
+]
+
 [[package]]
 name = "os_str_bytes"
 version = "6.4.1"
@@ -1220,6 +1364,12 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
 
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
@@ -1237,14 +1387,48 @@ dependencies = [
 
 [[package]]
 name = "pest"
-version = "2.3.1"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048"
+checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
 dependencies = [
  "thiserror",
  "ucd-trie",
 ]
 
+[[package]]
+name = "pest_derive"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.13",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
 [[package]]
 name = "picky-asn1"
 version = "0.3.3"
@@ -1501,6 +1685,24 @@ dependencies = [
  "rand_core 0.3.1",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_termios"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
+dependencies = [
+ "redox_syscall",
+]
+
 [[package]]
 name = "regex"
 version = "1.6.0"
@@ -1542,6 +1744,27 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "ron"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
+dependencies = [
+ "base64",
+ "bitflags",
+ "serde",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.21"
@@ -1751,6 +1974,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
 [[package]]
 name = "shlex"
 version = "1.1.0"
@@ -1909,6 +2143,18 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "termion"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
+dependencies = [
+ "libc",
+ "numtoa",
+ "redox_syscall",
+ "redox_termios",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.16.0"
@@ -2039,6 +2285,15 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "tracing"
 version = "0.1.37"
@@ -2104,6 +2359,12 @@ dependencies = [
  "target-lexicon",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
 [[package]]
 name = "ucd-trie"
 version = "0.1.5"
@@ -2140,6 +2401,12 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "vm-memory"
 version = "0.9.0"
@@ -2417,6 +2684,15 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.6.1"

+ 12 - 0
crates/btconfig/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "btconfig"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+config = "0.13.3"
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+serde = { version = "^1.0.136", features = ["derive"] }

+ 226 - 0
crates/btconfig/src/lib.rs

@@ -0,0 +1,226 @@
+use btlib::{
+    self, bterr,
+    crypto::{file_cred_store::FileCredStore, tpm::TpmCredStore, CredStore},
+    Result,
+};
+use config::{
+    builder::{AsyncState, DefaultState},
+    ConfigBuilder, Environment,
+};
+use serde::{de::Visitor, Deserialize, Deserializer};
+use std::{path::PathBuf, result::Result as StdResult, str::FromStr};
+
+/// Attempts to unwrap the [Option] field in `$config` called `$name`. This macro internal
+/// uses the `?` operator, and can only be used in method which return a [Result] which has an
+/// error type that a [btlib::Error] can be converted to.
+#[macro_export]
+macro_rules! get_setting {
+    ($config:expr, $name:ident) => {
+        $config
+            .$name
+            .ok_or_else(|| bterr!(concat!("setting '", stringify!($name), "' was not set")))?
+    };
+}
+
+/// Attempts to unwrap all of the [Option] fields with the given names in the configuration struct
+/// and returns them all as a tuple. The same consideration which apply to [get_setting!] apply to
+/// this macro.
+#[macro_export]
+macro_rules! get_settings {
+    ($config:expr$(, $names:ident)+) => {
+        ($(get_setting!($config, $names),)+)
+    };
+}
+
+fn environment_source() -> Environment {
+    Environment::with_prefix("BT").separator("_")
+}
+
+/// An extension to [ConfigBuilder] which allows it to be configured using Blocktree's default
+/// settings.
+pub trait ConfigBuilderExt {
+    /// Configure this [ConfigBuilder] with Blocktree's default settings.
+    fn btconfig(self) -> Self;
+}
+
+impl ConfigBuilderExt for ConfigBuilder<DefaultState> {
+    fn btconfig(self) -> Self {
+        self.add_source(environment_source())
+    }
+}
+
+impl ConfigBuilderExt for ConfigBuilder<AsyncState> {
+    fn btconfig(self) -> Self {
+        self.add_source(environment_source())
+    }
+}
+
+/// A struct which assists with very simple parsing of a configuration string.
+struct StrParser<'a>(&'a str);
+
+impl<'a> StrParser<'a> {
+    /// Assert that the given token occurs at the current position in the input `str`.
+    fn assert(&mut self, token: &str) -> btlib::Result<()> {
+        if self.0.starts_with(token) {
+            self.0 = &self.0[token.len()..];
+            Ok(())
+        } else {
+            Err(bterr!("string does not start with {token}"))
+        }
+    }
+
+    /// Consumes all characters up to `stop`, and returns them. Note that the returned `str` does
+    /// not contain `stop`, and the current position in the `str` is set to the next character after
+    /// `stop`. If `stop` is not encountered, then the remaining `str` is returned.
+    fn consume_up_to(&mut self, stop: char) -> &str {
+        let mut count = 0;
+        for c in self.0.chars() {
+            if c == stop {
+                break;
+            }
+            count += 1;
+        }
+        let output = &self.0[..count];
+        let new_start = self.0.len().min(count + 1);
+        self.0 = &self.0[new_start..];
+        output
+    }
+
+    /// Returns the remaining unconsumed part of the `str`.
+    fn remaining(&self) -> &str {
+        self.0
+    }
+}
+
+#[derive(Debug, Clone)]
+/// Configuration which specifies how to construct a [btlib::crypto::CredStore].
+pub enum CredStoreCfg {
+    /// Specifies that a [btlib::crypto::file_cred_store::FileCredStore] should be used.
+    File {
+        /// The path where the credential store is located.
+        path: PathBuf,
+    },
+    /// Specifies that a [btlib::crypto::tpm::TpmCredStore] should be used.
+    Tpm {
+        /// The state path to use for the TPM credential store.
+        tpm_state_path: PathBuf,
+        /// The configuration string to pass to `tss-esapi`.
+        tabrmd: String,
+    },
+}
+
+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 {
+                tpm_state_path,
+                tabrmd,
+            } => {
+                let store = TpmCredStore::from_tabrmd(&tabrmd, tpm_state_path)?;
+                Ok(consumer.consume(store))
+            }
+        }
+    }
+}
+
+impl FromStr for CredStoreCfg {
+    type Err = btlib::Error;
+    fn from_str(value: &str) -> StdResult<Self, Self::Err> {
+        let mut parser = StrParser(value);
+        parser.assert("kind=")?;
+        let kind = parser.consume_up_to(',');
+        match kind {
+            "file" => {
+                parser.assert("path=")?;
+                let path = PathBuf::from(parser.remaining());
+                Ok(CredStoreCfg::File { path })
+            }
+            "tpm" => {
+                parser.assert("tpm_state_path=")?;
+                let tpm_state_path = PathBuf::from(parser.consume_up_to(','));
+                let tabrmd = parser.remaining().to_string();
+                Ok(CredStoreCfg::Tpm {
+                    tpm_state_path,
+                    tabrmd,
+                })
+            }
+            _ => Err(bterr!(
+                "unrecognized CredStore kind (expected 'file' or 'tpm'): {kind}"
+            )),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for CredStoreCfg {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
+        struct StructVisitor;
+
+        impl<'a> Visitor<'a> for StructVisitor {
+            type Value = CredStoreCfg;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                write!(formatter, "cred store configuration string")
+            }
+
+            fn visit_str<E: serde::de::Error>(self, v: &str) -> StdResult<Self::Value, E> {
+                CredStoreCfg::from_str(v).map_err(serde::de::Error::custom)
+            }
+        }
+
+        deserializer.deserialize_str(StructVisitor)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use serde::de::value::StrDeserializer;
+
+    use super::*;
+
+    #[test]
+    fn deserialize_file_cred_store_config() {
+        const PATH: &str = "./state/cred_store";
+        let input = format!("kind=file,path={PATH}");
+        let deserializer = StrDeserializer::<'_, serde::de::value::Error>::new(input.as_str());
+
+        let actual = CredStoreCfg::deserialize(deserializer).unwrap();
+
+        let passed = if let CredStoreCfg::File { path } = actual {
+            path.to_str().unwrap() == PATH
+        } else {
+            false
+        };
+        assert!(passed);
+    }
+
+    #[test]
+    fn deserialize_tpm_cred_store_config() {
+        const STATE_PATH: &str = "./state/cred_store";
+        const TABRMD: &str = "bus_type=session";
+        let input = format!("kind=tpm,tpm_state_path={STATE_PATH},{TABRMD}");
+        let deserializer = StrDeserializer::<'_, serde::de::value::Error>::new(input.as_str());
+
+        let actual = CredStoreCfg::deserialize(deserializer).unwrap();
+
+        let passed = if let CredStoreCfg::Tpm {
+            tpm_state_path,
+            tabrmd,
+        } = actual
+        {
+            tpm_state_path.to_str().unwrap() == STATE_PATH && &tabrmd == TABRMD
+        } else {
+            false
+        };
+        assert!(passed);
+    }
+}

+ 2 - 2
crates/btfproto-tests/src/lib.rs

@@ -19,7 +19,7 @@ lazy_static! {
         let writecap = root_creds
             .issue_writecap(root_creds.principal(), vec![], one_hour_hence())
             .unwrap();
-        root_creds.set_writecap(writecap);
+        root_creds.set_writecap(writecap).unwrap();
         root_creds
     };
     static ref NODE_CREDS: ConcreteCreds = {
@@ -28,7 +28,7 @@ lazy_static! {
         let writecap = root_creds
             .issue_writecap(node_creds.principal(), vec![], one_hour_hence())
             .unwrap();
-        node_creds.set_writecap(writecap);
+        node_creds.set_writecap(writecap).unwrap();
         node_creds
     };
 }

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

@@ -722,7 +722,7 @@ mod tests {
                 Epoch::now() + Duration::from_secs(3600),
             )
             .unwrap();
-        creds.set_writecap(writecap);
+        creds.set_writecap(writecap).unwrap();
         let expected = IssuedProcRec {
             addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
             pub_creds: creds.concrete_pub(),
@@ -772,7 +772,7 @@ mod tests {
                 Epoch::now() + Duration::from_secs(3600),
             )
             .unwrap();
-        creds.set_writecap(writecap);
+        creds.set_writecap(writecap).unwrap();
         let expected = IssuedProcRec {
             addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
             pub_creds: creds.concrete_pub(),

+ 1 - 1
crates/btlib/benches/block_benches.rs

@@ -23,7 +23,7 @@ fn block_write(c: &mut Criterion) {
             Epoch::now() + Duration::from_secs(3600),
         )
         .expect("failed to issue writecap");
-    node_creds.set_writecap(writecap);
+    node_creds.set_writecap(writecap).unwrap();
 
     let mut fs_path = temp_dir.path().to_owned();
     fs_path.extend(components.iter());

+ 26 - 3
crates/btlib/src/block_path.rs

@@ -13,6 +13,8 @@ use std::{fmt::Display, hash::Hasher};
 pub use private::{BlockPath, BlockPathError, RelBlockPath};
 
 mod private {
+    use crate::Decompose;
+
     use super::*;
 
     /// Represents a relative block path.
@@ -23,11 +25,29 @@ mod private {
 
     impl RelBlockPath {
         /// Returns an iterator over the components of this relative path.
-        pub fn components(&self) -> impl Iterator<Item = &str> {
+        pub fn components(&self) -> impl DoubleEndedIterator<Item = &str> {
             self.components.iter().map(|e| e.as_str())
         }
     }
 
+    impl Decompose<Vec<String>> for RelBlockPath {
+        fn into_inner(self) -> Vec<String> {
+            self.components
+        }
+    }
+
+    impl<'a> TryFrom<&'a str> for RelBlockPath {
+        type Error = BlockPathError;
+
+        fn try_from(value: &'a str) -> std::result::Result<Self, Self::Error> {
+            if value.starts_with(BlockPath::SEP) {
+                return Err(BlockPathError::NotRelative);
+            }
+            let components = value.split(BlockPath::SEP).map(|s| s.to_string()).collect();
+            Ok(RelBlockPath { components })
+        }
+    }
+
     /// An identifier for a block in a tree.
     #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default, Hash)]
     pub struct BlockPath {
@@ -117,12 +137,12 @@ mod private {
         }
 
         /// Returns an iterator over the components in this path.
-        pub fn components(&self) -> impl Iterator<Item = &str> {
+        pub fn components(&self) -> impl DoubleEndedIterator<Item = &str> {
             self.components.iter().map(|e| e.as_str())
         }
 
         /// Returns an iterator over mutable references to the components in this path.
-        pub fn mut_components(&mut self) -> impl Iterator<Item = &mut String> {
+        pub fn mut_components(&mut self) -> impl DoubleEndedIterator<Item = &mut String> {
             self.components.iter_mut()
         }
 
@@ -221,6 +241,8 @@ mod private {
         InvalidLeadingComponent,
         /// Occurs when one path was expected to be contained in another, but that was not the case.
         NotContained,
+        /// Indicates that a relative path was expected, but an absolute path was encountered.
+        NotRelative,
     }
 
     impl Display for BlockPathError {
@@ -241,6 +263,7 @@ mod private {
                 BlockPathError::NotContained => {
                     formatter.write_str("one path was not contained in another")
                 }
+                BlockPathError::NotRelative => formatter.write_str("expected a relative path"),
             }
         }
     }

+ 45 - 3
crates/btlib/src/crypto.rs

@@ -115,6 +115,8 @@ pub enum Error {
     Library(Box<dyn ::std::error::Error + Send + Sync + 'static>),
     /// Occurs when an attempt is made to finish an [Op] that is already finished.
     OpAlreadyFinished,
+    /// Indicates that a writecap was not issued to a particular [Principal].
+    NotIssuedTo,
 }
 
 impl Error {
@@ -167,6 +169,7 @@ impl Display for Error {
             }
             Error::Library(err) => err.fmt(f),
             Error::OpAlreadyFinished => write!(f, "operation is already finished"),
+            Error::NotIssuedTo => write!(f, "writecap was not issued to the given principal"),
         }
     }
 }
@@ -1814,8 +1817,10 @@ impl ConcreteCreds {
         })
     }
 
-    pub fn set_writecap(&mut self, writecap: Writecap) {
-        self.writecap = Some(writecap)
+    pub fn set_writecap(&mut self, writecap: Writecap) -> Result<()> {
+        writecap.assert_issued_to(&self.principal())?;
+        self.writecap = Some(writecap);
+        Ok(())
     }
 
     pub fn sign_pair(&self) -> &AsymKeyPair<Sign> {
@@ -2327,7 +2332,7 @@ pub trait Creds: CredsPriv + CredsPub + Send + Sync {
     }
 }
 
-impl<C: CredsPriv + CredsPub + Clone + Send + Sync> Creds for C {}
+impl<C: ?Sized + CredsPriv + CredsPub + Send + Sync> Creds for C {}
 
 /// A trait for types which store credentials.
 pub trait CredStore {
@@ -2368,8 +2373,37 @@ pub trait CredStore {
         exported: Self::ExportedCreds,
     ) -> Result<Self::CredHandle>;
     /// Assigns the given [Writecap] to the node credentials referred to by the given handle.
+    ///  This method is responsible for committing the given [Writecap] to durable storage.
     fn assign_node_writecap(&self, handle: &mut Self::CredHandle, writecap: Writecap)
         -> Result<()>;
+    /// Assigns `writecap` to the root credentials referred to by `handle`. This method
+    /// is responsible for committing the given [Writecap] to durable storage.
+    fn assign_root_writecap(&self, handle: &mut Self::CredHandle, writecap: Writecap)
+        -> Result<()>;
+
+    /// Generates new root credentials protected by `password` and issues them a self-signed
+    /// [Writecap] which expires after `valid_for`. The newly generated root credentials are
+    /// returned.
+    fn provision_root(&self, password: &str, expires: Epoch) -> Result<Self::CredHandle> {
+        let mut root_creds = self.gen_root_creds(password)?;
+        let writecap = root_creds.issue_writecap(root_creds.principal(), vec![], expires)?;
+        self.assign_root_writecap(&mut root_creds, writecap)?;
+        Ok(root_creds)
+    }
+    /// Begin the provisioning process for a node by generating a new set of node credentials. The
+    /// [Principal] of the newly generated credentials is returned. This [Principal] may then be
+    /// transmitted to a root node which can use it to issue a [Writecap] to this node.
+    fn provision_node_start(&self) -> Result<Principal> {
+        let node_creds = self.node_creds()?;
+        Ok(node_creds.principal())
+    }
+    /// Assigns the given [Writecap] to the node credentials and commits it to durable storage.
+    /// A handle to the node credentials is returned.
+    fn provision_node_finish(&self, writecap: Writecap) -> Result<Self::CredHandle> {
+        let mut node_creds = self.node_creds()?;
+        self.assign_node_writecap(&mut node_creds, writecap)?;
+        Ok(node_creds)
+    }
 }
 
 impl<T: ?Sized + CredStore, P: Deref<Target = T>> CredStore for P {
@@ -2417,6 +2451,14 @@ impl<T: ?Sized + CredStore, P: Deref<Target = T>> CredStore for P {
     ) -> Result<()> {
         self.deref().assign_node_writecap(handle, writecap)
     }
+
+    fn assign_root_writecap(
+        &self,
+        handle: &mut Self::CredHandle,
+        writecap: Writecap,
+    ) -> Result<()> {
+        self.deref().assign_root_writecap(handle, writecap)
+    }
 }
 
 impl BlockMeta {

+ 50 - 10
crates/btlib/src/crypto/file_cred_store.rs

@@ -292,12 +292,25 @@ mod private {
             writecap: crate::Writecap,
         ) -> Result<()> {
             let node_creds = Arc::make_mut(handle);
-            node_creds.set_writecap(writecap);
+            node_creds.set_writecap(writecap)?;
             let mut state = self.state.write().display_err()?;
             state.node_creds = handle.clone();
             state.save(&self.file_path)?;
             Ok(())
         }
+
+        fn assign_root_writecap(
+            &self,
+            handle: &mut Self::CredHandle,
+            writecap: crate::Writecap,
+        ) -> Result<()> {
+            let root_creds = Arc::make_mut(handle);
+            root_creds.set_writecap(writecap)?;
+            let mut state = self.state.write().display_err()?;
+            state.root_creds = Some(handle.clone());
+            state.save(&self.file_path)?;
+            Ok(())
+        }
     }
 }
 
@@ -479,16 +492,20 @@ mod test {
     }
 
     #[test]
-    fn writecap_persisted() {
+    fn node_writecap_persisted() {
         let case = TestCase::new();
-        let mut node_creds = case.node_creds().unwrap();
-        let root_creds = case.gen_root_creds("password").unwrap();
-        let expires = Epoch::now() + Duration::from_secs(3600);
-        let expected = root_creds
-            .issue_writecap(node_creds.principal(), vec![], expires)
-            .unwrap();
-        case.assign_node_writecap(&mut node_creds, expected.clone())
-            .unwrap();
+        let expected = {
+            let mut node_creds = case.node_creds().unwrap();
+            let root_creds = case.gen_root_creds("password").unwrap();
+            let expires = Epoch::now() + Duration::from_secs(3600);
+            let expected = root_creds
+                .issue_writecap(node_creds.principal(), vec![], expires)
+                .unwrap();
+            case.assign_node_writecap(&mut node_creds, expected.clone())
+                .unwrap();
+            assert_eq!(&expected, node_creds.writecap().unwrap());
+            expected
+        };
 
         let case = TestCase::from_dir(case.dir);
         let node_creds = case.node_creds().unwrap();
@@ -496,4 +513,27 @@ mod test {
 
         assert_eq!(&expected, actual);
     }
+
+    #[test]
+    fn root_writecap_persisted() {
+        const PASSWORD: &str = "Intransigent";
+        let case = TestCase::new();
+        let expected = {
+            let mut root_creds = case.gen_root_creds(PASSWORD).unwrap();
+            let expires = Epoch::now() + Duration::from_secs(3600);
+            let expected = root_creds
+                .issue_writecap(root_creds.principal(), vec![], expires)
+                .unwrap();
+            case.assign_root_writecap(&mut root_creds, expected.clone())
+                .unwrap();
+            assert_eq!(&expected, root_creds.writecap().unwrap());
+            expected
+        };
+
+        let case = TestCase::from_dir(case.dir);
+        let root_creds = case.root_creds(PASSWORD).unwrap();
+        let actual = root_creds.writecap().unwrap();
+
+        assert_eq!(&expected, actual);
+    }
 }

+ 55 - 8
crates/btlib/src/crypto/tpm.rs

@@ -1159,7 +1159,7 @@ impl CredStore for TpmCredStore {
         handle: &mut Self::CredHandle,
         writecap: Writecap,
     ) -> Result<()> {
-        handle.writecap = Some(writecap.clone());
+        handle.set_writecap(writecap.clone())?;
         let mut state = self.state.write().display_err()?;
         if let Some(creds) = state.node_creds.as_mut() {
             creds.writecap = Some(writecap.clone());
@@ -1169,6 +1169,19 @@ impl CredStore for TpmCredStore {
         }
         self.save_storage(&mut state)
     }
+
+    fn assign_root_writecap(
+        &self,
+        handle: &mut Self::CredHandle,
+        writecap: Writecap,
+    ) -> Result<()> {
+        handle.set_writecap(writecap.clone())?;
+        let mut state = self.state.write().display_err()?;
+        if let Some(creds) = state.storage.root.as_mut() {
+            creds.writecap = Some(writecap);
+        }
+        self.save_storage(&mut state)
+    }
 }
 
 impl<S: Scheme> AsymKeyPub<S> {
@@ -1311,6 +1324,12 @@ impl TpmCreds {
         self.writecap = Some(writecap);
         Ok(())
     }
+
+    fn set_writecap(&mut self, writecap: Writecap) -> Result<()> {
+        writecap.assert_issued_to(&self.principal())?;
+        self.writecap = Some(writecap);
+        Ok(())
+    }
 }
 
 impl Principaled for TpmCreds {
@@ -2003,24 +2022,24 @@ mod test {
         Ok(())
     }
 
-    /// Ensures that the [Writecap] assigned using one instance of [TpmCredStore] is available from
-    /// another.
+    /// Ensures that the [Writecap] assigned to the node credentials using one instance of
+    /// [TpmCredStore] is available from another.
     #[test]
-    fn writecap_persisted_between_cred_stores() {
+    fn node_writecap_persisted() {
         let (harness, store) = test_store().unwrap();
         let expected = {
             let root_creds = store.gen_root_creds("TURTLES").unwrap();
             let mut node_creds = store.node_creds().unwrap();
             let expires = Epoch::now() + Duration::from_secs(3600);
-            let writecap = root_creds
+            let expected = root_creds
                 .issue_writecap(node_creds.principal(), vec![], expires)
                 .unwrap();
             store
-                .assign_node_writecap(&mut node_creds, writecap.clone())
+                .assign_node_writecap(&mut node_creds, expected.clone())
                 .unwrap();
-            writecap
+            assert_eq!(&expected, node_creds.writecap().unwrap());
+            expected
         };
-        drop(store);
 
         let store =
             TpmCredStore::from_tabrmd(harness.tabrmd_config(), harness.state_path().to_owned())
@@ -2031,6 +2050,34 @@ mod test {
         assert_eq!(&expected, actual);
     }
 
+    /// Tests that the [Writecap] for the root credentials is persisted across different instances
+    /// of [TpmCredStore].
+    #[test]
+    fn root_writecap_persisted() {
+        const PASSWORD: &str = "SuddenUnscheduledDisassembly";
+        let (harness, store) = test_store().unwrap();
+        let expected = {
+            let mut root_creds = store.gen_root_creds(PASSWORD).unwrap();
+            let expires = Epoch::now() + Duration::from_secs(3600);
+            let expected = root_creds
+                .issue_writecap(root_creds.principal(), vec![], expires)
+                .unwrap();
+            store
+                .assign_root_writecap(&mut root_creds, expected.clone())
+                .unwrap();
+            assert_eq!(&expected, root_creds.writecap().unwrap());
+            expected
+        };
+
+        let store =
+            TpmCredStore::from_context(harness.context().unwrap(), harness.state_path().to_owned())
+                .unwrap();
+        let root_creds = store.root_creds(PASSWORD).unwrap();
+        let actual = root_creds.writecap().unwrap();
+
+        assert_eq!(&expected, actual);
+    }
+
     /// Checks that when a writecap is assigned to the node creds it is present in a subsequently
     /// returned instance of the node creds.
     #[test]

+ 1 - 1
crates/btlib/src/crypto/x509.rs

@@ -633,7 +633,7 @@ mod tests {
                 Epoch::now() + Duration::from_secs(3600),
             )
             .unwrap();
-        process_creds.set_writecap(writecap);
+        process_creds.set_writecap(writecap).unwrap();
         let expected_key = process_creds.public_sign();
         let expected_wc = process_creds.writecap().unwrap();
 

+ 17 - 3
crates/btlib/src/lib.rs

@@ -1162,6 +1162,11 @@ impl Writecap {
         &self.body.issued_to
     }
 
+    /// Returns the [Epoch] representing the instant when this writecap expires.
+    pub fn expires(&self) -> Epoch {
+        self.body.expires
+    }
+
     /// Returns the principal of the root key which was used to sign this writecap.
     pub fn root_principal(&self) -> Principal {
         self.root_signing_key().principal()
@@ -1183,6 +1188,15 @@ impl Writecap {
         path.push_component(self.body.issued_to.to_string());
         path
     }
+
+    /// Returns [Ok] if and only if this [Writecap] was issued to `principal`.
+    pub fn assert_issued_to(&self, principal: &Principal) -> Result<()> {
+        if self.issued_to() == principal {
+            Ok(())
+        } else {
+            Err(crypto::Error::NotIssuedTo.into())
+        }
+    }
 }
 
 /// Fragments are created from blocks using Erasure Encoding and stored with other nodes in the
@@ -1567,7 +1581,7 @@ mod tests {
                         Epoch::now() + Duration::from_secs(3600),
                     )
                     .expect("failed to issue writecap");
-                node_creds.set_writecap(writecap);
+                node_creds.set_writecap(writecap).unwrap();
                 node_creds
             };
             let block_path = BlockPath::new(root_creds().principal(), components);
@@ -1666,7 +1680,7 @@ mod tests {
                     Epoch::now() + Duration::from_secs(3600),
                 )
                 .expect("failed to issue writecap");
-            node_creds.set_writecap(writecap);
+            node_creds.set_writecap(writecap).unwrap();
             let case = BlockTestCase {
                 temp_dir,
                 node_path: BlockPath::new(root_creds.principal(), components),
@@ -1801,7 +1815,7 @@ mod tests {
                     Epoch::now() + Duration::from_secs(60),
                 )
                 .expect("failed to issue writecap");
-            app_creds.set_writecap(writecap);
+            app_creds.set_writecap(writecap).unwrap();
             app_creds
         };
         {

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

@@ -100,7 +100,7 @@ lazy_static! {
         let writecap = ROOT_CREDS
             .issue_writecap(node_creds.principal(), vec![], expires)
             .expect("failed to issue writecap to test_helpers::NODE_CREDS");
-        node_creds.set_writecap(writecap);
+        node_creds.set_writecap(writecap).unwrap();
         node_creds
     };
 }

+ 2 - 2
crates/btmsg/tests/tests.rs

@@ -52,7 +52,7 @@ lazy_static! {
                 Epoch::now() + Duration::from_secs(3600),
             )
             .unwrap();
-        creds.set_writecap(writecap);
+        creds.set_writecap(writecap).unwrap();
         creds
     };
     static ref ROOT_PRINCIPAL: Principal = ROOT_CREDS.principal();
@@ -135,7 +135,7 @@ fn proc_creds() -> impl Creds {
             Epoch::now() + Duration::from_secs(3600),
         )
         .unwrap();
-    creds.set_writecap(writecap);
+    creds.set_writecap(writecap).unwrap();
     creds
 }
 

+ 19 - 0
crates/btprovision/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "btprovision"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+btconfig = { path = "../btconfig" }
+btserde = { path = "../btserde" }
+serde = { version = "^1.0.136", features = ["derive"] }
+config = "0.13.3"
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+termion = "2.0.1"
+
+
+[dev-dependencies]
+tempdir = { version = "0.3.7" }

+ 336 - 0
crates/btprovision/src/main.rs

@@ -0,0 +1,336 @@
+use btconfig::{get_setting, get_settings, ConfigBuilderExt, CredStoreCfg, CredStoreConsumer};
+use btlib::{
+    bterr,
+    crypto::{CredStore, Creds},
+    Decompose, Epoch, Principal, Principaled, RelBlockPath, Result, Writecap,
+};
+use btserde::{read_from, write_to};
+use config::Config;
+use serde::Deserialize;
+use std::{
+    fs::OpenOptions,
+    io::{BufReader, BufWriter, Cursor, Write},
+    path::PathBuf,
+    time::Duration,
+};
+use termion::input::TermRead;
+
+#[derive(Debug, Deserialize)]
+struct AppConfig {
+    /// The configuration for the [CredStore] to use.
+    credstore: CredStoreCfg,
+    /// The root password.
+    password: Option<String>,
+    /// The number of seconds after the UNIX epoch when the credentials expire.
+    writecapexpires: Option<u64>,
+    writecappath: Option<String>,
+    writecapissuee: Option<String>,
+    writecapsavepath: Option<PathBuf>,
+}
+
+impl AppConfig {
+    fn new() -> Result<Self> {
+        const DEFAULT_VALID_FOR: u64 = 60 * 60 * 24 * 365;
+        let expires = Epoch::now() + Duration::from_secs(DEFAULT_VALID_FOR);
+        Ok(Config::builder()
+            .set_default("writecapexpires", expires.value().to_string().as_str())?
+            .btconfig()
+            .build()?
+            .try_deserialize()?)
+    }
+}
+
+fn password_prompt(prompt: &str) -> Result<String> {
+    let mut stdout = std::io::stdout();
+    stdout.write_all(prompt.as_bytes())?;
+    stdout.flush()?;
+    let mut reader = BufReader::new(std::io::stdin());
+    let mut cursor = Cursor::new(Vec::new());
+    let line = if let Some(line) = reader.read_passwd(&mut cursor)? {
+        line
+    } else {
+        return Err(bterr!("failed to read password"));
+    };
+    writeln!(stdout)?;
+    stdout.flush()?;
+    Ok(line)
+}
+
+struct RootProvisionConsumer<'a> {
+    password: &'a str,
+    expires: Epoch,
+}
+
+impl<'a> CredStoreConsumer for RootProvisionConsumer<'a> {
+    type Output = Result<()>;
+    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+        cred_store.provision_root(self.password, self.expires)?;
+        Ok(())
+    }
+}
+
+fn gen_root_creds(config: AppConfig) -> Result<()> {
+    let password = if let Some(password) = config.password {
+        password
+    } else {
+        let password = password_prompt("Please enter a root password: ")?;
+        let password_confirm = password_prompt("Please confirm your entry: ")?;
+        if password != password_confirm {
+            return Err(bterr!("Error: entries do not match"));
+        }
+        password
+    };
+    let expires = get_setting!(config, writecapexpires);
+    config.credstore.consume(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);
+    Ok(())
+}
+
+struct IssueWritecapConsumer<'a> {
+    password: &'a str,
+    issuee: Principal,
+    components: Vec<String>,
+    expires: Epoch,
+}
+
+impl<'a> CredStoreConsumer for IssueWritecapConsumer<'a> {
+    type Output = Result<Writecap>;
+    fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+        let root_creds = cred_store.root_creds(self.password)?;
+        root_creds.issue_writecap(self.issuee, self.components, self.expires)
+    }
+}
+
+fn issue_node_writecap(config: AppConfig) -> Result<()> {
+    let password = if let Some(password) = config.password {
+        password
+    } else {
+        password_prompt("Please enter the root password: ")?
+    };
+    let (writecap_path, cred_path, issuee, expires) = get_settings!(
+        config,
+        writecapsavepath,
+        writecappath,
+        writecapissuee,
+        writecapexpires
+    );
+    let cred_components = RelBlockPath::try_from(cred_path.as_str())?;
+    let issuee = Principal::try_from(issuee.as_str())?;
+    let writecap = config.credstore.consume(IssueWritecapConsumer {
+        password: &password,
+        components: cred_components.into_inner(),
+        expires: Epoch::from_value(expires),
+        issuee,
+    })??;
+    let file = OpenOptions::new()
+        .write(true)
+        .create_new(true)
+        .open(writecap_path)?;
+    let mut writer = BufWriter::new(file);
+    write_to(&writecap, &mut writer)?;
+    Ok(())
+}
+
+struct SaveNodeWritecapConsumer {
+    writecap: Writecap,
+}
+
+impl CredStoreConsumer for SaveNodeWritecapConsumer {
+    type Output = Result<()>;
+    fn consume<C: CredStore>(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(())
+    }
+}
+
+fn save_node_writecap(config: AppConfig) -> Result<()> {
+    let writecap = {
+        let writecap_path = get_setting!(config, writecapsavepath);
+        let file = OpenOptions::new()
+            .read(true)
+            .write(false)
+            .create(false)
+            .open(writecap_path)?;
+        let mut reader = BufReader::new(file);
+        read_from::<Writecap, _>(&mut reader)?
+    };
+    config
+        .credstore
+        .consume(SaveNodeWritecapConsumer { writecap })?
+}
+
+fn run(command: &str, config: AppConfig) -> Result<()> {
+    match command {
+        "gen_root_creds" => gen_root_creds(config),
+        "gen_node_creds" => gen_node_creds(config),
+        "issue_node_writecap" => issue_node_writecap(config),
+        "save_node_writecap" => save_node_writecap(config),
+        _ => Err(bterr!("unrecognized command: {command}")),
+    }
+}
+
+fn main() -> Result<()> {
+    let config = AppConfig::new()?;
+    let mut args = std::env::args().skip(1);
+    let command = args
+        .next()
+        .ok_or_else(|| bterr!("at least one command line argument expected"))?;
+    run(command.as_str(), config)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use btlib::{crypto::CredsPriv, BlockError};
+    use tempdir::TempDir;
+
+    struct PrincipalConsumer;
+
+    impl CredStoreConsumer for PrincipalConsumer {
+        type Output = String;
+        fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+            cred_store.node_creds().unwrap().principal().to_string()
+        }
+    }
+
+    struct NodeWritecapConsumer;
+
+    impl CredStoreConsumer for NodeWritecapConsumer {
+        type Output = Writecap;
+        fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+            let node_creds = cred_store.node_creds().unwrap();
+            node_creds
+                .writecap()
+                .ok_or(BlockError::MissingWritecap)
+                .unwrap()
+                .clone()
+        }
+    }
+
+    struct RootWritecapConsumer<'a> {
+        password: &'a str,
+    }
+
+    impl<'a> CredStoreConsumer for RootWritecapConsumer<'a> {
+        type Output = Writecap;
+        fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
+            let root_creds = cred_store.root_creds(self.password).unwrap();
+            root_creds
+                .writecap()
+                .ok_or(BlockError::MissingWritecap)
+                .unwrap()
+                .clone()
+        }
+    }
+
+    /// Verifies that the provisioning workflow succeeds.
+    #[test]
+    fn workflow() {
+        let expires_expected = (Epoch::now() + Duration::from_secs(7200)).value();
+        let writecap_path = "home/gboole".to_string();
+        let password = "devalued".to_string();
+        let dir = TempDir::new("btprovision").unwrap();
+        let writecap_save_path = dir.path().join("writecap");
+        let root_store = CredStoreCfg::File {
+            path: dir.path().join("root_store"),
+        };
+        let node_store = CredStoreCfg::File {
+            path: dir.path().join("node_store"),
+        };
+
+        run(
+            "gen_root_creds",
+            AppConfig {
+                credstore: root_store.clone(),
+                password: Some(password.clone()),
+                writecapexpires: Some(expires_expected),
+                writecappath: None,
+                writecapissuee: None,
+                writecapsavepath: None,
+            },
+        )
+        .unwrap();
+        // Verify that the root credentials in the root cred store have a valid writecap.
+        {
+            let consumer = RootWritecapConsumer {
+                password: &password,
+            };
+            let actual = root_store.clone().consume(consumer).unwrap();
+
+            actual.assert_valid_for(actual.path()).unwrap();
+            actual.assert_issued_to(&actual.root_principal()).unwrap();
+            assert_eq!(expires_expected, actual.expires().value());
+            assert!(std::iter::empty::<&str>().eq(actual.path().components()));
+        }
+        run(
+            "gen_node_creds",
+            AppConfig {
+                credstore: node_store.clone(),
+                password: None,
+                writecapexpires: None,
+                writecappath: None,
+                writecapissuee: None,
+                writecapsavepath: None,
+            },
+        )
+        .unwrap();
+        let issued_to_expected = node_store.clone().consume(PrincipalConsumer).unwrap();
+        run(
+            "issue_node_writecap",
+            AppConfig {
+                credstore: root_store,
+                password: Some(password),
+                writecapexpires: Some(expires_expected),
+                writecappath: Some(writecap_path.clone()),
+                writecapissuee: Some(issued_to_expected.clone()),
+                writecapsavepath: Some(writecap_save_path.clone()),
+            },
+        )
+        .unwrap();
+        run(
+            "save_node_writecap",
+            AppConfig {
+                credstore: node_store.clone(),
+                password: None,
+                writecapexpires: None,
+                writecappath: None,
+                writecapissuee: None,
+                writecapsavepath: Some(writecap_save_path),
+            },
+        )
+        .unwrap();
+        // Verify that the node credentials in the node cred store have a valid writecap.
+        {
+            let actual = node_store.consume(NodeWritecapConsumer).unwrap();
+
+            actual.assert_valid_for(actual.path()).unwrap();
+            assert_eq!(issued_to_expected, actual.issued_to().to_string());
+            let actual_path = actual
+                .path()
+                .relative_to(&actual.root_block_path())
+                .unwrap();
+            let expected_path = RelBlockPath::try_from(writecap_path.as_str()).unwrap();
+            assert_eq!(expected_path, actual_path);
+            assert_eq!(expires_expected, actual.expires().value());
+        }
+    }
+}