icepick workflow: improve miniquorum embeddedness
This commit is contained in:
parent
5c9b5533d6
commit
7d2909bada
|
@ -48,6 +48,20 @@ dependencies = [
|
||||||
"cpufeatures",
|
"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]]
|
[[package]]
|
||||||
name = "aes-gcm-siv"
|
name = "aes-gcm-siv"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
@ -99,6 +113,12 @@ dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -413,7 +433,7 @@ dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -468,6 +488,17 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
version = "1.5.5"
|
version = "1.5.5"
|
||||||
|
@ -1492,6 +1523,12 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -1653,6 +1690,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
|
@ -1718,6 +1765,11 @@ name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
|
@ -1737,6 +1789,15 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5491a308e0214554f07a81d8944abe45f552871c12e3c3c6e7e5d354039a6c4c"
|
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]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -1871,6 +1932,8 @@ dependencies = [
|
||||||
"icepick-module",
|
"icepick-module",
|
||||||
"icepick-workflow",
|
"icepick-workflow",
|
||||||
"keyfork-derive-util",
|
"keyfork-derive-util",
|
||||||
|
"keyfork-prompt",
|
||||||
|
"keyfork-shard",
|
||||||
"keyforkd-client",
|
"keyforkd-client",
|
||||||
"keyforkd-models",
|
"keyforkd-models",
|
||||||
"miniquorum",
|
"miniquorum",
|
||||||
|
@ -2247,6 +2310,31 @@ dependencies = [
|
||||||
"signal-hook-mio",
|
"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]]
|
[[package]]
|
||||||
name = "keyfork-derive-util"
|
name = "keyfork-derive-util"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2289,15 +2377,42 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-prompt"
|
name = "keyfork-prompt"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||||
checksum = "4ab2d75e36a647f50a2950671cf388251c585b29604925995189bb066c0747eb"
|
checksum = "8df91df98bc6faa0cbc4f08e33797b832e384e33f8dbe066bffcb8ebb93216e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
"keyfork-crossterm",
|
"keyfork-crossterm",
|
||||||
|
"keyfork-mnemonic",
|
||||||
"thiserror 1.0.69",
|
"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]]
|
[[package]]
|
||||||
name = "keyforkd-client"
|
name = "keyforkd-client"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -2376,7 +2491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3149,7 +3264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.12.1",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.92",
|
"syn 2.0.92",
|
||||||
|
@ -3586,7 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e858e4e9e48ff079cede92e1b45c942a5466ce9a4e3cc0c2a7e66586a718ef59"
|
checksum = "e858e4e9e48ff079cede92e1b45c942a5466ce9a4e3cc0c2a7e66586a718ef59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.21.7",
|
||||||
"buffered-reader",
|
"buffered-reader",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -3834,6 +3949,12 @@ version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smex"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://git.distrust.co/public/_cargo-index.git"
|
||||||
|
checksum = "fec02cb08322118cdaff9f30b2eccad19e2c9906c6ade894593b00782b31a211"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
|
@ -6142,7 +6263,7 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6339,6 +6460,18 @@ version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
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]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.15"
|
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-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" }
|
||||||
|
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-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 }
|
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 icepick_module::help::*;
|
||||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
|
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::{IsTerminal, Write},
|
io::{IsTerminal, Write},
|
||||||
|
path::PathBuf,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -186,13 +187,84 @@ 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!(--"export-for-quorum").global(true))
|
|
||||||
.arg(
|
.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)
|
.global(true)
|
||||||
.requires_if(clap::builder::ArgPredicate::IsPresent, "export-for-quorum"),
|
.requires_if(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());
|
||||||
|
@ -231,18 +303,69 @@ pub fn do_cli_thing() {
|
||||||
|
|
||||||
// If we have a Workflow command, run the workflow and exit.
|
// If we have a Workflow command, run the workflow and exit.
|
||||||
if let Some(("workflow", matches)) = matches.subcommand() {
|
if let Some(("workflow", matches)) = matches.subcommand() {
|
||||||
let (module_name, matches) = matches
|
if let Some((module_name, matches)) = matches.subcommand() {
|
||||||
.subcommand()
|
let (workflow_name, matches) = matches
|
||||||
.expect("icepick workflow: missing module");
|
.subcommand()
|
||||||
let (workflow_name, matches) = matches
|
.expect("icepick workflow: missing workflow");
|
||||||
.subcommand()
|
let workflow = workflows
|
||||||
.expect("icepick workflow: missing workflow");
|
.iter()
|
||||||
let workflow = workflows
|
.find(|(module, _)| module == module_name)
|
||||||
.iter()
|
.and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name))
|
||||||
.find(|(module, _)| module == module_name)
|
.expect("workflow from CLI should match config");
|
||||||
.and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name))
|
workflow::handle(workflow, module_name, matches, commands, &config.modules);
|
||||||
.expect("workflow from CLI should match config");
|
} else if let Some(payload_file) = matches.get_one::<PathBuf>("add-signature-to-quorum") {
|
||||||
workflow::handle(workflow, module_name, matches, commands, &config.modules);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use icepick_workflow::{InvocableOperation, OperationResult, Workflow};
|
use icepick_workflow::{InvocableOperation, OperationResult, Workflow};
|
||||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
||||||
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
use miniquorum::{Payload, PayloadVerification};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
@ -9,6 +11,15 @@ use std::{
|
||||||
|
|
||||||
use super::{derive_keys, get_command, Commands, ModuleConfig, Operation};
|
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>;
|
pub type StringMap = std::collections::HashMap<String, String>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -170,6 +181,72 @@ fn load_operations(commands: Commands, config: &[ModuleConfig]) -> Vec<CLIOperat
|
||||||
operations
|
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(
|
pub fn handle(
|
||||||
workflow: &Workflow,
|
workflow: &Workflow,
|
||||||
module_name: &str,
|
module_name: &str,
|
||||||
|
@ -196,7 +273,7 @@ pub fn handle(
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches.get_flag("export-for-quorum") {
|
if matches.get_flag("export-for-quorum") {
|
||||||
let mut payload = miniquorum::Payload::new(
|
let mut payload = Payload::new(
|
||||||
serde_json::to_value(data).unwrap(),
|
serde_json::to_value(data).unwrap(),
|
||||||
module_name,
|
module_name,
|
||||||
&workflow.name,
|
&workflow.name,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::prelude::*;
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
default_handler, prompt_validated_passphrase,
|
default_handler, prompt_validated_passphrase,
|
||||||
validators::{PinValidator, Validator},
|
validators::{PinValidator, Validator},
|
||||||
|
@ -11,10 +12,9 @@ use sequoia_openpgp::{
|
||||||
packet::{signature::SignatureBuilder, Packet},
|
packet::{signature::SignatureBuilder, Packet},
|
||||||
parse::Parse,
|
parse::Parse,
|
||||||
serialize::Serialize as _,
|
serialize::Serialize as _,
|
||||||
types::SignatureType,
|
types::{HashAlgorithm, 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};
|
||||||
|
@ -27,6 +27,7 @@ pub struct Error {
|
||||||
policy: PayloadVerification,
|
policy: PayloadVerification,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum BaseError {
|
pub enum BaseError {
|
||||||
/// In the given certificate keyring, the provided fingerprint was not found.
|
/// 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>> {
|
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
|
||||||
let bincoded = unhashed(value)?;
|
let bincoded = unhashed(value)?;
|
||||||
let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?;
|
let mut digest = HashAlgorithm::SHA512.context()?;
|
||||||
digest.update(&bincoded);
|
digest.update(&bincoded);
|
||||||
|
|
||||||
Ok(digest)
|
Ok(digest)
|
||||||
|
@ -143,14 +144,17 @@ impl PayloadVerification {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Require a signature per key, regardless of any given threshold.
|
||||||
pub fn with_one_per_key(self, one_each: bool) -> Self {
|
pub fn with_one_per_key(self, one_each: bool) -> Self {
|
||||||
Self { one_each, ..self }
|
Self { one_each, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a threshold for required signatures.
|
||||||
pub fn with_threshold(self, threshold: u8) -> Self {
|
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 {
|
pub fn with_any_valid(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
threshold: 1,
|
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 {
|
pub fn with_all_valid(self, threshold: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
threshold,
|
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 {
|
pub fn ignoring_invalid_signatures(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
error_on_invalid: false,
|
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 {
|
pub fn ignoring_missing_keys(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
error_on_missing_key: true,
|
error_on_missing_key: true,
|
||||||
|
@ -196,9 +208,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.
|
/// 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 {
|
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,
|
values,
|
||||||
datetime: Utc::now(),
|
datetime: Utc::now(),
|
||||||
signatures: vec![],
|
signatures: vec![],
|
||||||
|
@ -230,6 +249,10 @@ impl Payload {
|
||||||
Ok((payload, certs))
|
Ok((payload, certs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn signature_count(&self) -> usize {
|
||||||
|
self.signatures.len()
|
||||||
|
}
|
||||||
|
|
||||||
/// Attach a signature from an OpenPGP card.
|
/// Attach a signature from an OpenPGP card.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
@ -237,7 +260,8 @@ impl Payload {
|
||||||
/// 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(serde_json::to_value(&self)?)?;
|
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 mut prompt_handler = default_handler()?;
|
||||||
let pin_validator = PinValidator {
|
let pin_validator = PinValidator {
|
||||||
min_length: Some(6),
|
min_length: Some(6),
|
||||||
|
@ -396,6 +420,14 @@ impl Payload {
|
||||||
|
|
||||||
Ok(&self.values)
|
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(
|
fn find_matching_certificate(
|
||||||
|
@ -420,6 +452,12 @@ fn find_matching_certificate(
|
||||||
.expect("smartcard signing key is unavailable");
|
.expect("smartcard signing key is unavailable");
|
||||||
for cert in certs {
|
for cert in certs {
|
||||||
let valid_cert = cert.with_policy(policy, None)?;
|
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() {
|
for key in valid_cert.keys().alive().for_signing() {
|
||||||
let fpr = key.fingerprint();
|
let fpr = key.fingerprint();
|
||||||
if fpr.as_bytes() == signing_fingerprint.as_bytes() {
|
if fpr.as_bytes() == signing_fingerprint.as_bytes() {
|
||||||
|
|
Loading…
Reference in New Issue