|
@@ -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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|