embed miniquorum signer into icepick

This commit is contained in:
Ryan Heywood 2025-01-30 14:51:53 -05:00
parent e49d6be339
commit b91a55b93d
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
8 changed files with 106 additions and 32 deletions

9
Cargo.lock generated
View File

@ -1866,21 +1866,17 @@ dependencies = [
name = "icepick" name = "icepick"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "chrono",
"card-backend-pcsc",
"clap", "clap",
"icepick-module", "icepick-module",
"icepick-workflow", "icepick-workflow",
"keyfork-derive-util", "keyfork-derive-util",
"keyforkd-client", "keyforkd-client",
"keyforkd-models", "keyforkd-models",
"openpgp-card", "miniquorum",
"openpgp-card-sequoia",
"sequoia-openpgp",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha3",
"thiserror 2.0.11", "thiserror 2.0.11",
"toml 0.8.19", "toml 0.8.19",
] ]
@ -2545,6 +2541,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"card-backend-pcsc", "card-backend-pcsc",
"chrono",
"clap", "clap",
"keyfork-prompt", "keyfork-prompt",
"openpgp-card", "openpgp-card",

View File

@ -4,12 +4,14 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
chrono = { version = "0.4.39", default-features = false, features = ["now", "serde", "std"] }
clap = { version = "4.5.20", features = ["cargo", "derive", "string"] } clap = { version = "4.5.20", features = ["cargo", "derive", "string"] }
icepick-module = { version = "0.1.0", path = "../icepick-module" } icepick-module = { version = "0.1.0", path = "../icepick-module" }
icepick-workflow = { version = "0.1.0", path = "../icepick-workflow" } icepick-workflow = { version = "0.1.0", path = "../icepick-workflow" }
keyfork-derive-util = { version = "0.2.1", registry = "distrust" } keyfork-derive-util = { version = "0.2.1", registry = "distrust" }
keyforkd-client = { version = "0.2.1", registry = "distrust" } keyforkd-client = { version = "0.2.1", registry = "distrust" }
keyforkd-models = { version = "0.2.0", 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 = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
serde_yaml = "0.9.34" serde_yaml = "0.9.34"

View File

@ -187,7 +187,13 @@ pub fn do_cli_thing() {
let workflows = workflows.leak(); let workflows = workflows.leak();
let mut workflow_command = clap::Command::new("workflow") let mut workflow_command = clap::Command::new("workflow")
.about("Run a pre-defined Icepick 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() { for module in workflows.iter() {
let mut module_subcommand = clap::Command::new(module.0.as_str()); let mut module_subcommand = clap::Command::new(module.0.as_str());
for workflow in &module.1 { for workflow in &module.1 {
@ -236,7 +242,7 @@ pub fn do_cli_thing() {
.find(|(module, _)| module == module_name) .find(|(module, _)| module == module_name)
.and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name)) .and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name))
.expect("workflow from CLI should match config"); .expect("workflow from CLI should match config");
workflow::handle(workflow, matches, commands, &config.modules); workflow::handle(workflow, module_name, matches, commands, &config.modules);
return; return;
} }

View File

