From b5034586ccb9e07e78c85f08f1c57f13828d6bc1 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 30 Jan 2025 11:07:32 -0500 Subject: [PATCH] miniquorum: initial commit --- Cargo.lock | 742 +++++++++++++++++++++++++++++++++- Cargo.toml | 1 + crates/miniquorum/Cargo.toml | 20 + crates/miniquorum/src/lib.rs | 475 ++++++++++++++++++++++ crates/miniquorum/src/main.rs | 118 ++++++ 5 files changed, 1340 insertions(+), 16 deletions(-) create mode 100644 crates/miniquorum/Cargo.toml create mode 100644 crates/miniquorum/src/lib.rs create mode 100644 crates/miniquorum/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 77fbf45..fe9b150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "assert_matches" version = "1.5.0" @@ -395,6 +404,24 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.92", +] + [[package]] name = "bip32" version = "0.5.2" @@ -411,6 +438,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -582,6 +624,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "buffered-reader" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabd1c5e55587a8e8526172d63ad2ba665fa18c8acb39ec9a77af1708c982b9b" +dependencies = [ + "bzip2", + "flate2", + "lazy_static", + "libc", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -633,6 +687,48 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafdbf26611df8c14810e268ddceda071c297570a5fb360ceddf617fe417ef58" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "card-backend" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3ee3a298842065dc489180c34a4fe4bbbb8643bb422009d79558a099fb42e5" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "card-backend-pcsc" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bb0b707b1b6b058ed93abd70ef65703ed6fd4150d32a0d735b78cfa61cbb35" +dependencies = [ + "card-backend", + "iso7816-tlv", + "log", + "pcsc", +] + [[package]] name = "cc" version = "1.2.6" @@ -644,6 +740,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -693,10 +798,21 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.23" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -704,21 +820,22 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -1002,6 +1119,17 @@ dependencies = [ "syn 2.0.92", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "der" version = "0.7.9" @@ -1059,6 +1187,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1070,6 +1219,12 @@ dependencies = [ "syn 2.0.92", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "eager" version = "0.1.0" @@ -1082,12 +1237,12 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.9", "digest 0.10.7", "elliptic-curve", "rfc6979", "signature 2.2.0", - "spki", + "spki 0.7.3", ] [[package]] @@ -1105,7 +1260,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "signature 2.2.0", ] @@ -1180,13 +1335,22 @@ dependencies = [ "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "subtle", "zeroize", ] +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1222,6 +1386,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -1232,6 +1406,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "feature-probe" version = "0.1.1" @@ -1254,6 +1434,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "five8_const" version = "0.1.3" @@ -1269,6 +1460,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.35" @@ -1462,6 +1659,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "group" version = "0.13.0" @@ -1528,6 +1731,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-slice" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a308e0214554f07a81d8944abe45f552871c12e3c3c6e7e5d354039a6c4c" + [[package]] name = "hmac" version = "0.8.1" @@ -1657,15 +1866,21 @@ dependencies = [ name = "icepick" version = "0.1.0" dependencies = [ + "bincode", + "card-backend-pcsc", "clap", "icepick-module", "icepick-workflow", "keyfork-derive-util", "keyforkd-client", "keyforkd-models", + "openpgp-card", + "openpgp-card-sequoia", + "sequoia-openpgp", "serde", "serde_json", "serde_yaml", + "sha3", "thiserror 2.0.11", "toml 0.8.19", ] @@ -1917,6 +2132,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "iso7816-tlv" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546" +dependencies = [ + "untrusted", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1926,6 +2150,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -2003,6 +2236,21 @@ version = "0.1.0" source = "registry+https://git.distrust.co/public/_cargo-index.git" checksum = "fb5eae1e7471415b59f852ccb43b7858f0650a5d158ccbfb1d39088d0881f582" +[[package]] +name = "keyfork-crossterm" +version = "0.27.2" +source = "registry+https://git.distrust.co/public/_cargo-index.git" +checksum = "1d8ffd39857ea9b2ad2c0078f645e39146280aab539918027d5a777df9670ccc" +dependencies = [ + "bitflags 2.6.0", + "filedescriptor", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", +] + [[package]] name = "keyfork-derive-util" version = "0.2.2" @@ -2043,6 +2291,17 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "keyfork-prompt" +version = "0.2.0" +source = "registry+https://git.distrust.co/public/_cargo-index.git" +checksum = "4ab2d75e36a647f50a2950671cf388251c585b29604925995189bb066c0747eb" +dependencies = [ + "keyfork-bug", + "keyfork-crossterm", + "thiserror 1.0.69", +] + [[package]] name = "keyforkd-client" version = "0.2.1" @@ -2069,11 +2328,44 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -2081,6 +2373,32 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -2129,6 +2447,12 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "litemap" version = "0.7.4" @@ -2175,6 +2499,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" + [[package]] name = "merlin" version = "3.0.0" @@ -2203,6 +2533,29 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniquorum" +version = "0.1.0" +dependencies = [ + "bincode", + "card-backend-pcsc", + "clap", + "keyfork-prompt", + "openpgp-card", + "openpgp-card-sequoia", + "sequoia-openpgp", + "serde", + "serde_json", + "sha3", + "thiserror 2.0.11", +] + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -2219,10 +2572,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "nettle" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936" +dependencies = [ + "getrandom 0.2.15", + "libc", + "nettle-sys", + "thiserror 1.0.69", + "typenum", +] + +[[package]] +name = "nettle-sys" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a3f5406064d310d59b1a219d3c5c9a49caf4047b6496032e3f930876488c34" +dependencies = [ + "bindgen", + "cc", + "libc", + "pkg-config", + "tempfile", + "vcpkg", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num" version = "0.2.1" @@ -2258,6 +2655,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -2324,6 +2738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2368,6 +2783,37 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openpgp-card" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e4e4146cf765e416a6c73e6bef6a4aa4cb12a76d9ef24c8c170eba4e4384ca" +dependencies = [ + "card-backend", + "chrono", + "hex-slice", + "log", + "nom", + "thiserror 1.0.69", +] + +[[package]] +name = "openpgp-card-sequoia" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0835400374822b27054b7a6606770f4a48b9255ad800a098f1d242d8695b4f22" +dependencies = [ + "anyhow", + "card-backend", + "chrono", + "log", + "openpgp-card", + "rsa", + "sequoia-openpgp", + "sha2 0.10.8", + "thiserror 1.0.69", +] + [[package]] name = "openssl" version = "0.10.68" @@ -2460,6 +2906,25 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "pcsc" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd833ecf8967e65934c49d3521a175929839bf6d0e497f3bd0d3a2ca08943da" +dependencies = [ + "bitflags 2.6.0", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ef017e15d2e5592a9e39a346c1dbaea5120bab7ed7106b210ef58ebd97003" +dependencies = [ + "pkg-config", +] + [[package]] name = "peg" version = "0.8.4" @@ -2487,6 +2952,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2502,6 +2976,25 @@ dependencies = [ "num", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.7" @@ -2534,14 +3027,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -2577,6 +3092,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.25" @@ -2735,6 +3256,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.11.1" @@ -2858,12 +3390,38 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rsa" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "signature 2.2.0", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2873,6 +3431,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2975,9 +3546,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.9", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -3011,6 +3582,35 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +[[package]] +name = "sequoia-openpgp" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e858e4e9e48ff079cede92e1b45c942a5466ce9a4e3cc0c2a7e66586a718ef59" +dependencies = [ + "anyhow", + "base64 0.22.1", + "buffered-reader", + "bzip2", + "chrono", + "dyn-clone", + "flate2", + "getrandom 0.2.15", + "idna", + "lalrpop", + "lalrpop-util", + "lazy_static", + "libc", + "memsec", + "nettle", + "once_cell", + "regex", + "regex-syntax", + "sha1collisiondetection", + "thiserror 1.0.69", + "xxhash-rust", +] + [[package]] name = "serde" version = "1.0.216" @@ -3120,6 +3720,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1collisiondetection" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" +dependencies = [ + "digest 0.10.7", + "generic-array", +] + [[package]] name = "sha2" version = "0.9.9" @@ -3160,6 +3770,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4277,6 +4908,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -4284,7 +4925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -4784,6 +5425,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4880,6 +5534,20 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.2.15", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendermint" version = "0.40.1" @@ -4972,6 +5640,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5042,6 +5731,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5211,6 +5909,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -5638,6 +6342,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 1eb94c4..282739c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/builtins/icepick-internal", "crates/by-chain/icepick-solana", "crates/by-chain/icepick-cosmos", + "crates/miniquorum", ] [workspace.dependencies] diff --git a/crates/miniquorum/Cargo.toml b/crates/miniquorum/Cargo.toml new file mode 100644 index 0000000..adc4386 --- /dev/null +++ b/crates/miniquorum/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "miniquorum" +version = "0.1.0" +edition = "2021" + +[features] +default = ["clap"] + +[dependencies] +bincode = "1.3.3" +card-backend-pcsc = "0.5.0" +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" +openpgp-card-sequoia = "0.2.2" +sequoia-openpgp = "1.22.0" +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sha3 = "0.10.8" +thiserror = "2.0.11" diff --git a/crates/miniquorum/src/lib.rs b/crates/miniquorum/src/lib.rs new file mode 100644 index 0000000..7c51328 --- /dev/null +++ b/crates/miniquorum/src/lib.rs @@ -0,0 +1,475 @@ +use keyfork_prompt::{ + default_handler, prompt_validated_passphrase, + validators::{PinValidator, Validator}, +}; +use openpgp_card::{Error as CardError, StatusBytes}; +use openpgp_card_sequoia::{state::Open, Card}; +use sequoia_openpgp::{ + self as openpgp, + armor::{Kind, Writer}, + crypto::hash::Digest, + packet::{signature::SignatureBuilder, Packet}, + parse::Parse, + serialize::Serialize as _, + types::SignatureType, + Cert, Fingerprint, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{collections::BTreeMap, fs::File, io::Read, path::Path}; + +#[derive(thiserror::Error, Debug)] +/// An error with a [`PayloadVerification`] policy. +#[error("{error} (policy: {policy:?})")] +pub struct Error { + error: BaseError, + policy: PayloadVerification, +} + +#[derive(thiserror::Error, Debug)] +pub enum BaseError { + /// In the given certificate keyring, the provided fingerprint was not found. + #[error("fingerprint not found: {0}")] + FingerprintNotFound(Fingerprint), + + /// No smartcard was found. + #[error("no smartcard found")] + NoSmartcard, + + /// None of the certificates in the given certificate keyring matched any plugged-in smartcard. + #[error("no certs found matching any available smartcard")] + NoCertMatchedSmartcard, + + /// The certificate was not trusted by the root of trust. + #[error("untrusted certificate: {0} has not signed {1:?}")] + UntrustedCertificates(Fingerprint, Vec), + + /// No certificate in the given certificate keyring matched the signature. + #[error("no public key matched signature")] + NoPublicKeyMatchedSignature, + + /// Not enough signatures matched based on the given threshold + #[error("not enough signatures: {0} < {1}")] + NotEnoughSignatures(u8, u8), + + /// 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, +} + +impl BaseError { + fn with_policy(self, policy: &PayloadVerification) -> Error { + Error { + error: self, + policy: policy.clone(), + } + } +} + +fn canonicalize(value: Value) -> Value { + match value { + Value::Array(vec) => { + let values = vec.into_iter().map(canonicalize).collect(); + Value::Array(values) + } + Value::Object(map) => { + // this sorts the values + let map: BTreeMap = + map.into_iter().map(|(k, v)| (k, canonicalize(v))).collect(); + let sorted: Vec = map + .into_iter() + .map(|(k, v)| Value::Array(vec![Value::String(k), v])) + .collect(); + Value::Array(sorted) + } + value => value, + } +} + +fn unhashed(value: Value) -> Result, Box> { + let value = canonicalize(value); + let bincoded = bincode::serialize(&value)?; + Ok(bincoded) +} + +fn hash(value: Value) -> Result, Box> { + let value = canonicalize(value); + let bincoded = bincode::serialize(&value)?; + let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?; + digest.update(&bincoded); + + Ok(digest) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Payload { + values: Value, + signatures: Vec, +} + +#[derive(Clone, Debug)] +pub struct PayloadVerification { + threshold: u8, + error_on_invalid: bool, + error_on_missing_key: bool, + one_each: bool, +} + +impl std::default::Default for PayloadVerification { + fn default() -> Self { + Self { + threshold: 0, + error_on_invalid: true, + error_on_missing_key: true, + one_each: true, + } + } +} + +#[allow(dead_code)] +impl PayloadVerification { + pub fn new() -> Self { + Default::default() + } + + pub fn with_one_per_key(self, one_each: bool) -> Self { + Self { one_each, ..self } + } + + pub fn with_threshold(self, threshold: u8) -> Self { + Self { threshold, ..self } + } + + pub fn with_any_valid(self) -> Self { + Self { + threshold: 1, + error_on_invalid: false, + ..self + } + } + + pub fn with_all_valid(self, threshold: u8) -> Self { + Self { + threshold, + error_on_invalid: true, + ..self + } + } + + pub fn ignoring_invalid_signatures(self) -> Self { + Self { + error_on_invalid: false, + ..self + } + } + + pub fn ignoring_missing_keys(self) -> Self { + Self { + error_on_missing_key: true, + ..self + } + } +} + +/// Format a name from an OpenPGP card. +fn format_name(input: impl AsRef) -> String { + let mut n = input + .as_ref() + .split("<<") + .take(2) + .map(|s| s.replace('<', " ")) + .collect::>(); + n.reverse(); + n.join(" ") +} + +impl Payload { + /// Load a Payload and the relevant certificates. + /// + /// # Errors + /// + /// The constructor may error if either file can't be read or if either file has invalid data. + pub fn load( + payload_path: impl AsRef, + keyring_path: impl AsRef, + ) -> Result<(Self, Vec), Box> { + let payload_file = File::open(payload_path)?; + let cert_file = File::open(keyring_path)?; + + Self::from_readers(payload_file, cert_file) + } + + pub fn from_readers( + payload: impl Read, + keyring: impl Read + Send + Sync, + ) -> Result<(Self, Vec), Box> { + let payload: Payload = serde_json::from_reader(payload)?; + let certs = + openpgp::cert::CertParser::from_reader(keyring)?.collect::, _>>()?; + 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 builder = SignatureBuilder::new(SignatureType::Binary); + let mut prompt_handler = default_handler()?; + let pin_validator = PinValidator { + min_length: Some(6), + ..Default::default() + }; + + for backend in card_backend_pcsc::PcscBackend::cards(None)? { + let mut card = Card::::new(backend?)?; + let mut transaction = card.transaction()?; + + let cardholder_name = format_name(transaction.cardholder_name()?); + let card_id = transaction.application_identifier()?.ident(); + let mut pin = None; + + while transaction.pw_status_bytes()?.err_count_pw1() > 0 && pin.is_none() { + transaction.reload_ard()?; + let attempts = transaction.pw_status_bytes()?.err_count_pw1(); + let rpea = "Remaining PIN entry attempts"; + let message = if cardholder_name.is_empty() { + format!("Unlock card {card_id}\n{rpea}: {attempts}\n\nPIN: ") + } else { + format!( + "Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: " + ) + }; + + let temp_pin = prompt_validated_passphrase( + &mut *prompt_handler, + &message, + 3, + pin_validator.to_fn(), + )?; + + let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); + match verification_status { + #[allow(clippy::ignored_unit_patterns)] + Ok(_) => { + pin.replace(temp_pin); + } + // NOTE: This should not be hit, because of the above validator. + Err(CardError::CardStatus( + StatusBytes::IncorrectParametersCommandDataField, + )) => { + prompt_handler.prompt_message(keyfork_prompt::Message::Text( + "Invalid PIN length entered.".to_string(), + ))?; + } + Err(_) => {} + } + } + + let mut signer_card = transaction.to_signing_card(pin.expect("valid PIN").as_str())?; + // NOTE: Can't use a PromptHandler to prompt a message as it doesn't provide a way to + // cancel a prompt when in terminal mode. Just eprintln to stderr. + // + // We shouldn't be writing with a PromptHandler, so the terminal should be reset. + let mut signer = + signer_card.signer(&|| eprintln!("Touch confirmation needed for signing"))?; + let signature = builder.clone().sign_message(&mut signer, &unhashed)?; + let signature = Packet::from(signature); + + let mut armored_signature = vec![]; + let mut writer = Writer::new(&mut armored_signature, Kind::Signature)?; + signature.serialize(&mut writer)?; + writer.finalize()?; + + self.signatures.push(String::from_utf8(armored_signature)?); + } + + Ok(()) + } + + /// Verify the keychain and certificates using either a Key ID or an OpenPGP card. + /// + /// # Errors + /// + /// The method may error if no certificate could be verified or if any singatures are invalid. + pub fn verify_signatures( + &self, + certs: &[Cert], + verification_policy: &PayloadVerification, + fingerprint: Option, + ) -> Result<&serde_json::Value, Box> { + let policy = openpgp::policy::StandardPolicy::new(); + let validated_cert = find_matching_certificate(fingerprint, certs, &policy)?; + let (certs, invalid_certs) = validate_cross_signed_certs(&validated_cert, certs, &policy)?; + + if !invalid_certs.is_empty() { + return Err(BaseError::UntrustedCertificates( + validated_cert.fingerprint(), + invalid_certs.iter().map(Cert::fingerprint).collect(), + ))?; + } + + let hashed = hash(self.values.clone())?; + + let PayloadVerification { + mut threshold, + error_on_invalid, + error_on_missing_key, + one_each, + } = *verification_policy; + let mut matches = 0; + + if one_each { + threshold = certs.len() as u8; + } + + for signature in &self.signatures { + let packet = Packet::from_bytes(signature.as_bytes())?; + let Packet::Signature(signature) = packet else { + panic!("bad packet found: {}", packet.tag()); + }; + let mut signature_matched = false; + for issuer in signature.get_issuers() { + for cert in &certs { + match cert + .with_policy(&policy, None)? + .keys() + .alive() + .for_signing() + .key_handle(issuer.clone()) + .next() + .map(|signing_key| signature.verify_hash(&signing_key, hashed.clone())) + { + Some(Ok(())) => { + // key found, signature matched + signature_matched = true; + } + Some(Err(e)) => { + if error_on_invalid { + return Err(e)?; + } + } + None => { + // key not found, but we have more certs to go through + } + } + } + } + + if signature_matched { + matches += 1; + } else if error_on_missing_key { + return Err( + BaseError::NoPublicKeyMatchedSignature.with_policy(verification_policy) + )?; + } + } + + if matches < threshold { + return Err( + BaseError::NotEnoughSignatures(matches, threshold).with_policy(verification_policy) + )?; + } + + Ok(&self.values) + } +} + +fn find_matching_certificate( + fingerprint: Option, + certs: &[Cert], + policy: &sequoia_openpgp::policy::StandardPolicy<'_>, +) -> Result> { + if let Some(fingerprint) = fingerprint { + Ok(certs + .iter() + .find(|cert| cert.fingerprint() == fingerprint) + .ok_or(BaseError::FingerprintNotFound(fingerprint))? + .clone()) + } else { + let mut any_smartcard = false; + for backend in card_backend_pcsc::PcscBackend::cards(None)? { + any_smartcard = true; + let mut card = Card::::new(backend?)?; + let mut transaction = card.transaction()?; + let signing_fingerprint = transaction + .fingerprint(openpgp_card::KeyType::Signing)? + .expect("smartcard signing key is unavailable"); + for cert in certs { + let valid_cert = cert.with_policy(policy, None)?; + for key in valid_cert.keys().alive().for_signing() { + let fpr = key.fingerprint(); + if fpr.as_bytes() == signing_fingerprint.as_bytes() { + return Ok(cert.clone()); + } + } + } + } + if any_smartcard { + Err(BaseError::NoCertMatchedSmartcard.into()) + } else { + Err(BaseError::NoSmartcard.into()) + } + } +} + +/// Validate that `certs` are signed by `validated_cert`, either by a signature directly upon the +/// primary key of that certificate, or a signature on a user ID of the certificate. +/// +/// Returns a list of trusted certs and a list of untrusted certs. +fn validate_cross_signed_certs( + validated_cert: &Cert, + certs: &[Cert], + policy: &sequoia_openpgp::policy::StandardPolicy, +) -> Result<(Vec, Vec), Box> { + let our_pkey = validated_cert.primary_key(); + let mut verified_certs = vec![validated_cert.clone()]; + let mut unverified_certs = vec![]; + + for cert in certs + .iter() + .filter(|cert| cert.fingerprint() != validated_cert.fingerprint()) + { + let mut has_valid_userid_signature = false; + let cert_pkey = cert.primary_key(); + + // check signatures on User IDs + let userids = cert + .userids() + .map(|ua| (ua.certifications(), ua.userid().clone())); + for (signatures, userid) in userids { + for signature in signatures { + if signature + .verify_userid_binding(&our_pkey, &*cert_pkey, &userid) + .is_ok() + { + has_valid_userid_signature = true; + } + } + } + + // check signatures on the primary key itself + let has_valid_direct_signature = cert_pkey + .active_certifications_by_key(policy, None, &***our_pkey.role_as_unspecified()) + .next() + .is_some(); + + if has_valid_userid_signature || has_valid_direct_signature { + verified_certs.push(cert.clone()); + } else { + unverified_certs.push(cert.clone()); + } + } + + Ok((verified_certs, unverified_certs)) +} diff --git a/crates/miniquorum/src/main.rs b/crates/miniquorum/src/main.rs new file mode 100644 index 0000000..1f3326c --- /dev/null +++ b/crates/miniquorum/src/main.rs @@ -0,0 +1,118 @@ +use clap::Parser; +use miniquorum::{Payload, PayloadVerification}; +use sequoia_openpgp::Fingerprint; +use std::{fs::File, path::PathBuf}; + +#[derive(clap::Parser)] +/// An Icepick-specific subset of the Quorum decision-making system. +enum MiniQuorum { + /// Verify signatures on an Icepick Payload file. + VerifySignatures { + /// The file containing OpenPGP Certificates used for verifying signatures. + keyring_file: PathBuf, + + /// The file provided as input. + /// + /// If no file is passed, standard input is used. + input_file: Option, + + /// An OpenPGP Fingerprint to use in place of on-smartcard certificate detection. + /// + /// This functionality is only recommended if verifying a payload without the physical + /// presence of any signer, and builds a web of trust from the signer fingerprint provided. + #[arg(long)] + fingerprint: Option, + + /// The file to write the resulting payload to, if verification is successful. + #[arg(long)] + output_file: Option, + }, + + /// Add a signature to an Icepick Payload file. + 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. + input_file: Option, + + /// The file to use as output. + /// + /// If no file is provided, but an input file is provided, the input file is used. If no + /// input file is provided, standard output is used. + #[arg(long)] + output_file: Option, + }, +} + +fn main() -> Result<(), Box> { + match MiniQuorum::parse() { + MiniQuorum::VerifySignatures { + keyring_file, + input_file, + fingerprint, + output_file, + } => { + assert_ne!( + input_file, output_file, + "output is verified data; not overwriting signed input data" + ); + let (payload, certs) = match input_file { + Some(input_file) => Payload::load(&input_file, &keyring_file)?, + None => { + let stdin = std::io::stdin(); + let keyring_file = File::open(&keyring_file)?; + Payload::from_readers(stdin, keyring_file)? + } + }; + + let policy = PayloadVerification::new().with_threshold(certs.len().try_into()?); + let inner_value = 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)?; + } else { + let stdout = std::io::stdout(); + serde_json::to_writer_pretty(stdout, inner_value)?; + } + } + MiniQuorum::AddSignature { + input_file, + output_file, + } => { + let mut payload = match &input_file { + Some(input_file) => { + let input_file = File::open(input_file)?; + let payload: Payload = serde_json::from_reader(input_file)?; + payload + } + None => { + let stdin = std::io::stdin(); + let value: serde_json::Value = serde_json::from_reader(stdin)?; + Payload::new(value) + } + }; + + payload.add_signature()?; + + if let Some(output_file) = output_file { + // write to output + let file = File::create(output_file)?; + serde_json::to_writer_pretty(file, &payload)?; + } else if let Some(input_file) = input_file { + // write to tempfile, move to input_file + let output_file = input_file.with_extension("tmp"); + let mut file = File::create_new(&output_file)?; + serde_json::to_writer_pretty(&mut file, &payload)?; + drop(file); + std::fs::copy(&output_file, input_file)?; + std::fs::remove_file(output_file)?; + } else { + // write to standard output? + println!("{}", serde_json::to_string_pretty(&payload)?); + } + } + } + Ok(()) +}