diff --git a/Cargo.lock b/Cargo.lock index fe9b150..97ad8b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1866,21 +1866,17 @@ dependencies = [ name = "icepick" version = "0.1.0" dependencies = [ - "bincode", - "card-backend-pcsc", + "chrono", "clap", "icepick-module", "icepick-workflow", "keyfork-derive-util", "keyforkd-client", "keyforkd-models", - "openpgp-card", - "openpgp-card-sequoia", - "sequoia-openpgp", + "miniquorum", "serde", "serde_json", "serde_yaml", - "sha3", "thiserror 2.0.11", "toml 0.8.19", ] @@ -2545,6 +2541,7 @@ version = "0.1.0" dependencies = [ "bincode", "card-backend-pcsc", + "chrono", "clap", "keyfork-prompt", "openpgp-card", diff --git a/crates/icepick/Cargo.toml b/crates/icepick/Cargo.toml index 44987c4..79551d3 100644 --- a/crates/icepick/Cargo.toml +++ b/crates/icepick/Cargo.toml @@ -4,12 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = { version = "0.4.39", default-features = false, features = ["now", "serde", "std"] } clap = { version = "4.5.20", features = ["cargo", "derive", "string"] } icepick-module = { version = "0.1.0", path = "../icepick-module" } icepick-workflow = { version = "0.1.0", path = "../icepick-workflow" } keyfork-derive-util = { version = "0.2.1", registry = "distrust" } keyforkd-client = { version = "0.2.1", registry = "distrust" } keyforkd-models = { version = "0.2.0", registry = "distrust" } +miniquorum = { version = "0.1.0", path = "../miniquorum", default-features = false } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml = "0.9.34" diff --git a/crates/icepick/src/cli/mod.rs b/crates/icepick/src/cli/mod.rs index 3863462..acc1bed 100644 --- a/crates/icepick/src/cli/mod.rs +++ b/crates/icepick/src/cli/mod.rs @@ -187,7 +187,13 @@ pub fn do_cli_thing() { let workflows = workflows.leak(); let mut workflow_command = clap::Command::new("workflow") .about("Run a pre-defined Icepick workflow") - .arg(clap::arg!(--"simulate-workflow").global(true)); + .arg(clap::arg!(--"simulate-workflow").global(true)) + .arg(clap::arg!(--"export-for-quorum").global(true)) + .arg( + clap::arg!(--"sign") + .global(true) + .requires_if(clap::builder::ArgPredicate::IsPresent, "export-for-quorum"), + ); for module in workflows.iter() { let mut module_subcommand = clap::Command::new(module.0.as_str()); for workflow in &module.1 { @@ -236,7 +242,7 @@ pub fn do_cli_thing() { .find(|(module, _)| module == module_name) .and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name)) .expect("workflow from CLI should match config"); - workflow::handle(workflow, matches, commands, &config.modules); + workflow::handle(workflow, module_name, matches, commands, &config.modules); return; } diff --git a/crates/icepick/src/cli/workflow.rs b/crates/icepick/src/cli/workflow.rs index fc4ee47..b89d9cd 100644 --- a/crates/icepick/src/cli/workflow.rs +++ b/crates/icepick/src/cli/workflow.rs @@ -172,6 +172,7 @@ fn load_operations(commands: Commands, config: &[ModuleConfig]) -> Vec Value { } fn unhashed(value: Value) -> Result, Box> { - let value = canonicalize(value); + let Value::Object(mut value) = value else { + return Err(BaseError::InvalidJSONValue.into()); + }; + value.remove("signatures"); + let value = canonicalize(Value::Object(value)); let bincoded = bincode::serialize(&value)?; Ok(bincoded) } fn hash(value: Value) -> Result, Box> { - let value = canonicalize(value); - let bincoded = bincode::serialize(&value)?; + let bincoded = unhashed(value)?; let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?; digest.update(&bincoded); @@ -103,7 +111,10 @@ fn hash(value: Value) -> Result, Box> { #[derive(Serialize, Deserialize, Debug)] pub struct Payload { + workflow: [String; 2], values: Value, + datetime: DateTime, + #[serde(default)] signatures: Vec, } @@ -184,6 +195,16 @@ fn format_name(input: impl AsRef) -> String { } impl Payload { + /// Create a new Payload, using the current system's time, in UTC. + pub fn new(values: serde_json::Value, module_name: impl AsRef, workflow_name: impl AsRef) -> Self { + Self { + workflow: [module_name.as_ref().to_string(), workflow_name.as_ref().to_string()], + values, + datetime: Utc::now(), + signatures: vec![], + } + } + /// Load a Payload and the relevant certificates. /// /// # Errors @@ -209,21 +230,13 @@ impl Payload { Ok((payload, certs)) } - /// Create an unsigned Payload from a [`serde_json::Value`]. - pub fn new(value: serde_json::Value) -> Self { - Self { - values: value, - signatures: vec![], - } - } - /// Attach a signature from an OpenPGP card. /// /// # Errors /// /// The method may error if a signature could not be created. pub fn add_signature(&mut self) -> Result<(), Box> { - let unhashed = unhashed(self.values.clone())?; + let unhashed = unhashed(serde_json::to_value(&self)?)?; let builder = SignatureBuilder::new(SignatureType::Binary); let mut prompt_handler = default_handler()?; let pin_validator = PinValidator { @@ -319,7 +332,7 @@ impl Payload { ))?; } - let hashed = hash(self.values.clone())?; + let hashed = hash(serde_json::to_value(self)?)?; let PayloadVerification { mut threshold, diff --git a/crates/miniquorum/src/main.rs b/crates/miniquorum/src/main.rs index 1f3326c..7430be5 100644 --- a/crates/miniquorum/src/main.rs +++ b/crates/miniquorum/src/main.rs @@ -32,9 +32,9 @@ enum MiniQuorum { AddSignature { /// The file to use as input. /// - /// If no file is provided, standard input of a payload value is used. If a file is - /// provided and no output file is provided, it will be used in-place as the output file - /// with the additional signature added. + /// If no file is provided, standard input is used. If a file is provided and no output + /// file is provided, it will be used in-place as the output file with the additional + /// signature added. input_file: Option, /// The file to use as output. @@ -68,29 +68,28 @@ fn main() -> Result<(), Box> { }; let policy = PayloadVerification::new().with_threshold(certs.len().try_into()?); - let inner_value = payload.verify_signatures(&certs, &policy, fingerprint)?; + payload.verify_signatures(&certs, &policy, fingerprint)?; + if let Some(output_file) = output_file { let file = File::create(output_file)?; - serde_json::to_writer_pretty(file, inner_value)?; + serde_json::to_writer_pretty(file, &payload)?; } else { let stdout = std::io::stdout(); - serde_json::to_writer_pretty(stdout, inner_value)?; + serde_json::to_writer_pretty(stdout, &payload)?; } } MiniQuorum::AddSignature { input_file, output_file, } => { - let mut payload = match &input_file { + let mut payload: Payload = match &input_file { Some(input_file) => { let input_file = File::open(input_file)?; - let payload: Payload = serde_json::from_reader(input_file)?; - payload + serde_json::from_reader(input_file)? } None => { let stdin = std::io::stdin(); - let value: serde_json::Value = serde_json::from_reader(stdin)?; - Payload::new(value) + serde_json::from_reader(stdin)? } };