@ -172,6 +172,7 @@ fn load_operations(commands: Commands, config: &[ModuleConfig]) -> Vec<CLIOperat
pub fn handle( pub fn handle(
workflow: &Workflow, workflow: &Workflow,
module_name: &str,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
modules: Commands, modules: Commands,
config: &[ModuleConfig], config: &[ModuleConfig],
@ -194,6 +195,19 @@ pub fn handle(
return; return;
} }
if matches.get_flag("export-for-quorum") {
let mut payload = miniquorum::Payload::new(
serde_json::to_value(data).unwrap(),
module_name,
&workflow.name,
);
if matches.get_flag("sign") {
payload.add_signature().unwrap();
}
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
return;
}
let result = workflow let result = workflow
.run_workflow(data, &operations, &derive_keys) .run_workflow(data, &operations, &derive_keys)
.expect("Invocation failure"); .expect("Invocation failure");

View File

@ -0,0 +1,42 @@
name: withdraw-rewards
inputs:
- delegate_address
- validator_address
- chain_name
optional_inputs:
- gas_factor
step:
- type: cosmos-get-chain-info
inputs:
chain_name: chain_name
outputs:
blockchain_config: blockchain_config
- type: internal-load-file
values:
filename: "account_info.json"
outputs:
account_number: account_number
sequence_number: sequence_number
- type: cosmos-withdraw-rewards
inputs:
delegate_address: delegate_address
validator_address: validator_address
blockchain_config: blockchain_config
gas_factor: gas_factor
outputs:
fee: fee
tx_messages: tx_messages
- type: cosmos-sign
inputs:
fee: fee
tx_messages: tx_messages
account_number: account_number
sequence_number: sequence_number
blockchain_config: blockchain_config
outputs:
transaction: signed_transaction
- type: internal-save-file
values:
filename: "transaction.json"
inputs:
transaction: signed_transaction

View File

@ -9,6 +9,7 @@ default = ["clap"]
[dependencies] [dependencies]
bincode = "1.3.3" bincode = "1.3.3"
card-backend-pcsc = "0.5.0" card-backend-pcsc = "0.5.0"
chrono = { version = "0.4.39", default-features = false, features = ["std", "now", "serde"] }
clap = { version = "4.5.27", features = ["derive", "wrap_help"], optional = true } clap = { version = "4.5.27", features = ["derive", "wrap_help"], optional = true }
keyfork-prompt = { version = "0.2.0", registry = "distrust", default-features = false } keyfork-prompt = { version = "0.2.0", registry = "distrust", default-features = false }
openpgp-card = "0.4" openpgp-card = "0.4"

View File

@ -14,6 +14,7 @@ use sequoia_openpgp::{
types::SignatureType, types::SignatureType,
Cert, Fingerprint, Cert, Fingerprint,
}; };
use chrono::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::{collections::BTreeMap, fs::File, io::Read, path::Path}; use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
@ -55,6 +56,10 @@ pub enum BaseError {
/// A Payload was provided when an inner [`serde_json::Value`] was expected. /// A Payload was provided when an inner [`serde_json::Value`] was expected.
#[error("a payload was provided when a non-payload JSON value was expected")] #[error("a payload was provided when a non-payload JSON value was expected")]
UnexpectedPayloadProvided, UnexpectedPayloadProvided,
/// The JSON object is not a valid value.
#[error("the JSON object is not a valid value")]
InvalidJSONValue,
} }
impl BaseError { impl BaseError {
@ -87,14 +92,17 @@ fn canonicalize(value: Value) -> Value {
} }
fn unhashed(value: Value) -> Result<Vec<u8>, Box<dyn std::error::Error>> { fn unhashed(value: Value) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
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)?; let bincoded = bincode::serialize(&value)?;
Ok(bincoded) Ok(bincoded)
} }
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> { fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
let value = canonicalize(value); let bincoded = unhashed(value)?;
let bincoded = bincode::serialize(&value)?;
let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?; let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?;
digest.update(&bincoded); digest.update(&bincoded);
@ -103,7 +111,10 @@ fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Payload { pub struct Payload {
workflow: [String; 2],
values: Value, values: Value,
datetime: DateTime<Utc>,
#[serde(default)]
signatures: Vec<String>, signatures: Vec<String>,
} }
@ -184,6 +195,16 @@ fn format_name(input: impl AsRef<str>) -> String {
} }
impl Payload { 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<str>, workflow_name: impl AsRef<str>) -> 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. /// Load a Payload and the relevant certificates.
/// ///
/// # Errors /// # Errors
@ -209,21 +230,13 @@ impl Payload {
Ok((payload, certs)) 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. /// Attach a signature from an OpenPGP card.
/// ///
/// # Errors /// # Errors
/// ///
/// The method may error if a signature could not be created. /// The method may error if a signature could not be created.
pub fn add_signature(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub fn add_signature(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let unhashed = unhashed(self.values.clone())?; let unhashed = unhashed(serde_json::to_value(&self)?)?;
let builder = SignatureBuilder::new(SignatureType::Binary); let builder = SignatureBuilder::new(SignatureType::Binary);
let mut prompt_handler = default_handler()?; let mut prompt_handler = default_handler()?;
let pin_validator = PinValidator { 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 { let PayloadVerification {
mut threshold, mut threshold,

View File

@ -32,9 +32,9 @@ enum MiniQuorum {
AddSignature { AddSignature {
/// The file to use as input. /// The file to use as input.
/// ///
/// If no file is provided, standard input of a payload value is used. If a file is /// If no file is provided, standard input is used. If a file is provided and no output
/// provided and no output file is provided, it will be used in-place as the output file /// file is provided, it will be used in-place as the output file with the additional
/// with the additional signature added. /// signature added.
input_file: Option<PathBuf>, input_file: Option<PathBuf>,
/// The file to use as output. /// The file to use as output.
@ -68,29 +68,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}; };
let policy = PayloadVerification::new().with_threshold(certs.len().try_into()?); 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 { if let Some(output_file) = output_file {
let file = File::create(output_file)?; let file = File::create(output_file)?;
serde_json::to_writer_pretty(file, inner_value)?; serde_json::to_writer_pretty(file, &payload)?;
} else { } else {
let stdout = std::io::stdout(); let stdout = std::io::stdout();
serde_json::to_writer_pretty(stdout, inner_value)?; serde_json::to_writer_pretty(stdout, &payload)?;
} }
} }
MiniQuorum::AddSignature { MiniQuorum::AddSignature {
input_file, input_file,
output_file, output_file,
} => { } => {
let mut payload = match &input_file { let mut payload: Payload = match &input_file {
Some(input_file) => { Some(input_file) => {
let input_file = File::open(input_file)?; let input_file = File::open(input_file)?;
let payload: Payload = serde_json::from_reader(input_file)?; serde_json::from_reader(input_file)?
payload
} }
None => { None => {
let stdin = std::io::stdin(); let stdin = std::io::stdin();
let value: serde_json::Value = serde_json::from_reader(stdin)?; serde_json::from_reader(stdin)?
Payload::new(value)
} }
}; };