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"
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",

View File

@ -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"

View File

@ -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;
}

View File

@ -172,6 +172,7 @@ fn load_operations(commands: Commands, config: &[ModuleConfig]) -> Vec<CLIOperat
pub fn handle(
workflow: &Workflow,
module_name: &str,
matches: &clap::ArgMatches,
modules: Commands,
config: &[ModuleConfig],
@ -194,6 +195,19 @@ pub fn handle(
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
.run_workflow(data, &operations, &derive_keys)
.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]
bincode = "1.3.3"
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 }
keyfork-prompt = { version = "0.2.0", registry = "distrust", default-features = false }
openpgp-card = "0.4"

View File

@ -14,6 +14,7 @@ use sequoia_openpgp::{
types::SignatureType,
Cert, Fingerprint,
};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
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.
#[error("a payload was provided when a non-payload JSON value was expected")]
UnexpectedPayloadProvided,
/// The JSON object is not a valid value.
#[error("the JSON object is not a valid value")]
InvalidJSONValue,
}
impl BaseError {
@ -87,14 +92,17 @@ fn canonicalize(value: Value) -> Value {
}
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)?;
Ok(bincoded)
}
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
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<dyn Digest>, Box<dyn std::error::Error>> {
#[derive(Serialize, Deserialize, Debug)]
pub struct Payload {
workflow: [String; 2],
values: Value,
datetime: DateTime<Utc>,
#[serde(default)]
signatures: Vec<String>,
}
@ -184,6 +195,16 @@ fn format_name(input: impl AsRef<str>) -> 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<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.
///
/// # 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<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 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,

View File

@ -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<PathBuf>,
/// 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 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)?
}
};