keyfork/crates/derive/keyfork-derive-openpgp/src/lib.rs

187 lines
7.8 KiB
Rust
Raw Normal View History

//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
2024-01-16 02:44:48 +00:00
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<SigningKey>;
2024-01-16 02:44:48 +00:00
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]
pub enum Error {
2024-01-16 02:44:48 +00:00
/// An error occurred with the internal OpenPGP library.
#[error("{0}")]
Anyhow(#[from] anyhow::Error),
2024-01-16 02:44:48 +00:00
/// 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),
2024-01-16 02:44:48 +00:00
/// A derivation index could not be created from the given index.
#[error("Could not create derivation index: {0}")]
Index(#[from] IndexError),
2024-01-16 02:44:48 +00:00
/// 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),
2024-01-16 02:44:48 +00:00
/// 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),
2024-01-16 02:44:48 +00:00
/// The first certificate in an OpenPGP keychain must have the Certify capability.
#[error("First key in certificate must have certify capability")]
NotCert,
2024-01-16 02:44:48 +00:00
/// The given index was out of bounds.
#[error("Index out of bounds: {0}")]
IndexOutOfBounds(#[from] std::num::TryFromIntError),
}
2024-01-16 02:44:48 +00:00
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// 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).
2024-01-16 02:44:48 +00:00
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
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();
2024-02-18 22:59:23 +00:00
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)
}