icepick workflow: improve miniquorum embeddedness
This commit is contained in:
parent
5c9b5533d6
commit
7d2909bada
|
@ -48,6 +48,20 @@ dependencies = [
|
|||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm-siv"
|
||||
version = "0.11.1"
|
||||
|
@ -99,6 +113,12 @@ dependencies = [
|
|||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
|
@ -413,7 +433,7 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
|
@ -468,6 +488,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blahaj"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5106bf2680d585dc5f29711b8aa5dde353180b8e14af89b7f0424f760c84e7ce"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"rand 0.8.5",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.5.5"
|
||||
|
@ -1492,6 +1523,12 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
|
@ -1653,6 +1690,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
|
@ -1718,6 +1765,11 @@ name = "hashbrown"
|
|||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
|
@ -1737,6 +1789,15 @@ version = "0.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5491a308e0214554f07a81d8944abe45f552871c12e3c3c6e7e5d354039a6c4c"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.8.1"
|
||||
|
@ -1871,6 +1932,8 @@ dependencies = [
|
|||
"icepick-module",
|
||||
"icepick-workflow",
|
||||
"keyfork-derive-util",
|
||||
"keyfork-prompt",
|
||||
"keyfork-shard",
|
||||
"keyforkd-client",
|
||||
"keyforkd-models",
|
||||
"miniquorum",
|
||||
|
@ -2247,6 +2310,31 @@ dependencies = [
|
|||
"signal-hook-mio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-derive-openpgp"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||
checksum = "da78a1c0b9dc65463bf6adc4e04efb1a733cd49172416a8dcdf59749668e26d0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ed25519-dalek 2.1.1",
|
||||
"keyfork-derive-path-data",
|
||||
"keyfork-derive-util",
|
||||
"keyforkd-client",
|
||||
"sequoia-openpgp",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-derive-path-data"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||
checksum = "f3446ddf10e1ffc1394409c856e1c45da37fcdf8302e53ea51a0f7418bd14382"
|
||||
dependencies = [
|
||||
"keyfork-derive-util",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-derive-util"
|
||||
version = "0.2.2"
|
||||
|
@ -2289,15 +2377,42 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "keyfork-prompt"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||
checksum = "4ab2d75e36a647f50a2950671cf388251c585b29604925995189bb066c0747eb"
|
||||
checksum = "8df91df98bc6faa0cbc4f08e33797b832e384e33f8dbe066bffcb8ebb93216e6"
|
||||
dependencies = [
|
||||
"keyfork-bug",
|
||||
"keyfork-crossterm",
|
||||
"keyfork-mnemonic",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-shard"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||
checksum = "a208781e8731184f165d32ac11d957720bebc5aba146a883ef325168e3ff91d9"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"blahaj",
|
||||
"card-backend",
|
||||
"card-backend-pcsc",
|
||||
"hkdf",
|
||||
"keyfork-bug",
|
||||
"keyfork-derive-openpgp",
|
||||
"keyfork-mnemonic",
|
||||
"keyfork-prompt",
|
||||
"openpgp-card",
|
||||
"openpgp-card-sequoia",
|
||||
"sequoia-openpgp",
|
||||
"sha2 0.10.8",
|
||||
"smex",
|
||||
"thiserror 1.0.69",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyforkd-client"
|
||||
version = "0.2.1"
|
||||
|
@ -2376,7 +2491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3149,7 +3264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.92",
|
||||
|
@ -3586,7 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e858e4e9e48ff079cede92e1b45c942a5466ce9a4e3cc0c2a7e66586a718ef59"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"base64 0.21.7",
|
||||
"buffered-reader",
|
||||
"bzip2",
|
||||
"chrono",
|
||||
|
@ -3834,6 +3949,12 @@ version = "1.13.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "smex"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||
checksum = "fec02cb08322118cdaff9f30b2eccad19e2c9906c6ade894593b00782b31a211"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
|
@ -6142,7 +6263,7 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6339,6 +6460,18 @@ version = "0.5.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
version = "0.8.15"
|
||||
|
|
|
@ -9,6 +9,8 @@ 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" }
|
||||
keyfork-prompt = { version = "0.2.1", registry = "distrust", default-features = false }
|
||||
keyfork-shard = { version = "0.3.0", registry = "distrust", default-features = false, features = ["openpgp", "openpgp-card"] }
|
||||
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 }
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use clap::command;
|
||||
use clap::{builder::ArgPredicate, command, value_parser};
|
||||
use icepick_module::help::*;
|
||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{IsTerminal, Write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
|
@ -186,13 +187,84 @@ 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!(--"export-for-quorum").global(true))
|
||||
.about("Run a pre-defined Icepick workflow.")
|
||||
.arg(
|
||||
clap::arg!(--"sign")
|
||||
clap::arg!(--"run-quorum" <FILE> "Run a workflow signed by a quorum of approvers.")
|
||||
.long_help(concat!(
|
||||
"Run a workflow signed by a quorum of approvers. ",
|
||||
"This command requires either `--shardfile` or `--keyring`. If given a ",
|
||||
"Shardfile, the certificates stored within the Shardfile will be used to ",
|
||||
"verify the quorum payload. If given an OpenPGP keyring, the ",
|
||||
"certificates within the keyring will be used to verify the quorum ",
|
||||
"payload. Both formats require all keys to be signed by the key matching a ",
|
||||
"currently plugged-in OpenPGP smartcard."
|
||||
))
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.conflicts_with_all([
|
||||
"simulate-workflow",
|
||||
"export-for-quorum",
|
||||
"add-signature-to-quorum",
|
||||
]),
|
||||
)
|
||||
.arg(
|
||||
clap::arg!(--"add-signature-to-quorum" <FILE> "Add a signature to a workflow quorum.")
|
||||
.long_help(concat!(
|
||||
"Add a signature to a workflow quorum. ",
|
||||
"Any existing signatures will be verified. ",
|
||||
"This command requires either `--shardfile` or `--keyring`. If given a ",
|
||||
"Shardfile, the certificates stored within the Shardfile will be used to ",
|
||||
"verify the quorum payload. If given an OpenPGP keyring, the ",
|
||||
"certificates within the keyring will be used to verify the quorum ",
|
||||
"payload. Both formats require all keys to be signed by the key matching a ",
|
||||
"currently plugged-in OpenPGP smartcard."
|
||||
))
|
||||
.value_parser(value_parser!(PathBuf)),
|
||||
)
|
||||
.arg(
|
||||
clap::arg!(--"keyring" <FILE> "OpenPGP Keyring file for verifying quorum.")
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.requires_ifs([
|
||||
(ArgPredicate::IsPresent, "run-quorum"),
|
||||
(ArgPredicate::IsPresent, "add-signature-to-quorum"),
|
||||
]),
|
||||
)
|
||||
.arg(
|
||||
clap::arg!(--"quorum-threshold" <THRESHOLD> "Quorum of signatures required to run.")
|
||||
.long_help(concat!(
|
||||
"Quorum of signatures required to run. ",
|
||||
"When not present, the default behavior is to require a signature from ",
|
||||
"every certificate present."
|
||||
))
|
||||
.value_parser(value_parser!(u8))
|
||||
.requires("run-quorum")
|
||||
.conflicts_with("shardfile"), // Shardfile contains its own threshold.
|
||||
)
|
||||
.arg(
|
||||
clap::arg!(--"shardfile" <FILE> "OpenPGP Shardfile for verifying quorum.")
|
||||
.long_help(concat!(
|
||||
"OpenPGP Shardfile for verifying quorum. ",
|
||||
"An OpenPGP Smartcard will be required to decrypt the quorum threshold and ",
|
||||
"OpenPGP certificates used for verifying the payload.",
|
||||
))
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.requires_ifs([
|
||||
(ArgPredicate::IsPresent, "run-quorum"),
|
||||
(ArgPredicate::IsPresent, "add-signature-to-quorum"),
|
||||
])
|
||||
.conflicts_with("keyring"),
|
||||
)
|
||||
.arg(clap::arg!(--"simulate-workflow" "Simulate an Icepick Workflow.").global(true))
|
||||
.arg(
|
||||
clap::arg!(
|
||||
--"export-for-quorum"
|
||||
"Export the given inputs as a quorum file."
|
||||
)
|
||||
.global(true),
|
||||
)
|
||||
.arg(
|
||||
clap::arg!(--"sign" "Sign the exported workflow values.")
|
||||
.global(true)
|
||||
.requires_if(clap::builder::ArgPredicate::IsPresent, "export-for-quorum"),
|
||||
.requires_if(ArgPredicate::IsPresent, "export-for-quorum"),
|
||||
);
|
||||
for module in workflows.iter() {
|
||||
let mut module_subcommand = clap::Command::new(module.0.as_str());
|
||||
|
@ -231,18 +303,69 @@ pub fn do_cli_thing() {
|
|||
|
||||
// If we have a Workflow command, run the workflow and exit.
|
||||
if let Some(("workflow", matches)) = matches.subcommand() {
|
||||
let (module_name, matches) = matches
|
||||
.subcommand()
|
||||
.expect("icepick workflow: missing module");
|
||||
let (workflow_name, matches) = matches
|
||||
.subcommand()
|
||||
.expect("icepick workflow: missing workflow");
|
||||
let workflow = workflows
|
||||
.iter()
|
||||
.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, module_name, matches, commands, &config.modules);
|
||||
if let Some((module_name, matches)) = matches.subcommand() {
|
||||
let (workflow_name, matches) = matches
|
||||
.subcommand()
|
||||
.expect("icepick workflow: missing workflow");
|
||||
let workflow = workflows
|
||||
.iter()
|
||||
.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, module_name, matches, commands, &config.modules);
|
||||
} else if let Some(payload_file) = matches.get_one::<PathBuf>("add-signature-to-quorum") {
|
||||
let purpose = workflow::Purpose::AddSignature;
|
||||
let mut payload = {
|
||||
if let Some(keyring_file) = matches.get_one::<PathBuf>("keyring") {
|
||||
workflow::parse_quorum_file(
|
||||
payload_file,
|
||||
keyring_file,
|
||||
matches.get_one::<u8>("quorum-threshold").copied(),
|
||||
purpose,
|
||||
)
|
||||
} else if let Some(shardfile) = matches.get_one::<PathBuf>("shardfile") {
|
||||
workflow::parse_quorum_with_shardfile(payload_file, shardfile, purpose)
|
||||
} else {
|
||||
panic!("neither --keyring nor --shardfile were given, no keys to verify")
|
||||
}
|
||||
};
|
||||
payload.add_signature().unwrap();
|
||||
let output_file = payload_file.with_extension("tmp");
|
||||
let mut file = std::fs::File::create_new(&output_file).unwrap();
|
||||
serde_json::to_writer_pretty(&mut file, &payload).unwrap();
|
||||
drop(file);
|
||||
std::fs::copy(&output_file, payload_file).unwrap();
|
||||
std::fs::remove_file(output_file).unwrap();
|
||||
} else if let Some(payload_file) = matches.get_one::<PathBuf>("run-quorum") {
|
||||
let purpose = workflow::Purpose::RunQuorum;
|
||||
let (module_name, workflow_name, inputs) = {
|
||||
if let Some(keyring_file) = matches.get_one::<PathBuf>("keyring") {
|
||||
workflow::parse_quorum_file(
|
||||
payload_file,
|
||||
keyring_file,
|
||||
matches.get_one::<u8>("quorum-threshold").copied(),
|
||||
purpose,
|
||||
)
|
||||
.into_values()
|
||||
} else if let Some(shardfile) = matches.get_one::<PathBuf>("shardfile") {
|
||||
workflow::parse_quorum_with_shardfile(payload_file, shardfile, purpose)
|
||||
.into_values()
|
||||
} else {
|
||||
panic!("neither --keyring nor --shardfile were given, no keys to verify")
|
||||
}
|
||||
};
|
||||
|
||||
let inputs: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_value(inputs).unwrap();
|
||||
|
||||
let workflow = workflows
|
||||
.iter()
|
||||
.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_payload(workflow, inputs, commands, &config.modules);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use icepick_workflow::{InvocableOperation, OperationResult, Workflow};
|
||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
use miniquorum::{Payload, PayloadVerification};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -9,6 +11,15 @@ use std::{
|
|||
|
||||
use super::{derive_keys, get_command, Commands, ModuleConfig, Operation};
|
||||
|
||||
/// The purpose for interacting with a payload.
|
||||
pub enum Purpose {
|
||||
/// Adding a signature.
|
||||
AddSignature,
|
||||
|
||||
/// Running a quorum-signed payload.
|
||||
RunQuorum,
|
||||
}
|
||||
|
||||
pub type StringMap = std::collections::HashMap<String, String>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -170,6 +181,72 @@ fn load_operations(commands: Commands, config: &[ModuleConfig]) -> Vec<CLIOperat
|
|||
operations
|
||||
}
|
||||
|
||||
pub fn parse_quorum_file(
|
||||
quorum_path: impl AsRef<std::path::Path>,
|
||||
cert_path: impl AsRef<std::path::Path>,
|
||||
threshold: Option<u8>,
|
||||
purpose: Purpose,
|
||||
) -> Payload {
|
||||
let (payload, certs) = Payload::load(quorum_path, cert_path).unwrap();
|
||||
let threshold = threshold.unwrap_or(u8::try_from(certs.len()).expect("too many certs!"));
|
||||
let policy = match purpose {
|
||||
Purpose::AddSignature => {
|
||||
// All signatures must be valid, but we don't require a minimum.
|
||||
PayloadVerification::new().with_threshold(0)
|
||||
}
|
||||
Purpose::RunQuorum => {
|
||||
PayloadVerification::new().with_threshold(threshold)
|
||||
|
||||
},
|
||||
};
|
||||
payload.verify_signatures(&certs, &policy, None).unwrap();
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
pub fn parse_quorum_with_shardfile(
|
||||
quorum_path: impl AsRef<std::path::Path>,
|
||||
shardfile_path: impl AsRef<std::path::Path>,
|
||||
purpose: Purpose,
|
||||
) -> Payload {
|
||||
let payload_file = std::fs::File::open(quorum_path).unwrap();
|
||||
let payload: Payload = serde_json::from_reader(payload_file).unwrap();
|
||||
|
||||
let opgp = OpenPGP;
|
||||
let (threshold, certs) = opgp.decrypt_metadata_from_file(
|
||||
None::<&std::path::Path>,
|
||||
std::fs::File::open(shardfile_path).unwrap(),
|
||||
keyfork_prompt::default_handler().unwrap(),
|
||||
).unwrap();
|
||||
let policy = match purpose {
|
||||
Purpose::AddSignature => {
|
||||
// All signatures must be valid, but we don't require a minimum.
|
||||
PayloadVerification::new().with_threshold(0)
|
||||
}
|
||||
Purpose::RunQuorum => {
|
||||
PayloadVerification::new().with_threshold(threshold)
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
payload.verify_signatures(&certs, &policy, None).unwrap();
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
pub fn handle_payload(
|
||||
workflow: &Workflow,
|
||||
inputs: HashMap<String, Value>,
|
||||
modules: Commands,
|
||||
config: &[ModuleConfig],
|
||||
) {
|
||||
let operations = load_operations(modules, config);
|
||||
let result = workflow
|
||||
.run_workflow(inputs, &operations, &derive_keys)
|
||||
.expect("Invocation failure");
|
||||
println!("{}", serde_json::to_string(&result).expect("valid JSON"));
|
||||
}
|
||||
|
||||
pub fn handle(
|
||||
workflow: &Workflow,
|
||||
module_name: &str,
|
||||
|
@ -196,7 +273,7 @@ pub fn handle(
|
|||
}
|
||||
|
||||
if matches.get_flag("export-for-quorum") {
|
||||
let mut payload = miniquorum::Payload::new(
|
||||
let mut payload = Payload::new(
|
||||
serde_json::to_value(data).unwrap(),
|
||||
module_name,
|
||||
&workflow.name,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use chrono::prelude::*;
|
||||
use keyfork_prompt::{
|
||||
default_handler, prompt_validated_passphrase,
|
||||
validators::{PinValidator, Validator},
|
||||
|
@ -11,10 +12,9 @@ use sequoia_openpgp::{
|
|||
packet::{signature::SignatureBuilder, Packet},
|
||||
parse::Parse,
|
||||
serialize::Serialize as _,
|
||||
types::SignatureType,
|
||||
types::{HashAlgorithm, SignatureType},
|
||||
Cert, Fingerprint,
|
||||
};
|
||||
use chrono::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
|
||||
|
@ -27,6 +27,7 @@ pub struct Error {
|
|||
policy: PayloadVerification,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum BaseError {
|
||||
/// In the given certificate keyring, the provided fingerprint was not found.
|
||||
|
@ -103,7 +104,7 @@ fn unhashed(value: Value) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
|||
|
||||
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
|
||||
let bincoded = unhashed(value)?;
|
||||
let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?;
|
||||
let mut digest = HashAlgorithm::SHA512.context()?;
|
||||
digest.update(&bincoded);
|
||||
|
||||
Ok(digest)
|
||||
|
@ -143,14 +144,17 @@ impl PayloadVerification {
|
|||
Default::default()
|
||||
}
|
||||
|
||||
/// Require a signature per key, regardless of any given threshold.
|
||||
pub fn with_one_per_key(self, one_each: bool) -> Self {
|
||||
Self { one_each, ..self }
|
||||
}
|
||||
|
||||
/// Set a threshold for required signatures.
|
||||
pub fn with_threshold(self, threshold: u8) -> Self {
|
||||
Self { threshold, ..self }
|
||||
Self { one_each: false, threshold, ..self }
|
||||
}
|
||||
|
||||
/// Require a single valid signature; other signatures may be invalid.
|
||||
pub fn with_any_valid(self) -> Self {
|
||||
Self {
|
||||
threshold: 1,
|
||||
|
@ -159,6 +163,7 @@ impl PayloadVerification {
|
|||
}
|
||||
}
|
||||
|
||||
/// Require a threshold of signatures to be valid, allowing no invalid signatures.
|
||||
pub fn with_all_valid(self, threshold: u8) -> Self {
|
||||
Self {
|
||||
threshold,
|
||||
|
@ -167,6 +172,10 @@ impl PayloadVerification {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ignore invalid signatures. A threshold of valid signatures is still required.
|
||||
///
|
||||
/// The default behavior is to error when encountering an invalid signature, even if a quorum
|
||||
/// is reached.
|
||||
pub fn ignoring_invalid_signatures(self) -> Self {
|
||||
Self {
|
||||
error_on_invalid: false,
|
||||
|
@ -174,6 +183,9 @@ impl PayloadVerification {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ignoring signatures signed by unknown keys.
|
||||
///
|
||||
/// The default behavior is to error when encountering an unknown signature.
|
||||
pub fn ignoring_missing_keys(self) -> Self {
|
||||
Self {
|
||||
error_on_missing_key: true,
|
||||
|
@ -196,9 +208,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 {
|
||||
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()],
|
||||
workflow: [
|
||||
module_name.as_ref().to_string(),
|
||||
workflow_name.as_ref().to_string(),
|
||||
],
|
||||
values,
|
||||
datetime: Utc::now(),
|
||||
signatures: vec![],
|
||||
|
@ -230,6 +249,10 @@ impl Payload {
|
|||
Ok((payload, certs))
|
||||
}
|
||||
|
||||
pub fn signature_count(&self) -> usize {
|
||||
self.signatures.len()
|
||||
}
|
||||
|
||||
/// Attach a signature from an OpenPGP card.
|
||||
///
|
||||
/// # Errors
|
||||
|
@ -237,7 +260,8 @@ impl Payload {
|
|||
/// 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(serde_json::to_value(&self)?)?;
|
||||
let builder = SignatureBuilder::new(SignatureType::Binary);
|
||||
let builder =
|
||||
SignatureBuilder::new(SignatureType::Binary).set_hash_algo(HashAlgorithm::SHA512);
|
||||
let mut prompt_handler = default_handler()?;
|
||||
let pin_validator = PinValidator {
|
||||
min_length: Some(6),
|
||||
|
@ -396,6 +420,14 @@ impl Payload {
|
|||
|
||||
Ok(&self.values)
|
||||
}
|
||||
|
||||
pub fn into_values(self) -> (String, String, serde_json::Value) {
|
||||
let Payload {
|
||||
workflow, values, ..
|
||||
} = self;
|
||||
let [module, workflow] = workflow;
|
||||
(module, workflow, values)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_matching_certificate(
|
||||
|
@ -420,6 +452,12 @@ fn find_matching_certificate(
|
|||
.expect("smartcard signing key is unavailable");
|
||||
for cert in certs {
|
||||
let valid_cert = cert.with_policy(policy, None)?;
|
||||
// NOTE: We must verify that it is for_signing because back signatures
|
||||
// mean that the signing key verifies the certificate.
|
||||
//
|
||||
// We don't want a certificate to be able to adopt, for example, an encryption key
|
||||
// because that means there is no back signature and the encryption key can be
|
||||
// adopted onto a malicious certificate.
|
||||
for key in valid_cert.keys().alive().for_signing() {
|
||||
let fpr = key.fingerprint();
|
||||
if fpr.as_bytes() == signing_fingerprint.as_bytes() {
|
||||
|
|
Loading…
Reference in New Issue