From a72bfaecec5fddb7b6cfb167d8265e509777bac3 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 19 Oct 2023 17:06:34 -0500 Subject: [PATCH] keyfork-shard: split openpgp code into its own module --- keyfork-shard/Cargo.toml | 6 +- .../src/bin/keyfork-shard-combine-openpgp.rs | 2 +- .../src/bin/keyfork-shard-split-openpgp.rs | 2 +- keyfork-shard/src/lib.rs | 340 +----------------- keyfork-shard/src/openpgp.rs | 338 +++++++++++++++++ keyfork-shard/src/{ => openpgp}/keyring.rs | 2 +- 6 files changed, 348 insertions(+), 342 deletions(-) create mode 100644 keyfork-shard/src/openpgp.rs rename keyfork-shard/src/{ => openpgp}/keyring.rs (99%) diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index 2bffee0..f5a5ec1 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -5,11 +5,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["openpgp"] +openpgp = ["sequoia-openpgp"] + [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" +sequoia-openpgp = { version = "1.16.1", optional = true } 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 index 8e20e1c..5b9bcbf 100644 --- a/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -6,7 +6,7 @@ use std::{ str::FromStr, }; -use keyfork_shard::{combine, discover_certs, parse_messages, openpgp::Cert}; +use keyfork_shard::openpgp::{combine, discover_certs, parse_messages, openpgp::Cert}; type Result> = std::result::Result; diff --git a/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs index 7d41640..cdf2107 100644 --- a/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs +++ b/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs @@ -1,6 +1,6 @@ use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; -use keyfork_shard::{discover_certs, openpgp::Cert, split}; +use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split}; #[derive(Clone, Debug)] enum Error { diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index 76c0b4c..9c60f1d 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -1,338 +1,2 @@ -use std::{ - collections::{HashMap, VecDeque}, - 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::{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, PacketPile -}; -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()?; - - 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) - } -} - -pub fn parse_messages(reader: impl Read + Send + Sync) -> Result >{ - let mut pkesks = Vec::new(); - let mut encrypted_messages = VecDeque::new(); - - for packet in PacketPile::from_reader(reader)?.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()); - } - } - } - - Ok(encrypted_messages) -} - -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(()) -} +#[cfg(feature = "openpgp")] +pub mod openpgp; diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs new file mode 100644 index 0000000..76c0b4c --- /dev/null +++ b/keyfork-shard/src/openpgp.rs @@ -0,0 +1,338 @@ +use std::{ + collections::{HashMap, VecDeque}, + 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::{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, PacketPile +}; +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()?; + + 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) + } +} + +pub fn parse_messages(reader: impl Read + Send + Sync) -> Result >{ + let mut pkesks = Vec::new(); + let mut encrypted_messages = VecDeque::new(); + + for packet in PacketPile::from_reader(reader)?.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()); + } + } + } + + Ok(encrypted_messages) +} + +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(()) +} diff --git a/keyfork-shard/src/keyring.rs b/keyfork-shard/src/openpgp/keyring.rs similarity index 99% rename from keyfork-shard/src/keyring.rs rename to keyfork-shard/src/openpgp/keyring.rs index c169019..db4352f 100644 --- a/keyfork-shard/src/keyring.rs +++ b/keyfork-shard/src/openpgp/keyring.rs @@ -1,4 +1,4 @@ -use crate::openpgp::{ +use super::openpgp::{ self, cert::Cert, packet::{PKESK, SKESK},