//! Creation of OpenPGP certificates from BIP-0032 derived data. use std::time::{Duration, SystemTime, SystemTimeError}; use derive_util::{ request::{DerivationResponse, TryFromDerivationResponseError}, DerivationIndex, ExtendedPrivateKey, PrivateKey, }; use ed25519_dalek::SigningKey; pub use keyfork_derive_util as derive_util; use sequoia_openpgp::{ packet::{ key::{Key4, PrimaryRole, SubordinateRole}, signature::SignatureBuilder, Key, UserID, }, types::{KeyFlags, SignatureType}, Cert, Packet, }; pub use sequoia_openpgp as openpgp; /// An error occurred while creating an OpenPGP key. #[derive(Debug, thiserror::Error)] pub enum Error { /// An error occurred with the internal OpenPGP library. #[error("{0}")] Anyhow(#[from] anyhow::Error), /// The key was configured with both encryption and non-encryption key flags. Keys can either /// support Ed25519 signatures or Curve25519 ECDH. #[error("Key configured with both encryption and non-encryption key flags: {0:?}")] InvalidKeyFlags(KeyFlags), /// The derivation response contained incorrect data. #[error("Incorrect derived data: {0}")] IncorrectDerivedData(#[from] TryFromDerivationResponseError), /// A derivation index could not be created from the given index. #[error("Could not create derivation index: {0}")] Index(#[from] keyfork_derive_util::index::Error), /// A derivation operation could not be performed against the private key. #[error("Could not perform operation against private key: {0}")] PrivateKey(#[from] keyfork_derive_util::extended_key::private_key::Error), /// The operation involving system time was invalid. This means the system clock moved a /// significant amount of time during the operation. #[error("Invalid system time: {0}")] SystemTime(#[from] SystemTimeError), /// The first certificate in an OpenPGP keychain must have the Certify capability. #[error("First key in certificate must have certify capability")] NotCert, /// The given index was out of bounds. #[error("Index out of bounds: {0}")] IndexOutOfBounds(#[from] std::num::TryFromIntError), } #[allow(missing_docs)] pub type Result = std::result::Result; /// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User /// ID. /// /// # Errors /// The function may error for any condition mentioned in [`Error`]. pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result { let primary_key_flags = match keys.get(0) { Some(kf) if kf.for_certification() => kf, _ => return Err(Error::NotCert), }; let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1); let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24); // Create certificate with initial key and signature let xprv = ExtendedPrivateKey::::try_from(data)?; let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?; let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519( &PrivateKey::to_bytes(derived_primary_key.private_key()), epoch, )?); let cert = Cert::from_packets(vec![Packet::SecretKey(primary_key.clone())].into_iter())?; // Sign and attach primary key and primary userid let builder = SignatureBuilder::new(SignatureType::PositiveCertification) .set_key_validity_period(one_day.duration_since(epoch)?)? .set_signature_creation_time(epoch)? .set_key_flags(primary_key_flags.clone())?; let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?; let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?; let policy = sequoia_openpgp::policy::StandardPolicy::new(); // Set certificate expiration to one day let mut keypair = primary_key.clone().into_keypair()?; let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?; let cert = cert.insert_packets(signatures)?; let mut cert = cert; for (index, subkey_flags) in keys.iter().enumerate().skip(1) { // Generate subkey let index = u32::try_from(index)?; let derived_key = xprv.derive_child(&DerivationIndex::new(index, true)?)?; let is_enc = subkey_flags.for_transport_encryption() || subkey_flags.for_storage_encryption(); let is_non_enc = subkey_flags.for_certification() || subkey_flags.for_signing() || subkey_flags.for_authentication(); let subkey = if is_enc && is_non_enc { return Err(Error::InvalidKeyFlags(subkey_flags.clone())); } else if is_enc { // Clamp key before exporting as OpenPGP. Reference: // https://gitlab.com/sequoia-pgp/sequoia/-/blob/main/openpgp/src/crypto/backend/rust/asymmetric.rs (see: generate_ecc constructor) // https://github.com/jedisct1/libsodium/blob/b4c5d37fb5ee2736caa4823433926b588911e893/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L91-L93 let mut bytes = PrivateKey::to_bytes(derived_key.private_key()); bytes[0] &= 0b1111_1000; bytes[31] &= !0b1000_0000; bytes[31] |= 0b0100_0000; Key::from( Key4::<_, SubordinateRole>::import_secret_cv25519( &bytes, None, None, epoch, )? ) } else { Key::from( Key4::<_, SubordinateRole>::import_secret_ed25519( &PrivateKey::to_bytes(derived_key.private_key()), epoch, )? ) }; // As per OpenPGP spec, signing keys must backsig the primary key let builder = if subkey_flags.for_signing() { SignatureBuilder::new(SignatureType::SubkeyBinding) .set_key_flags(subkey_flags.clone())? .set_signature_creation_time(epoch)? .set_key_validity_period(one_day.duration_since(epoch)?)? .set_embedded_signature( SignatureBuilder::new(SignatureType::PrimaryKeyBinding) .set_signature_creation_time(epoch)? .sign_primary_key_binding( &mut subkey.clone().into_keypair()?, &primary_key, &subkey, )?, )? } else { SignatureBuilder::new(SignatureType::SubkeyBinding) .set_key_flags(subkey_flags.clone())? .set_signature_creation_time(epoch)? .set_key_validity_period(one_day.duration_since(epoch)?)? }; // Sign subkey with primary key and attach to cert let binding = builder.sign_subkey_binding(&mut primary_key.clone().into_keypair()?, None, &subkey)?; cert = cert.insert_packets(vec![ Packet::SecretSubkey(subkey.clone()), Packet::Signature(binding), ])?; } Ok(cert) }