//! signature usecase: signature verification. use gix::hash::ObjectId; use crate::GitRepository; use crate::signature::impls::SignatureOps; use crate::signature::types::{SignatureInfo, VerifyResult}; fn extract_signature( extra_headers: &[(gix::bstr::BString, gix::bstr::BString)], ) -> Option<&gix::bstr::BString> { for (k, v) in extra_headers { if k.eq_ignore_ascii_case(b"gpgsig") { return Some(v); } } None } /// Verify a PGP signature over the given signed data using the system `gpg`. /// /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if invalid/unsigned. /// Returns `Err` if gpg is unavailable (fail-closed so callers can decide). fn verify_pgp(signature: &[u8], signed_data: &[u8]) -> Result { use std::io::Write; use std::process::{Command, Stdio}; let mut child = Command::new("gpg") .arg("--batch") .arg("--verify") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| crate::Error::Other(format!("gpg unavailable: {e}")))?; let mut stdin = child .stdin .take() .ok_or_else(|| crate::Error::Other("gpg stdin unavailable".to_string()))?; // Write signature then signed data to gpg's stdin. stdin .write_all(signature) .map_err(|e| crate::Error::Io(e.to_string()))?; stdin .write_all(b"\n") .map_err(|e| crate::Error::Io(e.to_string()))?; stdin .write_all(signed_data) .map_err(|e| crate::Error::Io(e.to_string()))?; drop(stdin); let output = child .wait_with_output() .map_err(|e| crate::Error::Io(e.to_string()))?; Ok(output.status.success()) } impl SignatureOps for GitRepository { fn signature_verify_commit(&self, id: &ObjectId) -> Result { let repo = self.repo()?; // Get the raw commit bytes so we can extract the PGP signature and // the exact signed data. let raw = repo .find_object(*id) .map_err(|e| crate::Error::Find(e.to_string()))? .data .to_vec(); let (sig, signed_data) = match gix::diff::object::CommitRefIter::signature(&raw, repo.object_hash()) { Ok(Some((sig, signed_data))) => (sig, signed_data), Ok(None) | Err(_) => { return Ok(VerifyResult { signed: false, valid: false, key_id: None, signer: None, }); } }; let valid = verify_pgp(sig.as_ref(), &signed_data.to_bstring())?; let commit = repo .find_commit(*id) .map_err(|e| crate::Error::Find(e.to_string()))?; let decoded = commit .decode() .map_err(|e| crate::Error::Find(e.to_string()))? .into_owned() .map_err(|e| crate::Error::Find(e.to_string()))?; let info = extract_signature(&decoded.extra_headers).map(|s| SignatureInfo { signature: s.to_string(), payload: decoded.message.to_string(), key_id: None, signer: None, }); Ok(VerifyResult { signed: info.is_some(), valid, key_id: None, signer: None, }) } fn signature_verify_tag(&self, id: &ObjectId) -> Result { let repo = self.repo()?; let tag = repo .find_tag(*id) .map_err(|e| crate::Error::Find(e.to_string()))?; let tag_ref = tag .decode() .map_err(|e| crate::Error::Find(e.to_string()))?; let Some(sig) = tag_ref.pgp_signature else { return Ok(VerifyResult { signed: false, valid: false, key_id: None, signer: None, }); }; // Reconstruct the signed tag payload (gix does not expose it directly, // so verify the signature over the raw tag object bytes). let mut buf = Vec::new(); use gix::objs::WriteTo; tag_ref .write_to(&mut buf) .map_err(|e| crate::Error::Odb(e.to_string()))?; let valid = verify_pgp(sig.as_ref(), &buf)?; Ok(VerifyResult { signed: true, valid, key_id: None, signer: None, }) } fn signature_commit_signature( &self, id: &ObjectId, ) -> Result, crate::Error> { if let Some(hit) = self.cache_get_oid::>("sig_commit_meta", id) { return Ok(hit); } let repo = self.repo()?; let commit = repo .find_commit(*id) .map_err(|e| crate::Error::Find(e.to_string()))?; let decoded = commit .decode() .map_err(|e| crate::Error::Find(e.to_string()))? .into_owned() .map_err(|e| crate::Error::Find(e.to_string()))?; let v = extract_signature(&decoded.extra_headers).map(|s| SignatureInfo { signature: s.to_string(), payload: decoded.message.to_string(), key_id: None, signer: None, }); self.cache_put_oid("sig_commit_meta", id, &v); Ok(v) } fn signature_tag_signature( &self, id: &ObjectId, ) -> Result, crate::Error> { if let Some(hit) = self.cache_get_oid::>("sig_tag_meta", id) { return Ok(hit); } let repo = self.repo()?; let tag = repo .find_tag(*id) .map_err(|e| crate::Error::Find(e.to_string()))?; let tag_ref = tag .decode() .map_err(|e| crate::Error::Find(e.to_string()))?; let v = tag_ref.pgp_signature.map(|s| SignatureInfo { signature: s.to_string(), payload: tag_ref.message.to_string(), key_id: None, signer: None, }); self.cache_put_oid("sig_tag_meta", id, &v); Ok(v) } }