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