From 3b5c1340db2a1372f2cd2713cd0f2847f9e356d7 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 12 Feb 2024 11:51:49 -0500 Subject: [PATCH] keyfork-shard: add new methods to trait to support split() --- crates/keyfork-shard/src/lib.rs | 59 ++++--- crates/keyfork-shard/src/openpgp.rs | 237 ++++++++++++++++++---------- 2 files changed, 188 insertions(+), 108 deletions(-) diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 047ee09..5135cbc 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -31,17 +31,20 @@ pub trait Format { type Error: std::error::Error + 'static; /// A type encapsulating the public key recipients of shards. - type PublicKeyData; + type PublicKeyData: IntoIterator; + + /// A type encapsulating a single public key recipient. + type PublicKey; /// A type encapsulating the private key recipients of shards. type PrivateKeyData; + /// A type representing a Signer derived from the secret. + type SigningKey; + /// A type representing the parsed, but encrypted, Shard data. type ShardData; - /// A type representing a Signer derived from the secret. - type Signer; - /// Parse the public key data from a readable type. /// /// # Errors @@ -53,6 +56,36 @@ pub trait Format { key_data_path: impl AsRef, ) -> Result; + /// Derive a signer + fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey; + + /// Format a header containing necessary metadata. Such metadata contains a version byte, a + /// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to + /// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork + /// will use the same key_data for both, ensuring an iteration of this method will match with + /// iterations in methods called later. + /// + /// # Errors + /// The method may return an error if encryption to any of the public keys fails. + fn format_encrypted_header( + &self, + signing_key: &Self::SigningKey, + key_data: &Self::PublicKeyData, + threshold: u8, + ) -> Result, Self::Error>; + + /// Format a shard encrypted to the given public key, signing with the private key. + /// + /// # Errors + /// The method may return an error if the public key used to encrypt the shard is unsuitable + /// for encryption, or if an error occurs while encrypting. + fn encrypt_shard( + &self, + shard: &[u8], + public_key: &Self::PublicKey, + signing_key: &mut Self::SigningKey, + ) -> Result, Self::Error>; + /// Parse the private key data from a readable type. The private key may not be accessible (it /// may be hardware only, such as a smartcard), for which this method may return None. /// @@ -85,24 +118,6 @@ pub trait Format { shard_file: impl Write, ) -> Result<(), Self::Error>; - /// Derive a Signer from the secret. - /// - /// # Errors - /// This function may return an error if a Signer could not be properly created. - fn derive_signer(&self, secret: &[u8]) -> Result; - - /// Encrypt multiple shares to public keys. - /// - /// # Errors - /// The method may return an error if the share could not be encrypted to a public key or if - /// the ShardData could not be created. - fn generate_shard_data( - &self, - shares: &[Share], - signer: &Self::Signer, - public_keys: Self::PublicKeyData, - ) -> Result; - /// Decrypt shares and associated metadata from a readable input. For the current version of /// Keyfork, the only associated metadata is a u8 representing the threshold to combine /// secrets. diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index efe099e..d58f91a 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -57,9 +57,8 @@ const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; use super::{ - InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT, - QRCODE_TIMEOUT, - Format, + Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, + QRCODE_PROMPT, QRCODE_TIMEOUT, }; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding @@ -256,10 +255,11 @@ impl OpenPGP { impl Format for OpenPGP { type Error = Error; + type PublicKey = Cert; type PublicKeyData = Vec; type PrivateKeyData = Vec; + type SigningKey = Cert; type ShardData = Vec; - type Signer = openpgp::crypto::KeyPair; fn parse_public_key_data( &self, @@ -268,6 +268,145 @@ impl Format for OpenPGP { Self::discover_certs(key_data_path) } + /// Derive an OpenPGP Shard certificate from the given seed. + fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey { + let seed = VariableLengthSeed::new(seed); + // build cert to sign encrypted shares + let userid = UserID::from("keyfork-sss"); + let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path"); + let xprv = XPrv::new(seed) + .derive_path(&path) + .expect("valid derivation"); + keyfork_derive_openpgp::derive( + xprv, + &[KeyFlags::empty().set_certification().set_signing()], + &userid, + ) + .expect("valid cert creation") + } + + fn format_encrypted_header( + &self, + signing_key: &Self::SigningKey, + key_data: &Self::PublicKeyData, + threshold: u8, + ) -> Result, Self::Error> { + let policy = StandardPolicy::new(); + let mut pp = vec![SHARD_METADATA_VERSION, threshold]; + // Note: Sequoia does not export private keys on a Cert, only on a TSK + signing_key + .serialize(&mut pp) + .expect("serialize cert into bytes"); + for cert in key_data { + cert.serialize(&mut pp) + .expect("serialize pubkey into bytes"); + } + + // verify packet pile + let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..]) + .expect("should have certs"); + let first_cert = iter.next().transpose().ok().flatten().expect("first cert"); + assert_eq!(signing_key, &first_cert); + + for (packet_cert, cert) in iter.zip(key_data) { + assert_eq!( + &packet_cert.expect("parsed packet cert"), + cert, + "packet pile could not recreate cert: {}", + cert.fingerprint(), + ); + } + + let valid_certs = key_data + .iter() + .map(|c| c.with_policy(&policy, None)) + .collect::>>() + .map_err(Error::Sequoia)?; + let recipients = valid_certs.iter().flat_map(|vc| { + get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key())) + }); + + // Process is as follows: + // * Any OpenPGP message + // * An encrypted message + // * A literal message + // * The packet pile + // + // When decrypting, OpenPGP will see: + // * A message, and parse it + // * An encrypted message, and decrypt it + // * A literal message, and extract it + // * The packet pile + let mut output = vec![]; + let message = Message::new(&mut output); + let encrypted_message = Encryptor2::for_recipients(message, recipients) + .build() + .map_err(Error::Sequoia)?; + let mut literal_message = LiteralWriter::new(encrypted_message) + .build() + .map_err(Error::Sequoia)?; + literal_message.write_all(&pp).map_err(Error::SequoiaIo)?; + literal_message.finalize().map_err(Error::Sequoia)?; + + Ok(output) + } + + fn encrypt_shard( + &self, + shard: &[u8], + public_key: &Cert, + signing_key: &mut Self::SigningKey, + ) -> Result> { + let policy = StandardPolicy::new(); + let valid_cert = public_key + .with_policy(&policy, None) + .map_err(Error::Sequoia)?; + let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); + + let signing_key = signing_key + .primary_key() + .parts_into_secret() + .map_err(Error::Sequoia)? + .key() + .clone() + .into_keypair() + .map_err(Error::Sequoia)?; + + // Process is as follows: + // * Any OpenPGP message + // * An encrypted message + // * A signed message + // * A literal message + // * The shard itself + // + // When decrypting, OpenPGP will see: + // * A message, and parse it + // * An encrypted message, and decrypt it + // * A signed message, and verify it + // * A literal message, and extract it + // * The shard itself + let mut message_output = vec![]; + let message = Message::new(&mut message_output); + let encrypted_message = Encryptor2::for_recipients( + message, + encryption_keys + .iter() + .map(|k| Recipient::new(KeyID::wildcard(), k.key())), + ) + .build() + .map_err(Error::Sequoia)?; + let signed_message = Signer::new(encrypted_message, signing_key) + .build() + .map_err(Error::Sequoia)?; + let mut message = LiteralWriter::new(signed_message) + .build() + .map_err(Error::Sequoia)?; + message.write_all(shard).map_err(Error::SequoiaIo)?; + message.finalize().map_err(Error::Sequoia)?; + + Ok(message_output) + } + fn parse_private_key_data( &self, key_data_path: impl AsRef, @@ -300,29 +439,6 @@ impl Format for OpenPGP { Ok(encrypted_messages) } - fn derive_signer(&self, secret: &[u8]) -> Result { - let userid = UserID::from("keyfork-sss"); - let path = DerivationPath::from_str("m/7366512'/0'")?; - let seed = VariableLengthSeed::new(secret); - let xprv = XPrv::new(seed).derive_path(&path)?; - let derived_cert = keyfork_derive_openpgp::derive( - xprv, - &[KeyFlags::empty().set_certification().set_signing()], - &userid, - )?; - - let signing_key = derived_cert - .primary_key() - .parts_into_secret() - .map_err(Error::Sequoia)? - .key() - .clone() - .into_keypair() - .map_err(Error::Sequoia)?; - - Ok(signing_key) - } - fn format_shard_file( &self, shard_data: Self::ShardData, @@ -336,53 +452,6 @@ impl Format for OpenPGP { Ok(()) } - fn generate_shard_data( - &self, - shares: &[Share], - signer: &Self::Signer, - public_keys: Self::PublicKeyData, - ) -> std::result::Result { - let policy = StandardPolicy::new(); - let mut total_recipients = vec![]; - let mut messages = vec![]; - - for (share, cert) in shares.iter().zip(public_keys) { - total_recipients.push(cert.clone()); - let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?; - let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); - - let mut message_output = vec![]; - let message = Message::new(&mut message_output); - let message = Encryptor2::for_recipients( - message, - encryption_keys - .iter() - .map(|k| Recipient::new(KeyID::wildcard(), k.key())), - ) - .build() - .map_err(Error::Sequoia)?; - let message = Signer::new(message, signer.clone()) - .build() - .map_err(Error::Sequoia)?; - let mut message = LiteralWriter::new(message) - .build() - .map_err(Error::Sequoia)?; - // NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine. - message - .write_all(&Vec::from(share)) - .map_err(Error::SequoiaIo)?; - message.finalize().map_err(Error::Sequoia)?; - - messages.push(message_output); - } - - // A little bit of back and forth, we're going to parse the messages just to serialize them - // later. - let message = messages.into_iter().flatten().collect::>(); - let data = self.parse_shard_file(message.as_slice())?; - Ok(data) - } - fn decrypt_all_shards( &self, private_keys: Option, @@ -406,11 +475,8 @@ impl Format for OpenPGP { // because we control the order packets are encrypted and certificates are stored. // TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage - let mut messages: HashMap = certs - .iter() - .map(Cert::keyid) - .zip(shard_data) - .collect(); + let mut messages: HashMap = + certs.iter().map(Cert::keyid).zip(shard_data).collect(); let mut decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; @@ -454,17 +520,15 @@ impl Format for OpenPGP { keyring.set_root_cert(root_cert.clone()); manager.set_root_cert(root_cert.clone()); - let mut messages: HashMap = certs - .iter() - .map(Cert::keyid) - .zip(shard_data) - .collect(); + let mut messages: HashMap = + certs.iter().map(Cert::keyid).zip(shard_data).collect(); let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; if let Some(message) = decrypted_messages.into_values().next() { - let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + let share = Share::try_from(message.as_slice()) + .map_err(|e| SharksError::Share(e.to_string()))?; return Ok((share, threshold)); } @@ -472,7 +536,8 @@ impl Format for OpenPGP { decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?; if let Some(message) = decrypted_messages.into_values().next() { - let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + let share = Share::try_from(message.as_slice()) + .map_err(|e| SharksError::Share(e.to_string()))?; return Ok((share, threshold)); }