From 31d1992e163c197dd10ffc0a9ea9835dd2c40bfe Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Oct 2023 04:28:12 -0500 Subject: [PATCH] keyfork-shard: initial commit --- Cargo.lock | 78 +++- Cargo.toml | 1 + keyfork-shard/Cargo.toml | 15 + .../src/bin/keyfork-shard-combine-openpgp.rs | 98 +++++ .../src/bin/keyfork-shard-split-openpgp.rs | 85 +++++ keyfork-shard/src/keyring.rs | 118 ++++++ keyfork-shard/src/lib.rs | 337 ++++++++++++++++++ 7 files changed, 730 insertions(+), 2 deletions(-) create mode 100644 keyfork-shard/Cargo.toml create mode 100644 keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs create mode 100644 keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs create mode 100644 keyfork-shard/src/keyring.rs create mode 100644 keyfork-shard/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 669e511..bd215d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + [[package]] name = "aho-corasick" version = "1.0.4" @@ -741,6 +747,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.1" @@ -797,7 +812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.1", ] [[package]] @@ -948,6 +963,19 @@ dependencies = [ "smex", ] +[[package]] +name = "keyfork-shard" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "keyfork-derive-openpgp", + "sequoia-openpgp", + "serde", + "sharks", + "smex", +] + [[package]] name = "keyfork-slip10-test-data" version = "0.1.0" @@ -1376,11 +1404,22 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -1391,6 +1430,16 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -1697,6 +1746,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "sharks" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902b1e955f8a2e429fb1bad49f83fb952e6195d3c360ac547ff00fb826388753" +dependencies = [ + "hashbrown 0.9.1", + "rand 0.8.5", + "zeroize", +] + [[package]] name = "shlex" version = "1.2.0" @@ -2332,3 +2392,17 @@ name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] diff --git a/Cargo.toml b/Cargo.toml index 6852632..ca15585 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "keyfork-frame", "keyfork-mnemonic-util", "keyfork-plumbing", + "keyfork-shard", "keyfork-slip10-test-data", "keyforkd", "keyforkd-client", diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml new file mode 100644 index 0000000..2bffee0 --- /dev/null +++ b/keyfork-shard/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "keyfork-shard" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +bincode = "1.3.3" +keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } +sequoia-openpgp = "1.16.1" +serde = "1.0.188" +sharks = "0.5.0" +smex = { version = "0.1.0", path = "../smex" } diff --git a/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs new file mode 100644 index 0000000..98963c1 --- /dev/null +++ b/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -0,0 +1,98 @@ +use std::{ + collections::VecDeque, + env, + io::{stdin, stdout}, + path::PathBuf, + process::ExitCode, + str::FromStr, +}; + +use keyfork_shard::{combine, discover_certs, openpgp::Cert, EncryptedMessage}; +use openpgp::{ + packet::Packet, + parse::Parse, + PacketPile, +}; +use sequoia_openpgp as openpgp; + +#[derive(Clone, Debug)] +enum Error { + Usage(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Usage(program_name) => { + write!(f, "Usage: {program_name} threshold key_discovery") + } + } + } +} + +impl std::error::Error for Error {} + +type Result> = std::result::Result; + +fn validate(threshold: &str, key_discovery: &str) -> Result<(u8, Vec)> { + let threshold = u8::from_str(threshold)?; + let key_discovery = PathBuf::from(key_discovery); + + // Verify path exists + std::fs::metadata(&key_discovery)?; + + // Load certs from path + let certs = discover_certs(key_discovery)?; + + Ok((threshold, certs)) +} + +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)?, + _ => return Err(Error::Usage(program_name).into()), + }; + + let stdin = stdin(); + + let mut pkesks = Vec::new(); + let mut encrypted_messages = VecDeque::new(); + + for packet in PacketPile::from_reader(stdin)?.into_children() { + match packet { + Packet::PKESK(p) => pkesks.push(p), + Packet::SEIP(s) => { + encrypted_messages.push_back(EncryptedMessage::with_swap(&mut pkesks, s)); + } + s => { + panic!("Invalid variant found: {}", s.tag()); + } + } + } + + let encrypted_metadata = encrypted_messages + .pop_front() + .expect("any pgp encrypted message"); + + combine( + threshold, + cert_list, + encrypted_metadata, + encrypted_messages.into(), + stdout(), + )?; + + Ok(()) +} + +fn main() -> ExitCode { + let result = run(); + if let Err(e) = result { + eprintln!("Error: {e}"); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} diff --git a/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs new file mode 100644 index 0000000..7d41640 --- /dev/null +++ b/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs @@ -0,0 +1,85 @@ +use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; + +use keyfork_shard::{discover_certs, openpgp::Cert, split}; + +#[derive(Clone, Debug)] +enum Error { + Usage(String), + Input, + Threshold(u8, u8), + InvalidCertCount(usize, u8), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Usage(program_name) => { + write!(f, "Usage: {program_name} threshold max key_discovery") + } + Error::Input => f.write_str("Expected hex encoded input"), + Error::Threshold(threshold, max) => { + write!( + f, + "Invalid threshold: 0 < threshold {threshold} <= max {max} < 256" + ) + } + Error::InvalidCertCount(count, max) => { + write!(f, "Invalid cert count: count {count} != max {max}") + } + } + } +} + +impl std::error::Error for Error {} + +type Result> = std::result::Result; + +fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec)> { + let threshold = u8::from_str(threshold)?; + let max = u8::from_str(max)?; + let key_discovery = PathBuf::from(key_discovery); + if threshold > max { + return Err(Error::Threshold(threshold, max).into()); + } + + // Verify path exists + std::fs::metadata(&key_discovery)?; + + // Load certs from path + let certs = discover_certs(key_discovery)?; + if certs.len() != max.into() { + return Err(Error::InvalidCertCount(certs.len(), max).into()); + } + + Ok((threshold, certs)) +} + +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, max, key_discovery] => validate(threshold, max, key_discovery)?, + _ => return Err(Error::Usage(program_name).into()), + }; + let input = { + use std::io::stdin; + let Some(line) = stdin().lines().next() else { + return Err(Error::Input.into()); + }; + smex::decode(&line?)? + }; + + split(threshold, cert_list, &input, std::io::stdout())?; + + Ok(()) +} + +fn main() -> ExitCode { + let result = run(); + if let Err(e) = result { + eprintln!("Error: {e}"); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} diff --git a/keyfork-shard/src/keyring.rs b/keyfork-shard/src/keyring.rs new file mode 100644 index 0000000..c169019 --- /dev/null +++ b/keyfork-shard/src/keyring.rs @@ -0,0 +1,118 @@ +use crate::openpgp::{ + self, + cert::Cert, + packet::{PKESK, SKESK}, + parse::stream::{DecryptionHelper, VerificationHelper, MessageStructure}, + KeyHandle, KeyID, +}; + +#[derive(Clone, Debug)] +pub enum KeyringFailure { + SecretKeyNotFound, + #[allow(dead_code)] + SmartcardDecrypt, +} + +impl std::fmt::Display for KeyringFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyringFailure::SecretKeyNotFound => f.write_str("Secret key was not found"), + KeyringFailure::SmartcardDecrypt => { + f.write_str("Smartcard could not decrypt any PKESKs") + } + } + } +} + +impl std::error::Error for KeyringFailure {} + +#[derive(Clone, Debug)] +pub struct Keyring { + full_certs: Vec, + root: Option, +} + +impl Keyring { + pub fn new(certs: impl AsRef<[Cert]>) -> Self { + Self { + full_certs: certs.as_ref().to_vec(), + root: Default::default(), + } + } + + // Sets the root cert, returning the old cert + pub fn set_root_cert(&mut self, cert: impl Into>) -> Option { + let mut cert = cert.into(); + std::mem::swap(&mut self.root, &mut cert); + cert + } + + pub fn get_cert_for_primary_keyid<'a>(&'a self, keyid: &KeyID) -> Option<&'a Cert> { + self + .full_certs + .iter() + .find(|cert| &cert.keyid() == keyid) + } + + // NOTE: This can't return an iterator because iterators are all different types + // and returning different types is naughty + fn get_certs_for_pkesk<'a>(&'a self, pkesk: &'a PKESK) -> impl Iterator + 'a { + self.full_certs.iter().filter(move |cert| { + pkesk.recipient().is_wildcard() + || cert.keys().any(|k| { + &k.keyid() == pkesk.recipient() + }) + }) + } +} + +impl VerificationHelper for &mut Keyring { + fn get_certs(&mut self, _ids: &[KeyHandle]) -> openpgp::Result> { + // TODO: no verification logic until we mark a cert as "root" + // this is the first cert in the metadata list + Ok(Vec::new()) + } + fn check(&mut self, _structure: MessageStructure) -> openpgp::Result<()> { + // TODO: ensure that we have a "root" cert and assign it + Ok(()) + } +} + +impl DecryptionHelper for &mut Keyring { + 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, + { + // optimized 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() { + let secret_key = key.key().clone(); + // NOTE: Returns an error if using an encrypted secret key. + // TODO: support skipping or validating encrypted secret keys. + let mut keypair = secret_key.into_keypair()?; + if pkesk + .decrypt(&mut keypair, sym_algo) + .map(|(algo, sk)| decrypt(algo, &sk)) + .unwrap_or(false) + { + return Ok(Some(key.fingerprint())); + } + } + } + } + + // smartcard route: plug in smartcard, attempt decrypt, fail and bail + + Err(KeyringFailure::SecretKeyNotFound.into()) + } +} diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs new file mode 100644 index 0000000..64f24f9 --- /dev/null +++ b/keyfork-shard/src/lib.rs @@ -0,0 +1,337 @@ +use std::{ + collections::HashMap, + io::{Read, Write}, + path::Path, + str::FromStr, +}; + +use keyfork_derive_openpgp::derive_util::{ + request::{DerivationAlgorithm, DerivationRequest}, + DerivationPath, +}; +use openpgp::{ + armor::{Kind, Writer}, + cert::{Cert, CertParser, ValidCert}, + packet::{Tag, UserID, PKESK, SEIP}, + parse::{stream::DecryptorBuilder, Parse}, + policy::{NullPolicy, Policy, StandardPolicy}, + serialize::{ + stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer}, + Marshal, + }, + types::KeyFlags, + KeyID, +}; +pub use sequoia_openpgp as openpgp; +use sharks::{Share, Sharks}; + +mod keyring; +use keyring::Keyring; + +// TODO: better error handling + +#[derive(Debug, Clone)] +pub struct WrappedError(String); + +impl std::fmt::Display for WrappedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for WrappedError {} + +pub type Result> = std::result::Result; + +#[derive(Debug, Clone)] +pub struct EncryptedMessage { + pkesks: Vec, + message: SEIP, +} + +impl EncryptedMessage { + pub fn with_swap(pkesks: &mut Vec, seip: SEIP) -> Self { + Self { + pkesks: std::mem::take(pkesks), + message: seip, + } + } + + pub fn decrypt_with(&self, policy: &'_ dyn Policy, keyring: &mut Keyring) -> Result> { + let mut packets = vec![]; + + for pkesk in &self.pkesks { + let mut packet = vec![]; + pkesk.serialize(&mut packet)?; + let message = Message::new(&mut packets); + let mut message = ArbitraryWriter::new(message, Tag::PKESK)?; + message.write_all(&packet)?; + message.finalize()?; + } + let mut packet = vec![]; + self.message.serialize(&mut packet)?; + let message = Message::new(&mut packets); + let mut message = ArbitraryWriter::new(message, Tag::SEIP)?; + message.write_all(&packet)?; + message.finalize()?; + + /* + // TODO: only serialize the message and use provided PKESK vec + let mut pkesks = vec![]; + let ppr = PacketParser::from_reader(&packets[..])?; + while let PacketParserResult::Some(mut pp) = ppr { + match pp.packet { + openpgp::Packet::PKESK(pkesk) => pkesks.push(pkesk), + openpgp::Packet::SEIP(_) => { + keyring.decrypt(&pkesks, &[], None, |algo, sk| { + // pushes packet to stack + pp.decrypt(algo, sk).is_ok() + }); + }, + p => panic!("Unexpected packet type: {}", p.tag()), + } + } + */ + + let mut decryptor = + DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, keyring)?; + + let mut content = vec![]; + decryptor.read_to_end(&mut content)?; + Ok(content) + } +} + +pub fn discover_certs(path: impl AsRef) -> Result> { + let path = path.as_ref(); + + if path.is_file() { + let mut vec = vec![]; + for cert in CertParser::from_file(path)? { + vec.push(cert?); + } + Ok(vec) + } else { + let mut vec = vec![]; + for entry in path + .read_dir()? + .filter_map(Result::ok) + .filter(|p| p.path().is_file()) + { + vec.push(Cert::from_file(entry.path())?); + } + Ok(vec) + } +} + +fn get_encryption_keys<'a>( + cert: &'a ValidCert, +) -> openpgp::cert::prelude::ValidKeyAmalgamationIter< + 'a, + openpgp::packet::key::PublicParts, + openpgp::packet::key::UnspecifiedRole, +> { + cert.keys() + .alive() + .revoked(false) + .supported() + .for_storage_encryption() +} + +fn get_decryption_keys<'a>( + cert: &'a ValidCert, +) -> openpgp::cert::prelude::ValidKeyAmalgamationIter< + 'a, + openpgp::packet::key::SecretParts, + openpgp::packet::key::UnspecifiedRole, +> { + cert.keys() + /* + .alive() + .revoked(false) + .supported() + */ + .for_storage_encryption() + .secret() +} + +pub fn combine( + threshold: u8, + certs: Vec, + metadata: EncryptedMessage, + messages: Vec, + mut output: impl Write, +) -> Result<()> { + // Be as liberal as possible when decrypting. + // We don't want to invalidate someone's keys just because the old sig expired. + let policy = NullPolicy::new(); + + let mut keyring = Keyring::new(certs); + let content = metadata.decrypt_with(&policy, &mut keyring)?; + + let mut cert_parser = CertParser::from_bytes(&content)?; + let root_cert = match cert_parser.next() { + Some(Ok(c)) => c, + Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"), + None => panic!("No certs found in cert parser"), + }; + let certs = cert_parser.collect::>>()?; + keyring.set_root_cert(root_cert); + let mut messages: HashMap = + HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages)); + let mut decrypted_messages: HashMap> = HashMap::new(); + + // NOTE: This is ONLY stable because we control the generation of PKESK packets and + // encode the policy to ourselves. + for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) { + let valid_cert = valid_cert?; + // get keys from keyring for cert + let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else { + continue; + }; + let secret_cert = secret_cert.with_policy(&policy, None)?; + let keys = get_decryption_keys(&secret_cert).collect::>(); + if !keys.is_empty() { + if let Some(message) = messages.get_mut(&valid_cert.keyid()) { + for (pkesk, key) in message.pkesks.iter_mut().zip(keys) { + pkesk.set_recipient(key.keyid()); + } + // we have a pkesk, decrypt via keyring + let result = message.decrypt_with(&policy, &mut keyring); + match result { + Ok(message) => { + decrypted_messages.insert(valid_cert.keyid(), message); + } + Err(e) => { + eprintln!( + "Could not decrypt with fingerprint {}: {}", + valid_cert.keyid(), + e + ); + // do nothing, key will be retained + } + } + } + } + } + + // clean decrypted messages from encrypted messages + messages.retain(|k, _v| !decrypted_messages.contains_key(k)); + + 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") + } + + let shares = decrypted_messages + .values() + .map(|message| Share::try_from(message.as_slice())) + .collect::, &str>>() + .map_err(|e| WrappedError(e.to_string()))?; + let secret = Sharks(threshold).recover(&shares)?; + + output.write_all(smex::encode(&secret).as_bytes())?; + + Ok(()) +} + +pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { + // build cert to sign encrypted shares + let userid = UserID::from("keyfork-sss"); + let kdr = DerivationRequest::new( + DerivationAlgorithm::Ed25519, + &DerivationPath::from_str("m/7366512'/0'")?, + ) + .derive_with_master_seed(secret.to_vec())?; + let derived_cert = keyfork_derive_openpgp::derive( + kdr, + &[KeyFlags::empty().set_certification().set_signing()], + userid, + )?; + let signing_key = derived_cert + .primary_key() + .parts_into_secret()? + .key() + .clone() + .into_keypair()?; + + let sharks = Sharks(threshold); + let dealer = sharks.dealer(secret); + let shares = dealer.map(|s| Vec::from(&s)).collect::>(); + let policy = StandardPolicy::new(); + let mut writer = Writer::new(output, Kind::Message)?; + + let mut total_recipients = vec![]; + let mut messages = vec![]; + + for (share, cert) in shares.iter().zip(certs) { + total_recipients.push(cert.clone()); + let valid_cert = cert.with_policy(&policy, None)?; + let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); + + let mut message_output = vec![]; + let message = Message::new(&mut message_output); + let message = Encryptor::for_recipients( + message, + encryption_keys + .iter() + .map(|k| Recipient::new(KeyID::wildcard(), k.key())), + ) + .build()?; + let message = Signer::new(message, signing_key.clone()).build()?; + let mut message = LiteralWriter::new(message).build()?; + message.write_all(share)?; + message.finalize()?; + + messages.push(message_output); + } + + let mut pp = vec![]; + // store derived cert to verify provided shares + derived_cert.serialize(&mut pp)?; + for recipient in &total_recipients { + recipient.serialize(&mut pp)?; + } + + // verify packet pile + for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp)? + .skip(1) + .zip(total_recipients.iter()) + { + if packet_cert? != *cert { + panic!( + "packet pile could not recreate cert: {}", + cert.fingerprint() + ); + } + } + + let valid_certs = total_recipients + .iter() + .map(|c| c.with_policy(&policy, None)) + .collect::>>()?; + + let total_recipients = valid_certs.iter().flat_map(|vc| { + get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key())) + }); + + // metadata + let mut message_output = vec![]; + let message = Message::new(&mut message_output); + let message = Encryptor::for_recipients(message, total_recipients).build()?; + let mut message = LiteralWriter::new(message).build()?; + message.write_all(&pp)?; + message.finalize()?; + writer.write_all(&message_output)?; + + for message in messages { + writer.write_all(&message)?; + } + + writer.finalize()?; + + Ok(()) +}