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