//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data. use std::{ str::FromStr, time::{Duration, SystemTime, SystemTimeError}, }; use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey}; use ed25519_dalek::SigningKey; pub use keyfork_derive_util as derive_util; pub use sequoia_openpgp as openpgp; use sequoia_openpgp::{ packet::{ key::{Key4, PrimaryRole, SubordinateRole}, signature::SignatureBuilder, Key, UserID, }, types::{KeyFlags, SignatureType}, Cert, Packet, }; pub type XPrvKey = SigningKey; pub type XPrv = ExtendedPrivateKey; /// 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), /// A derivation index could not be created from the given index. #[error("Could not create derivation index: {0}")] Index(#[from] IndexError), /// 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 private key data, with derived keys from the given derivation /// response, keys, and User ID. /// /// Certificates are created with a default expiration of one day, but may be configured to expire /// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15 /// days), "1m" (one month), or "2y" (two years). /// /// # Errors /// The function may error for any condition mentioned in [`Error`]. pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result { let primary_key_flags = match keys.first() { Some(kf) if kf.for_certification() => kf, _ => return Err(Error::NotCert), }; let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1); let now = SystemTime::now(); let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() { Ok(var) => { let ch = var.pop(); match (ch, u64::from_str(var)) { (Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => { let multiplier = match ch { 'd' => 1, 'm' => 30, 'y' => 365, _ => unreachable!(), }; now + Duration::from_secs(60 * 60 * 24 * expire * multiplier) } _ => now + Duration::from_secs(60 * 60 * 24), } } Err(_) => now + Duration::from_secs(60 * 60 * 24), }; // Create certificate with initial key and signature let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?; let mut primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519( &PrivateKey::to_bytes(derived_primary_key.private_key()), epoch, )?); primary_key.set_creation_time(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(expiration_date.duration_since(epoch)?)? // .set_signature_creation_time(now)? .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 configured expiration or (default) one day let mut keypair = primary_key.clone().into_keypair()?; let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?; 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 mut 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, )?) }; subkey.set_creation_time(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(expiration_date.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(expiration_date.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) }