From adad3e5b6b12616afbccc319b35d94defd7cda8e Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 2 Nov 2023 01:01:34 -0500 Subject: [PATCH] keyfork-shard: begin work on OpenPGP card support --- Cargo.lock | 301 +++++++++++++++++- keyfork-shard/Cargo.toml | 5 +- .../src/bin/keyfork-shard-combine-openpgp.rs | 33 +- keyfork-shard/src/openpgp.rs | 54 +++- keyfork-shard/src/openpgp/keyring.rs | 4 +- keyfork-shard/src/openpgp/smartcard.rs | 198 ++++++++++++ 6 files changed, 569 insertions(+), 26 deletions(-) create mode 100644 keyfork-shard/src/openpgp/smartcard.rs diff --git a/Cargo.lock b/Cargo.lock index cb47f5e..53b8b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.5.0" @@ -234,6 +249,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.4.0" @@ -261,6 +282,27 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "card-backend" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3ee3a298842065dc489180c34a4fe4bbbb8643bb422009d79558a099fb42e5" +dependencies = [ + "thiserror", +] + +[[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.0.83" @@ -291,9 +333,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", + "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", + "windows-targets 0.48.5", ] [[package]] @@ -372,6 +417,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -446,6 +497,17 @@ dependencies = [ "syn 2.0.29", ] +[[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.8" @@ -478,6 +540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -515,10 +578,10 @@ version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ - "der", + "der 0.7.8", "elliptic-curve", "signature 2.1.0", - "spki", + "spki 0.7.2", ] [[package]] @@ -536,7 +599,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "signature 2.1.0", ] @@ -571,7 +634,7 @@ dependencies = [ "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "subtle", @@ -787,6 +850,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[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.12.1" @@ -796,6 +865,29 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.3.0" @@ -851,6 +943,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iso7816-tlv" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d47365efc3b4c252f8a3384445c0f7e8a4e0ae5c22bf3bedd2dd16f9bb45016a" +dependencies = [ + "untrusted", +] + [[package]] name = "itertools" version = "0.10.5" @@ -982,7 +1083,9 @@ version = "0.1.0" dependencies = [ "anyhow", "bincode", + "card-backend-pcsc", "keyfork-derive-openpgp", + "openpgp-card-sequoia", "sequoia-openpgp", "serde", "sharks", @@ -1063,6 +1166,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -1086,6 +1192,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1220,6 +1332,44 @@ dependencies = [ "winapi", ] +[[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-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1227,6 +1377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1254,6 +1405,36 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openpgp-card" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6b39b46a9deba985be9cc960709e284806b550d7e1aff915f8be4b06c3640" +dependencies = [ + "card-backend", + "chrono", + "hex-slice", + "log", + "nom", + "thiserror", +] + +[[package]] +name = "openpgp-card-sequoia" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7637080b15379df16fef0f81fd2664d403366b7514c721f2231c8974778017c3" +dependencies = [ + "anyhow", + "card-backend", + "chrono", + "log", + "openpgp-card", + "rsa", + "sequoia-openpgp", + "thiserror", +] + [[package]] name = "overload" version = "0.1.1" @@ -1293,12 +1474,40 @@ dependencies = [ "hmac", ] +[[package]] +name = "pcsc" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4" +dependencies = [ + "bitflags 1.3.2", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b7bfecba2c0f1b5efb0e7caf7533ab1c295024165bcbb066231f60d33e23ea" +dependencies = [ + "pkg-config", +] + [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "petgraph" version = "0.6.4" @@ -1350,14 +1559,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.8", + "spki 0.7.2", ] [[package]] @@ -1601,6 +1832,26 @@ 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.1.0", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1674,9 +1925,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.8", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -1817,6 +2068,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -1852,6 +2104,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[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.2" @@ -1859,7 +2127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", - "der", + "der 0.7.8", ] [[package]] @@ -2171,6 +2439,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.1" @@ -2283,6 +2557,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index f5a5ec1..574b5d5 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -6,13 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["openpgp"] +default = ["openpgp", "openpgp-card"] openpgp = ["sequoia-openpgp"] +openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc"] [dependencies] anyhow = "1.0.75" bincode = "1.3.3" +card-backend-pcsc = { version = "0.5.0", optional = true } keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } +openpgp-card-sequoia = { version = "0.2.0", optional = true } sequoia-openpgp = { version = "1.16.1", optional = true } serde = "1.0.188" sharks = "0.5.0" diff --git a/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index 5b9bcbf..0a41e71 100644 --- a/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -1,16 +1,21 @@ use std::{ env, + fs::File, io::{stdin, stdout}, path::PathBuf, process::ExitCode, str::FromStr, }; -use keyfork_shard::openpgp::{combine, discover_certs, parse_messages, openpgp::Cert}; +use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages}; type Result> = std::result::Result; -fn validate(threshold: &str, key_discovery: &str) -> Result<(u8, Vec)> { +fn validate( + threshold: &str, + key_discovery: &str, + recovery_file: &str, +) -> Result<(u8, Vec, PathBuf)> { let threshold = u8::from_str(threshold)?; let key_discovery = PathBuf::from(key_discovery); @@ -20,19 +25,33 @@ fn validate(threshold: &str, key_discovery: &str) -> Result<(u8, Vec)> { // Load certs from path let certs = discover_certs(key_discovery)?; - Ok((threshold, certs)) + let recovery_file = PathBuf::from(if recovery_file == "=" { + eprintln!("loading certs from stdin; note that prompting smartcard devices will not work"); + "/dev/stdin" + } else { + recovery_file + }); + + std::fs::metadata(&recovery_file)?; + + Ok((threshold, certs, recovery_file)) } fn run() -> Result<()> { let mut args = env::args(); let program_name = args.next().expect("program name"); let args = args.collect::>(); - let (threshold, cert_list) = match args.as_slice() { - [threshold, key_discovery] => validate(threshold, key_discovery)?, - _ => panic!("Usage: {program_name} threshold key_discovery"), + let (threshold, cert_list, recovery_file) = match args.as_slice() { + [threshold, key_discovery, recovery_file] => { + validate(threshold, key_discovery, recovery_file)? + } + [threshold, key_discovery] => { + validate(threshold, key_discovery, "-")? + } + _ => panic!("Usage: {program_name} threshold key_discovery recovery_file"), }; - let mut encrypted_messages = parse_messages(stdin())?; + let mut encrypted_messages = parse_messages(File::open(recovery_file)?)?; let encrypted_metadata = encrypted_messages .pop_front() diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index d1619a6..e4ee90f 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -13,7 +13,10 @@ use openpgp::{ armor::{Kind, Writer}, cert::{Cert, CertParser, ValidCert}, packet::{Packet, Tag, UserID, PKESK, SEIP}, - parse::{stream::DecryptorBuilder, Parse}, + parse::{ + stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper}, + Parse, + }, policy::{NullPolicy, Policy, StandardPolicy}, serialize::{ stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer}, @@ -28,6 +31,9 @@ use sharks::{Share, Sharks}; mod keyring; use keyring::Keyring; +mod smartcard; +use smartcard::SmartcardManager; + // TODO: better error handling #[derive(Debug, Clone)] @@ -57,7 +63,10 @@ impl EncryptedMessage { } } - pub fn decrypt_with(&self, policy: &'_ dyn Policy, keyring: &mut Keyring) -> Result> { + pub fn decrypt_with(&self, policy: &'_ dyn Policy, decryptor: H) -> Result> + where + H: VerificationHelper + DecryptionHelper, + { let mut packets = vec![]; for pkesk in &self.pkesks { @@ -76,7 +85,7 @@ impl EncryptedMessage { message.finalize()?; let mut decryptor = - DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, keyring)?; + DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, decryptor)?; let mut content = vec![]; decryptor.read_to_end(&mut content)?; @@ -222,9 +231,42 @@ pub fn combine( let left_from_threshold = threshold as usize - decrypted_messages.len(); if left_from_threshold > 0 { eprintln!("remaining keys: {left_from_threshold}, prompting yubikeys"); - } - for _ in 0..left_from_threshold { - todo!("prompt for Yubikeys") + // TODO: allow decrypt metadata with Yubikey, avoid require stage 1 + let mut manager = + SmartcardManager::new(keyring.root_cert().expect("stage 1 decrypt").clone()); + let mut remaining_usable_certs = certs + .iter() + .filter(|cert| messages.contains_key(&cert.keyid())) + .collect::>(); + + while threshold as usize - decrypted_messages.len() > 0 { + remaining_usable_certs.retain(|cert| messages.contains_key(&cert.keyid())); + let mut fingerprints = HashMap::new(); + for valid_cert in remaining_usable_certs + .iter() + .map(|cert| cert.with_policy(&policy, None)) + { + let valid_cert = valid_cert?; + fingerprints.insert( + valid_cert.keyid(), + valid_cert + .keys() + .for_storage_encryption() + .map(|k| k.fingerprint()) + .collect::>(), + ); + } + for (cert_id, fingerprints) in fingerprints { + if manager.load_any_fingerprint(fingerprints)?.is_some() { + // manager is loaded with a Card, utilize in tx + let message = messages.remove(&cert_id); + if let Some(message) = message { + let message = message.decrypt_with(&policy, &mut manager)?; + decrypted_messages.insert(cert_id, message); + } + } + } + } } let shares = decrypted_messages diff --git a/keyfork-shard/src/openpgp/keyring.rs b/keyfork-shard/src/openpgp/keyring.rs index a79fb95..1b3f829 100644 --- a/keyfork-shard/src/openpgp/keyring.rs +++ b/keyfork-shard/src/openpgp/keyring.rs @@ -110,7 +110,7 @@ impl DecryptionHelper for &mut Keyring { where D: FnMut(openpgp::types::SymmetricAlgorithm, &openpgp::crypto::SessionKey) -> bool, { - // optimized route: use all locally stored certs + // unoptimized route: use all locally stored certs for pkesk in pkesks { for cert in self.get_certs_for_pkesk(pkesk) { for key in cert.keys().secret() { @@ -129,8 +129,6 @@ impl DecryptionHelper for &mut Keyring { } } - // smartcard route: plug in smartcard, attempt decrypt, fail and bail - Err(KeyringFailure::SecretKeyNotFound.into()) } } diff --git a/keyfork-shard/src/openpgp/smartcard.rs b/keyfork-shard/src/openpgp/smartcard.rs new file mode 100644 index 0000000..0f561e0 --- /dev/null +++ b/keyfork-shard/src/openpgp/smartcard.rs @@ -0,0 +1,198 @@ +use std::collections::HashSet; + +use super::openpgp::{ + self, + cert::Cert, + packet::{PKESK, SKESK}, + parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper}, + Fingerprint, +}; + +use card_backend_pcsc::PcscBackend; +use openpgp_card_sequoia::{state::Open, Card}; + +#[derive(Clone, Debug)] +pub enum SmartcardFailure { + #[allow(dead_code)] + SmartCardPromptFailed, + + SmartCardNotFound, +} + +impl std::fmt::Display for SmartcardFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SmartCardPromptFailed => f.write_str("Attempt to prompt for smart card failed"), + Self::SmartCardNotFound => f.write_str("No smart card backend was stored"), + } + } +} + +impl std::error::Error for SmartcardFailure {} + +pub struct SmartcardManager { + current_card: Option>, + root: Cert, +} + +impl SmartcardManager { + pub fn new(root: Cert) -> Self { + Self { + current_card: None, + root, + } + } + + /// Utility function to prompt for a newline from standard input. + pub fn prompt(&self, prompt: impl std::fmt::Display) -> std::io::Result<()> { + eprint!("{prompt}: "); + std::io::stdin().read_line(&mut String::new()).map(|_| ()) + } + + /// Utility function to obtain a prompt from the command line. + pub fn prompt_pin(&self, prompt: impl std::fmt::Display) -> std::io::Result { + eprint!("{prompt}: "); + let mut output = String::new(); + std::io::stdin().read_line(&mut output)?; + Ok(output) + } + + /// Return all [`Fingerprint`] for the currently accessible backends. + /// + /// NOTE: Only implemented for decryption keys. + pub fn iter_fingerprints() -> impl Iterator { + PcscBackend::cards(None).into_iter().flat_map(|iter| { + iter.filter_map(|backend| { + let backend = backend.ok()?; + let mut card = Card::::new(backend).ok()?; + let transaction = card.transaction().ok()?; + transaction + .fingerprints() + .ok()? + .decryption() + .map(|fp| Fingerprint::from_bytes(fp.as_bytes())) + }) + }) + } + + /// Load a backend if any [`Fingerprint`] has been matched by a currently active card. + /// + /// NOTE: Only implemented for decryption keys. + pub fn load_any_fingerprint( + &mut self, + fingerprints: impl IntoIterator, + ) -> Result, Box> { + // NOTE: This can't be HashSet::from_iter() because from_iter() requires a passed-in state + // I do not want to provide. + let mut requested_fingerprints = HashSet::new(); + requested_fingerprints.extend(fingerprints); + + let mut had_any_backend = false; + + while !had_any_backend { + // Load all backends, confirm if any have any fingerprints + for backend in PcscBackend::cards(None)? { + had_any_backend = true; + let backend = backend?; + let mut card = Card::::new(backend)?; + let transaction = card.transaction()?; + let mut fingerprint = None; + if let Some(fp) = transaction + .fingerprints()? + .decryption() + .map(|fp| Fingerprint::from_bytes(fp.as_bytes())) + { + if requested_fingerprints.contains(&fp) { + fingerprint.replace(fp); + } + } + drop(transaction); + if fingerprint.is_some() { + self.current_card.replace(card); + return Ok(fingerprint); + } + } + + eprintln!("No matching smartcard detected."); + self.prompt("Please plug in a smart card and press enter")?; + } + + Ok(None) + } +} + +impl VerificationHelper for &mut SmartcardManager { + fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result> { + Ok(ids + .iter() + .flat_map(|kh| { + if &self.root.key_handle() == kh { + Some(self.root.clone()) + } else { + None + } + }) + .collect()) + } + + fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> { + for layer in structure.into_iter() { + #[allow(unused_variables)] + match layer { + MessageLayer::Compression { algo } => {} + MessageLayer::Encryption { + sym_algo, + aead_algo, + } => {} + MessageLayer::SignatureGroup { results } => { + for result in results { + if let Err(e) = result { + // FIXME: anyhow leak + return Err(anyhow::anyhow!(e.to_string())); + } + } + } + } + } + Ok(()) + } +} + +impl DecryptionHelper for &mut SmartcardManager { + fn decrypt( + &mut self, + pkesks: &[PKESK], + _skesks: &[SKESK], + sym_algo: Option, + mut decrypt: D, + ) -> openpgp::Result> + where + D: FnMut(openpgp::types::SymmetricAlgorithm, &openpgp::crypto::SessionKey) -> bool, + { + let mut card = self.current_card.take(); + let Some(card) = card.as_mut() else { + return Err(SmartcardFailure::SmartCardNotFound.into()); + }; + + let mut transaction = card.transaction()?; + let fp = transaction + .fingerprints()? + .decryption() + .map(|fp| Fingerprint::from_bytes(fp.as_bytes())); + let pin = self.prompt_pin("Please enter PIN to unlock card")?; + let mut user = transaction.to_user_card(pin.as_str())?; + let mut decryptor = + user.decryptor(&|| println!("Touch confirmation needed for decryption"))?; + for pkesk in pkesks { + if pkesk + .decrypt(&mut decryptor, sym_algo) + .map(|(algo, sk)| decrypt(algo, &sk)) + .unwrap_or(false) + { + return Ok(fp); + } + } + + Err(SmartcardFailure::SmartCardNotFound.into()) + } +}