Skip to main content

zoi_core/
pgp.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use std::time::SystemTime;
5
6use anyhow::{Result, anyhow};
7use chrono::{DateTime, Utc};
8use colored::Colorize;
9use sequoia_openpgp::Cert;
10use sequoia_openpgp::parse::Parse;
11use sequoia_openpgp::policy::StandardPolicy;
12use sequoia_openpgp::types::RevocationStatus;
13
14// Manages PGP keys and signature verification for the Zoi "Chain of Trust".
15//
16// Zoi uses PGP to verify:
17// - Registry Integrity: Every git commit in an official registry should be
18//   signed.
19// - Package Authenticity: Pre-built archives are verified against maintainer
20//   keys.
21//
22// This module handles local keyring management (`~/.zoi/pgps/`) and provides
23// utilities for importing, searching, and verifying signatures.
24
25include!(concat!(env!("OUT_DIR"), "/generated_pgp_keys.rs"));
26
27/// Synchronizes the local keyring with trusted keys embedded in the Zoi binary.
28///
29/// During the build process, Zoi bakes in "Root of Trust" keys for official
30/// registries. This function ensures these keys are present and up-to-date
31/// in the user's local keyring on every startup.
32///
33/// # Errors
34///
35/// Returns an error if the local keyring directory cannot be created or
36/// accessed.
37pub fn ensure_builtin_keys() -> Result<()> {
38    for (name, bytes) in BUILTIN_KEYS {
39        if let Err(e) = add_key_from_bytes(bytes, name, true) {
40            eprintln!(
41                "Warning: Failed to ensure builtin PGP key '{name}': {e}"
42            );
43        }
44    }
45    Ok(())
46}
47
48/// Returns a human-readable string representing the status of a PGP
49/// certificate.
50///
51/// The status can be "Valid", "Revoked", "Expired", or "Invalid" with
52/// additional details like expiration dates where applicable.
53pub fn get_cert_status(cert: &Cert) -> String {
54    let policy = StandardPolicy::new();
55    let now = SystemTime::now();
56    match cert.with_policy(&policy, now) {
57        Ok(vc) => {
58            if let RevocationStatus::Revoked(_) = vc.revocation_status() {
59                return "Revoked".red().bold().to_string();
60            }
61            if let Some(expiration) = vc.primary_key().key_expiration_time() {
62                let datetime: DateTime<Utc> = DateTime::<Utc>::from(expiration);
63                if expiration < now {
64                    return format!(
65                        "Expired ({})",
66                        datetime.format("%Y-%m-%d")
67                    )
68                    .red()
69                    .to_string();
70                }
71                return format!(
72                    "Valid (expires {})",
73                    datetime.format("%Y-%m-%d")
74                )
75                .green()
76                .to_string();
77            }
78            "Valid (no expiration)".green().to_string()
79        }
80        Err(e) => format!("Invalid: {e}").red().to_string()
81    }
82}
83
84/// Validates a PGP certificate, checking for revocation and expiration.
85///
86/// Returns `Ok(())` if the certificate is valid, or an error if it is revoked,
87/// expired, or otherwise invalid under the standard policy.
88///
89/// # Errors
90///
91/// Returns an error if the certificate is revoked, expired, or invalid.
92pub fn validate_cert(cert: &Cert) -> Result<()> {
93    let policy = StandardPolicy::new();
94    let now = SystemTime::now();
95    match cert.with_policy(&policy, now) {
96        Ok(vc) => {
97            if let RevocationStatus::Revoked(_) = vc.revocation_status() {
98                return Err(anyhow!("The PGP key is revoked."));
99            }
100            if let Some(expiration) = vc.primary_key().key_expiration_time()
101                && expiration < now
102            {
103                let datetime: DateTime<Utc> = DateTime::<Utc>::from(expiration);
104                return Err(anyhow!(
105                    "The PGP key expired on {}.",
106                    datetime.format("%Y-%m-%d")
107                ));
108            }
109            Ok(())
110        }
111        Err(e) => Err(anyhow!("The PGP key is invalid: {e}"))
112    }
113}
114
115/// Returns the path to the local PGP keyring directory.
116///
117/// This usually resides at `~/.zoi/pgps/`. The directory is created if it does
118/// not exist.
119///
120/// # Errors
121///
122/// Returns an error if the home directory cannot be found or the PGP directory
123/// cannot be created.
124pub fn get_pgp_dir() -> Result<PathBuf> {
125    let home_dir = crate::utils::get_user_home()
126        .ok_or_else(|| anyhow!("Could not find home directory."))?;
127    let pgp_dir = home_dir.join(".zoi").join("pgps");
128    fs::create_dir_all(&pgp_dir)?;
129    Ok(pgp_dir)
130}
131
132/// Adds a PGP key from a byte slice to the local keyring.
133///
134/// The key is saved as `<name>.asc` in the PGP directory. If a key with the
135/// same name exists but has different content, it will be overwritten (with a
136/// warning if `quiet` is false).
137///
138/// # Errors
139///
140/// Returns an error if the PGP directory cannot be accessed, or if the key is
141/// invalid.
142pub fn add_key_from_bytes(
143    key_bytes: &[u8],
144    name: &str,
145    quiet: bool
146) -> Result<()> {
147    let pgp_dir = get_pgp_dir()?;
148    let dest_path = pgp_dir.join(format!("{name}.asc"));
149
150    if dest_path.exists() {
151        let existing_bytes = fs::read(&dest_path)?;
152        if existing_bytes == key_bytes {
153            return Ok(());
154        }
155        if !quiet {
156            println!(
157                "{} A different key with the name '{name}' already exists. \
158                 Overwriting.",
159                "Warning:".yellow(),
160            );
161        }
162    }
163
164    let cert = Cert::from_bytes(key_bytes)?;
165    validate_cert(&cert)?;
166
167    fs::write(&dest_path, key_bytes)?;
168    if !quiet {
169        println!("Successfully added/updated key '{}'.", name.cyan());
170    }
171
172    Ok(())
173}
174
175/// Imports a PGP key from a file path into the local keyring.
176///
177/// If `name` is provided, it is used as the key's name in the store.
178/// Otherwise, the file stem of the path is used.
179///
180/// # Errors
181///
182/// Returns an error if the key file does not exist or cannot be read.
183pub fn add_key_from_path(
184    path: &str,
185    name: Option<&str>,
186    quiet: bool
187) -> Result<()> {
188    let key_path = Path::new(path);
189    if !key_path.exists() {
190        return Err(anyhow!("Key file not found at: {path}"));
191    }
192
193    let key_name = name.unwrap_or_else(|| {
194        key_path
195            .file_stem()
196            .and_then(|s| s.to_str())
197            .unwrap_or("unnamed")
198    });
199
200    if !quiet {
201        println!("Validating PGP key file...");
202    }
203    let key_bytes = fs::read(key_path)?;
204    if !quiet {
205        println!("{}", "Key is valid.".green());
206    }
207
208    add_key_from_bytes(&key_bytes, key_name, quiet)
209}
210
211/// Fetches a PGP key from a keyserver by fingerprint and adds it to the local
212/// keyring.
213///
214/// Currently uses `keys.openpgp.org` as the keyserver.
215///
216/// # Errors
217///
218/// Returns an error if the key cannot be fetched from the keyserver or is
219/// invalid.
220pub fn add_key_from_fingerprint(
221    fingerprint: &str,
222    name: &str,
223    quiet: bool
224) -> Result<()> {
225    let url = format!(
226        "https://keys.openpgp.org/vks/v1/by-fingerprint/{}",
227        fingerprint.to_uppercase()
228    );
229    if !quiet {
230        println!(
231            "Fetching key for fingerprint {} from keys.openpgp.org...",
232            fingerprint.cyan()
233        );
234    }
235
236    let client = crate::utils::get_http_client()?;
237    let response = client.get(&url).send()?;
238    if !response.status().is_success() {
239        return Err(anyhow!(
240            "Failed to fetch key from keyserver (HTTP {}).",
241            response.status()
242        ));
243    }
244
245    let key_bytes = response.bytes()?.to_vec();
246
247    if !quiet {
248        println!("Validating PGP key...");
249    }
250    Cert::from_bytes(&key_bytes)?;
251    if !quiet {
252        println!("{}", "Key is valid.".green());
253    }
254
255    add_key_from_bytes(&key_bytes, name, quiet)
256}
257
258/// Downloads a PGP key from a URL and adds it to the local keyring.
259///
260/// # Errors
261///
262/// Returns an error if the key cannot be fetched from the URL or is invalid.
263pub fn add_key_from_url(url: &str, name: &str, quiet: bool) -> Result<()> {
264    if !quiet {
265        println!(
266            "Fetching key for {} from url {}...",
267            name.cyan(),
268            url.cyan()
269        );
270    }
271
272    let client = crate::utils::get_http_client()?;
273    let response = client.get(url).send()?;
274    if !response.status().is_success() {
275        return Err(anyhow!(
276            "Failed to fetch key from url (HTTP {})",
277            response.status()
278        ));
279    }
280
281    let key_bytes = response.bytes()?.to_vec();
282
283    if !quiet {
284        println!("Validating PGP key...");
285    }
286    Cert::from_bytes(&key_bytes)?;
287    if !quiet {
288        println!("{}", "Key is valid.".green());
289    }
290
291    add_key_from_bytes(&key_bytes, name, quiet)
292}
293
294/// Removes a PGP key from the local keyring by its name.
295///
296/// # Errors
297///
298/// Returns an error if the key with the given name is not found or cannot be
299/// removed.
300pub fn remove_key_by_name(name: &str) -> Result<()> {
301    let pgp_dir = get_pgp_dir()?;
302    let key_path = pgp_dir.join(format!("{name}.asc"));
303
304    if !key_path.exists() {
305        return Err(anyhow!("Key with name '{name}' not found."));
306    }
307
308    fs::remove_file(&key_path)?;
309    println!("Successfully removed key '{}'.", name.cyan());
310
311    Ok(())
312}
313
314/// Searches for and removes a PGP key from the local keyring by its
315/// fingerprint.
316///
317/// # Errors
318///
319/// Returns an error if no key with the given fingerprint is found or cannot be
320/// removed.
321pub fn remove_key_by_fingerprint(fingerprint: &str) -> Result<()> {
322    let pgp_dir = get_pgp_dir()?;
323    let fingerprint_upper = fingerprint.to_uppercase();
324
325    for entry in fs::read_dir(pgp_dir)? {
326        let entry = entry?;
327        let path = entry.path();
328        if path.is_file()
329            && path.extension().and_then(|s| s.to_str()) == Some("asc")
330        {
331            let key_bytes = fs::read(&path)?;
332            if let Ok(cert) = Cert::from_bytes(&key_bytes)
333                && cert.fingerprint().to_string().to_uppercase()
334                    == fingerprint_upper
335            {
336                fs::remove_file(&path)?;
337                println!(
338                    "Successfully removed key with fingerprint {}.",
339                    fingerprint.cyan()
340                );
341                return Ok(());
342            }
343        }
344    }
345
346    Err(anyhow!("Key with fingerprint '{fingerprint}' not found."))
347}
348
349/// Prints a formatted list of all PGP keys stored in the local keyring.
350///
351/// # Errors
352///
353/// Returns an error if the local keyring cannot be read.
354pub fn list_keys() -> Result<()> {
355    let keys = get_all_local_keys_info()?;
356
357    if keys.is_empty() {
358        println!("No PGP keys found in the store.");
359        return Ok(());
360    }
361
362    println!("{} Stored PGP Keys", "::".bold().blue());
363
364    for key_info in keys {
365        println!();
366        println!("{}: {}", "Name".cyan(), key_info.name.bold());
367        println!("{}: {}", "  Status".cyan(), get_cert_status(&key_info.cert));
368        println!(
369            "  {}: {}",
370            "Fingerprint".cyan(),
371            key_info.cert.fingerprint()
372        );
373        for userid_amalgamation in key_info.cert.userids() {
374            let userid_packet = userid_amalgamation.userid();
375            let name = userid_packet
376                .name()
377                .ok()
378                .flatten()
379                .unwrap_or("[invalid name]");
380            let email =
381                userid_packet.email().ok().flatten().unwrap_or_default();
382
383            if email.is_empty() {
384                println!("  {}: {}", "UserID".cyan(), name);
385            } else {
386                println!("  {}: {} <{}>", "UserID".cyan(), name, email);
387            }
388        }
389    }
390
391    Ok(())
392}
393
394/// Searches for PGP keys in the local keyring by name, fingerprint, or `UserID`
395/// (name/email).
396///
397/// # Errors
398///
399/// Returns an error if the local keyring cannot be read.
400pub fn search_keys(term: &str) -> Result<()> {
401    let keys = get_all_local_keys_info()?;
402    let term_lower = term.to_lowercase();
403    let mut found_keys = Vec::new();
404
405    for key_info in keys {
406        let fingerprint =
407            key_info.cert.fingerprint().to_string().to_lowercase();
408        let name = key_info.name.to_lowercase();
409
410        let mut is_match =
411            name.contains(&term_lower) || fingerprint.contains(&term_lower);
412
413        if !is_match {
414            for userid_amalgamation in key_info.cert.userids() {
415                let userid_packet = userid_amalgamation.userid();
416                let uid_name = userid_packet
417                    .name()
418                    .ok()
419                    .flatten()
420                    .unwrap_or_default()
421                    .to_lowercase();
422                let uid_email = userid_packet
423                    .email()
424                    .ok()
425                    .flatten()
426                    .unwrap_or_default()
427                    .to_lowercase();
428
429                if uid_name.contains(&term_lower)
430                    || uid_email.contains(&term_lower)
431                {
432                    is_match = true;
433                    break;
434                }
435            }
436        }
437
438        if is_match {
439            found_keys.push(key_info);
440        }
441    }
442
443    if found_keys.is_empty() {
444        println!("\n{}", "No keys found matching your query.".yellow());
445        return Ok(());
446    }
447
448    println!(
449        "{} Found {} key(s) matching '{}'",
450        "::".bold().blue(),
451        found_keys.len(),
452        term.blue().bold()
453    );
454
455    for key_info in found_keys {
456        println!();
457        println!("{}: {}", "Name".cyan(), key_info.name.bold());
458        println!("{}: {}", "  Status".cyan(), get_cert_status(&key_info.cert));
459        println!(
460            "  {}: {}",
461            "Fingerprint".cyan(),
462            key_info.cert.fingerprint()
463        );
464        for userid_amalgamation in key_info.cert.userids() {
465            let userid_packet = userid_amalgamation.userid();
466            let name = userid_packet
467                .name()
468                .ok()
469                .flatten()
470                .unwrap_or("[invalid name]");
471            let email =
472                userid_packet.email().ok().flatten().unwrap_or_default();
473
474            if email.is_empty() {
475                println!("  {}: {}", "UserID".cyan(), name);
476            } else {
477                println!("  {}: {} <{}>", "UserID".cyan(), name, email);
478            }
479        }
480    }
481
482    Ok(())
483}
484
485/// Prints the ASCII-armored content of a PGP key from the local keyring.
486///
487/// # Errors
488///
489/// Returns an error if the key with the given name is not found or cannot be
490/// read.
491pub fn show_key(name: &str) -> Result<()> {
492    let pgp_dir = get_pgp_dir()?;
493    let key_path = pgp_dir.join(format!("{name}.asc"));
494
495    if !key_path.exists() {
496        return Err(anyhow!("Key with name '{name}' not found."));
497    }
498
499    let key_contents = fs::read_to_string(&key_path)?;
500    println!("{key_contents}");
501
502    Ok(())
503}
504
505/// A structure holding a PGP key's name and its parsed certificate.
506pub struct KeyInfo {
507    /// The name of the key (usually its filename without extension).
508    pub name: String,
509    /// The parsed PGP certificate.
510    pub cert: Cert
511}
512
513/// Retrieves information for all PGP keys stored in the local keyring.
514///
515/// # Errors
516///
517/// Returns an error if the local keyring cannot be read or contains invalid
518/// keys.
519pub fn get_all_local_keys_info() -> Result<Vec<KeyInfo>> {
520    let pgp_dir = get_pgp_dir()?;
521    let mut keys = Vec::new();
522    if !pgp_dir.exists() {
523        return Ok(keys);
524    }
525    for entry in fs::read_dir(pgp_dir)? {
526        let entry = entry?;
527        let path = entry.path();
528        if path.is_file()
529            && path.extension().and_then(|s| s.to_str()) == Some("asc")
530            && let Ok(bytes) = fs::read(&path)
531            && let Ok(cert) = Cert::from_bytes(&bytes)
532        {
533            let name = path
534                .file_stem()
535                .ok_or_else(|| {
536                    anyhow!("Path should have a file stem: {}", path.display())
537                })?
538                .to_string_lossy()
539                .to_string();
540            keys.push(KeyInfo { name, cert });
541        }
542    }
543    keys.sort_by(|a, b| a.name.cmp(&b.name));
544    Ok(keys)
545}
546
547/// Retrieves all PGP certificates stored in the local keyring.
548///
549/// # Errors
550///
551/// Returns an error if the local keyring cannot be read or contains invalid
552/// keys.
553pub fn get_all_local_certs() -> Result<Vec<Cert>> {
554    let pgp_dir = get_pgp_dir()?;
555    let mut certs = Vec::new();
556    if !pgp_dir.exists() {
557        return Ok(certs);
558    }
559    for entry in fs::read_dir(pgp_dir)? {
560        let entry = entry?;
561        let path = entry.path();
562        if path.is_file()
563            && path.extension().and_then(|s| s.to_str()) == Some("asc")
564            && let Ok(bytes) = fs::read(&path)
565            && let Ok(cert) = Cert::from_bytes(&bytes)
566        {
567            certs.push(cert);
568        }
569    }
570    Ok(certs)
571}
572
573use sequoia_openpgp::KeyHandle;
574use sequoia_openpgp::parse::stream::{
575    DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper
576};
577
578/// A Sequoia verification helper that allows verification against multiple
579/// trusted certificates.
580struct MultiCertHelper {
581    /// The list of trusted certificates to use for verification.
582    certs: Vec<Cert>
583}
584
585impl VerificationHelper for MultiCertHelper {
586    fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
587        Ok(self.certs.clone())
588    }
589
590    fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
591        if let Some(layer) = structure.into_iter().next() {
592            match layer {
593                MessageLayer::SignatureGroup { results } => {
594                    if results.iter().any(Result::is_ok) {
595                        return Ok(());
596                    }
597                    return Err(anyhow!(
598                        "No valid signature found from any trusted key."
599                    ));
600                }
601                _ => {
602                    return Err(anyhow!(
603                        "Unexpected message structure: not a signature group."
604                    ));
605                }
606            }
607        }
608        Err(anyhow!(
609            "No signature layer found in the message structure."
610        ))
611    }
612}
613
614/// A Sequoia verification helper that allows verification against a single
615/// trusted certificate.
616struct OneCertHelper {
617    /// The trusted certificate to use for verification.
618    cert: Cert
619}
620
621impl VerificationHelper for OneCertHelper {
622    fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
623        Ok(vec![self.cert.clone()])
624    }
625
626    fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
627        if let Some(layer) = structure.into_iter().next() {
628            match layer {
629                MessageLayer::SignatureGroup { results } => {
630                    if results.iter().any(Result::is_ok) {
631                        return Ok(());
632                    }
633                    return Err(anyhow!("No valid signature found"));
634                }
635                _ => return Err(anyhow!("Unexpected message structure"))
636            }
637        }
638        Err(anyhow!("No signature layer found"))
639    }
640}
641
642/// A CLI-friendly wrapper for verifying a file's signature using a named key
643/// from the local keyring.
644///
645/// # Errors
646///
647/// Returns an error if the key is not found or the signature is invalid.
648pub fn cli_verify_signature(
649    file_path: &str,
650    sig_path: &str,
651    key_name: &str
652) -> Result<()> {
653    println!(
654        "Verifying {file_path} with signature {sig_path} using key \
655         '{key_name}'"
656    );
657
658    let pgp_dir = get_pgp_dir()?;
659    let key_path = pgp_dir.join(format!("{key_name}.asc"));
660    if !key_path.exists() {
661        return Err(anyhow!("Key '{key_name}' not found in local store."));
662    }
663    let key_bytes = fs::read(key_path)?;
664    let cert = Cert::from_bytes(&key_bytes)?;
665
666    verify_detached_signature(
667        Path::new(file_path),
668        Path::new(sig_path),
669        &cert
670    )?;
671
672    println!("{}", "Signature is valid.".green());
673    Ok(())
674}
675
676/// Verifies a detached PGP signature for a file using a specific certificate.
677///
678/// # Errors
679///
680/// Returns an error if the file cannot be read or the signature is invalid.
681pub fn verify_detached_signature(
682    data_path: &Path,
683    signature_path: &Path,
684    cert: &Cert
685) -> Result<()> {
686    let data = fs::read(data_path)?;
687    let signature = fs::read(signature_path)?;
688    verify_detached_signature_raw(&data, &signature, cert)
689}
690
691/// Verifies a detached PGP signature for raw data using a specific certificate.
692///
693/// # Errors
694///
695/// Returns an error if the signature is invalid.
696pub fn verify_detached_signature_raw(
697    data: &[u8],
698    signature: &[u8],
699    cert: &Cert
700) -> Result<()> {
701    let policy = &StandardPolicy::new();
702    let helper = OneCertHelper { cert: cert.clone() };
703
704    let mut verifier = DetachedVerifierBuilder::from_bytes(signature)?
705        .with_policy(policy, None, helper)?;
706
707    verifier.verify_bytes(data)?;
708
709    Ok(())
710}
711
712/// Signs a file using `GnuPG`.
713///
714/// This function calls the external `gpg` command to create a detached
715/// signature. It can take an optional `GPG_PASSWORD` environment variable for
716/// password-protected keys.
717///
718/// # Errors
719///
720/// Returns an error if `gpg` is not installed, the file cannot be read, or
721/// signing fails.
722pub fn sign_detached(
723    data_path: &Path,
724    signature_path: &Path,
725    key_id: &str
726) -> Result<()> {
727    if !crate::utils::command_exists("gpg") {
728        return Err(anyhow!(
729            "gpg command not found. Please install GnuPG and ensure it's in \
730             your PATH."
731        ));
732    }
733
734    let data_path_str = data_path
735        .to_str()
736        .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
737    let signature_path_str = signature_path
738        .to_str()
739        .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
740
741    let mut command = Command::new("gpg");
742    command
743        .arg("--batch")
744        .arg("--no-tty")
745        .arg("--yes")
746        .arg("--detach-sign");
747
748    if let Ok(password) = std::env::var("GPG_PASSWORD") {
749        command
750            .arg("--pinentry-mode")
751            .arg("loopback")
752            .arg("--passphrase")
753            .arg(password);
754    }
755
756    command
757        .arg("--local-user")
758        .arg(key_id)
759        .arg("--output")
760        .arg(signature_path_str)
761        .arg(data_path_str);
762
763    let output = command.output()?;
764
765    if !output.status.success() {
766        use std::fmt::Write;
767        let stderr = String::from_utf8_lossy(&output.stderr);
768        let mut error_message =
769            format!("gpg signing failed with status: {}.\n", output.status);
770        if stderr.contains("No secret key") {
771            let _ = writeln!(
772                error_message,
773                "The secret key for '{key_id}' was not found in your GPG \
774                 keychain."
775            );
776            error_message.push_str(
777                "Please ensure the key is imported into GPG and is trusted."
778            );
779        } else if stderr.contains("bad passphrase")
780            || stderr.contains("Passphrase check failed")
781        {
782            error_message.push_str(
783                "Incorrect passphrase provided, or the agent could not get \
784                 the passphrase.\n"
785            );
786            error_message.push_str(
787                "Ensure your GPG agent is running and configured correctly if \
788                 the key is password-protected."
789            );
790        } else {
791            let _ = write!(error_message, "Stderr: {stderr}");
792        }
793
794        return Err(anyhow!(error_message));
795    }
796
797    Ok(())
798}
799
800/// Resolves a list of names or fingerprints to PGP certificates from the local
801/// keyring.
802///
803/// # Errors
804///
805/// Returns an error if any of the trusted keys are not found in the local
806/// keyring.
807pub fn get_certs_by_name_or_fingerprint(
808    identifiers: &[String]
809) -> Result<Vec<Cert>> {
810    let all_keys = get_all_local_keys_info()?;
811    let mut found_certs = Vec::new();
812
813    for identifier in identifiers {
814        let identifier_lower = identifier.to_lowercase();
815        let mut found = false;
816        for key_info in &all_keys {
817            let fingerprint_lower =
818                key_info.cert.fingerprint().to_string().to_lowercase();
819            if key_info.name == *identifier
820                || fingerprint_lower.starts_with(&identifier_lower)
821            {
822                found_certs.push(key_info.cert.clone());
823                found = true;
824                break;
825            }
826        }
827        if !found {
828            return Err(anyhow!(
829                "Trusted key '{identifier}' not found in Zoi's PGP keyring."
830            ));
831        }
832    }
833    Ok(found_certs)
834}
835
836/// Verifies a detached PGP signature for a file against a set of trusted
837/// certificates.
838///
839/// Returns `Ok(())` if at least one trusted certificate successfully verifies
840/// the signature.
841///
842/// # Errors
843///
844/// Returns an error if the file cannot be read or no valid signature is found.
845pub fn verify_detached_signature_multi_key(
846    data_path: &Path,
847    signature_path: &Path,
848    trusted_certs: Vec<Cert>
849) -> Result<()> {
850    let policy = &StandardPolicy::new();
851    let data = fs::read(data_path)?;
852    let signature = fs::read(signature_path)?;
853
854    let helper = MultiCertHelper {
855        certs: trusted_certs
856    };
857
858    let mut verifier = DetachedVerifierBuilder::from_bytes(&signature)?
859        .with_policy(policy, None, helper)?;
860
861    verifier.verify_bytes(&data)?;
862
863    Ok(())
864}