use sequoia_openpgp as openpgp;
use std::io::Read;
#[derive(Debug)]
pub enum PgpError {
SignatureParseError(String),
VerificationError(String),
IoError(std::io::Error),
SequoiaError(String),
}
impl std::fmt::Display for PgpError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PgpError::SignatureParseError(s) => write!(f, "signature parse error: {}", s),
PgpError::VerificationError(s) => write!(f, "verification error: {}", s),
PgpError::IoError(e) => write!(f, "IO error: {}", e),
PgpError::SequoiaError(e) => write!(f, "sequoia error: {}", e),
}
}
}
impl std::error::Error for PgpError {}
impl From<std::io::Error> for PgpError {
fn from(e: std::io::Error) -> Self {
PgpError::IoError(e)
}
}
impl From<openpgp::Error> for PgpError {
fn from(e: openpgp::Error) -> Self {
PgpError::SequoiaError(e.to_string())
}
}
impl From<anyhow::Error> for PgpError {
fn from(e: anyhow::Error) -> Self {
PgpError::SequoiaError(e.to_string())
}
}
pub const SIGNATURE_EXTENSIONS: &[&str] = &[".asc", ".sig", ".sign", ".gpg"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureVerification {
pub valid: bool,
pub fingerprint: Option<String>,
pub error: Option<String>,
}
impl SignatureVerification {
pub fn valid(fingerprint: String) -> Self {
Self {
valid: true,
fingerprint: Some(fingerprint),
error: None,
}
}
pub fn invalid(error: String) -> Self {
Self {
valid: false,
fingerprint: None,
error: Some(error),
}
}
}
pub fn probe_signature_urls(url: &str) -> Vec<String> {
SIGNATURE_EXTENSIONS
.iter()
.map(|ext| format!("{}{}", url, ext))
.collect()
}
pub fn verify_detached<S, D, C>(signature: S, data: D, cert: C) -> Result<String, PgpError>
where
S: Read + Send + Sync,
D: Read + Send + Sync,
C: Read + Send + Sync,
{
use openpgp::parse::stream::*;
use openpgp::parse::Parse;
use openpgp::policy::StandardPolicy;
let p = &StandardPolicy::new();
let cert = openpgp::Cert::from_reader(cert)
.map_err(|e| PgpError::SignatureParseError(e.to_string()))?;
struct Helper<'a> {
cert: &'a openpgp::Cert,
fingerprint: Option<String>,
}
impl<'a> VerificationHelper for Helper<'a> {
fn get_certs(
&mut self,
_ids: &[openpgp::KeyHandle],
) -> openpgp::Result<Vec<openpgp::Cert>> {
Ok(vec![self.cert.clone()])
}
fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
let mut valid_signature = false;
for layer in structure.iter() {
match layer {
MessageLayer::SignatureGroup { results } => {
for result in results {
match result {
Ok(GoodChecksum { ka, .. }) => {
valid_signature = true;
self.fingerprint = Some(ka.key().fingerprint().to_hex());
}
Err(e) => {
eprintln!("Signature verification failed: {}", e);
}
}
}
}
MessageLayer::Compression { .. } => {}
MessageLayer::Encryption { .. } => {}
}
}
if valid_signature {
Ok(())
} else {
Err(anyhow::anyhow!("No valid signature found"))
}
}
}
let helper = Helper {
cert: &cert,
fingerprint: None,
};
let mut verifier =
DetachedVerifierBuilder::from_reader(signature)?.with_policy(p, None, helper)?;
verifier.verify_reader(data)?;
let fingerprint = verifier
.into_helper()
.fingerprint
.ok_or_else(|| PgpError::VerificationError("No fingerprint found".to_string()))?;
Ok(fingerprint)
}
pub fn verify_detached_bytes(
signature: &[u8],
data: &[u8],
cert: &[u8],
) -> Result<String, PgpError> {
verify_detached(
std::io::Cursor::new(signature),
std::io::Cursor::new(data),
std::io::Cursor::new(cert),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_probe_signature_urls() {
let url = "https://example.com/project-1.0.tar.gz";
let sig_urls = probe_signature_urls(url);
assert_eq!(
sig_urls,
vec![
"https://example.com/project-1.0.tar.gz.asc",
"https://example.com/project-1.0.tar.gz.sig",
"https://example.com/project-1.0.tar.gz.sign",
"https://example.com/project-1.0.tar.gz.gpg",
]
);
}
#[test]
fn test_probe_signature_urls_tar_xz() {
let url = "https://example.com/release.tar.xz";
let sig_urls = probe_signature_urls(url);
assert_eq!(
sig_urls,
vec![
"https://example.com/release.tar.xz.asc",
"https://example.com/release.tar.xz.sig",
"https://example.com/release.tar.xz.sign",
"https://example.com/release.tar.xz.gpg",
]
);
}
#[test]
fn test_signature_extensions_constant() {
assert_eq!(SIGNATURE_EXTENSIONS, &[".asc", ".sig", ".sign", ".gpg"]);
}
}