use std::{ path::{Path, PathBuf}, time::SystemTime, }; use anyhow::{bail, Context, Result}; use sequoia_openpgp::{ cert::CertRevocationBuilder, packet::Signature, parse::Parse, serialize::SerializeInto, types::{ReasonForRevocation, SignatureType}, Cert, Packet, }; pub(crate) struct Paths { pub(crate) spec: PathBuf, pub(crate) secret: PathBuf, pub(crate) public: PathBuf, pub(crate) rev: PathBuf, } impl Paths { pub(crate) fn new(base: PathBuf) -> Paths { let mut spec = base.clone(); spec.push("spec.yml"); let mut secret = base.clone(); secret.push("secret.asc"); let mut public = base.clone(); public.push("public.asc"); let mut rev = base; rev.push("rev.asc"); Paths { spec, secret, public, rev, } } pub(crate) fn load(&self) -> Result> { let mut merged = if self .secret .try_exists() .context("Couldn't check if secret part of certificate exists")? { Cert::from_file(&self.secret) .context("Couldn't load secret part of certificate from file")? } else { return Ok(None); }; if let (_, Some(secret_material)) = merged.primary_key().key().clone().take_secret() { if secret_material.is_encrypted() { bail!("Secret material for primary key is encrypted!"); } } else { bail!("Primary key has no secret material!") } if self .public .try_exists() .context("Couldn't check if public part of certificate exists")? { merged = merged .merge_public_and_secret( Cert::from_file(&self.public) .context("Couldn't load public part from certificate from file")?, ) .context("Couldn't merge public part onto secret part of certificate")? } if self.rev.try_exists()? { merged = merged .merge_public_and_secret( Cert::from_file(&self.rev) .context("Couldn't load revocation for certificate from file")?, ) .context("Couldn't merge revocation onto secret part of certificate")? } let fingerprint = merged.fingerprint(); let mut filtered = vec![]; let mut revocation = None; // strip revocation for primary key for packet in merged.into_packets() { match packet { Packet::Signature(Signature::V4(sig)) => { if sig.typ() == SignatureType::KeyRevocation && sig .issuer_fingerprints() .any(|issuer| issuer.eq(&fingerprint)) { revocation = Some(Signature::V4(sig)); } else { filtered.push(Packet::Signature(Signature::V4(sig))); } } packet => filtered.push(packet), } } let filtered = Cert::from_packets(filtered.into_iter()) .context("Could't parse cert from packets after removing primary key revocations")?; let revocation = revocation .ok_or(()) .or_else(|()| { let now = SystemTime::now(); CertRevocationBuilder::new() .set_signature_creation_time(now) .context("Failed to set signature creation time")? .set_reason_for_revocation(ReasonForRevocation::Unspecified, b"Unspecified") .context("Failed to set revocation reason")? .build( &mut filtered .primary_key() .key() .clone() .parts_into_secret() .context("Failed to get secret parts of certificate primary key")? .into_keypair() .context("Failed to convert primary key into keypair")?, &filtered, None, ) .context("Failed to generate revocation for certificate primary key") }) .context("Failed to generate new revocation after not finding an existing one")?; Ok(Some((filtered, revocation))) } pub(crate) fn write(&self, cert: Cert, rev: Signature) -> Result<()> { self.write_part(Self::public, cert.armored()) .context("Failed to write public certificate to file")?; self.write_part(Self::secret, cert.as_tsk().armored()) .context("Failed to certificate including private keys to file")?; self.write_part( Self::rev, cert.insert_packets(rev) .context("Failed to write revocation signature packet into certificate")? .armored(), ) .context("Failed to write revoked certificate to file")?; Ok(()) } fn write_part(&self, path_selector: F, data: S) -> Result<()> where F: Fn(&Self) -> &Path, S: SerializeInto, { std::fs::write( path_selector(self), SerializeInto::to_vec(&data) .context("Failed to serialize certificate into byte vec")?, ) .context("Failed to write serialized certificate into file")?; Ok(()) } fn public(&self) -> &Path { self.public.as_path() } fn secret(&self) -> &Path { self.secret.as_path() } fn rev(&self) -> &Path { self.rev.as_path() } }