Skip to main content

zoi_cli/cmd/
pgp.rs

1//! PGP key management commands for the Zoi CLI.
2//!
3//! These commands allow users to manage PGP keys used for verifying package
4//! signatures, ensuring the authenticity and integrity of installed software.
5
6use std::path::Path;
7
8use anyhow::{Result, anyhow};
9use clap::{ArgGroup, Parser, Subcommand};
10
11use crate::pkg;
12
13/// The root PGP management command.
14#[derive(Parser, Debug)]
15#[command(long_about = "Manages PGP keys for package signature verification.")]
16pub struct PgpCommand {
17    /// The specific PGP subcommand to execute.
18    #[command(subcommand)]
19    pub command: PgpCommands
20}
21
22/// Available PGP subcommands.
23#[derive(Subcommand, Debug)]
24pub enum PgpCommands {
25    /// Add a PGP key from a file, URL, or a keyserver
26    Add(AddKey),
27    /// Remove a PGP key
28    #[command(alias = "rm")]
29    Remove(RemoveKey),
30    /// List all imported PGP keys
31    #[command(alias = "ls")]
32    List,
33    /// Search for a PGP key by user ID or fingerprint
34    Search(SearchKey),
35    /// Show the public key of a stored PGP key
36    Show(ShowKey),
37    /// Verify a file's detached signature
38    Verify(VerifySig)
39}
40
41/// Arguments for the add-key command.
42#[derive(Parser, Debug)]
43#[command(group(
44    ArgGroup::new("source")
45        .required(true)
46        .args(["path", "fingerprint", "url"]),
47))]
48pub struct AddKey {
49    /// Path to the PGP key file (.asc)
50    #[arg(long)]
51    pub path: Option<String>,
52
53    /// Fingerprint of the PGP key to fetch from keys.openpgp.org
54    #[arg(long)]
55    pub fingerprint: Option<String>,
56
57    /// URL of the PGP key to import
58    #[arg(long)]
59    pub url: Option<String>,
60
61    /// Name to associate with the key (defaults to filename if adding from
62    /// path/url)
63    #[arg(long)]
64    pub name: Option<String>
65}
66
67/// Arguments for the remove-key command.
68#[derive(Parser, Debug)]
69#[command(group(
70    ArgGroup::new("key_id")
71        .required(true)
72        .args(["name", "fingerprint"]),
73))]
74pub struct RemoveKey {
75    /// Name of the key to remove
76    pub name: Option<String>,
77
78    /// Fingerprint of the key to remove
79    #[arg(long)]
80    pub fingerprint: Option<String>
81}
82
83/// Arguments for the search-key command.
84#[derive(Parser, Debug)]
85pub struct SearchKey {
86    /// The user ID (name, email) or fingerprint to search for
87    #[arg(required = true)]
88    pub term: String
89}
90
91/// Arguments for the show-key command.
92#[derive(Parser, Debug)]
93pub struct ShowKey {
94    /// The name of the key to show
95    #[arg(required = true)]
96    pub name: String
97}
98
99/// Arguments for the verify-signature command.
100#[derive(Parser, Debug)]
101pub struct VerifySig {
102    /// Path to the file to verify
103    #[arg(long)]
104    pub file: String,
105
106    /// Path to the detached signature file
107    #[arg(long)]
108    pub sig: String,
109
110    /// Name of the key in the local store to use for verification
111    #[arg(long)]
112    pub key: String
113}
114
115/// Run the PGP management command.
116///
117/// # Errors
118///
119/// Returns an error if the PGP operation (key generation, signing, etc.) fails.
120pub fn run(args: PgpCommand) -> Result<()> {
121    match args.command {
122        PgpCommands::Add(add_args) => {
123            if let Some(path) = add_args.path {
124                pkg::pgp::add_key_from_path(
125                    &path,
126                    add_args.name.as_deref(),
127                    false
128                )?;
129            } else if let Some(fingerprint) = add_args.fingerprint {
130                if let Some(name) = add_args.name {
131                    pkg::pgp::add_key_from_fingerprint(
132                        &fingerprint,
133                        &name,
134                        false
135                    )?;
136                } else {
137                    return Err(anyhow!(
138                        "A name must be provided when adding a key by \
139                         fingerprint."
140                    ));
141                }
142            } else if let Some(url) = add_args.url {
143                let name = if let Some(n) = add_args.name {
144                    n
145                } else {
146                    Path::new(&url)
147                        .file_stem()
148                        .and_then(|s| s.to_str())
149                        .ok_or(anyhow!("Could not derive name from URL"))?
150                        .to_string()
151                };
152                pkg::pgp::add_key_from_url(&url, &name, false)?;
153            }
154        }
155        PgpCommands::Remove(remove_args) => {
156            if let Some(name) = remove_args.name {
157                pkg::pgp::remove_key_by_name(&name)?;
158            } else if let Some(fingerprint) = remove_args.fingerprint {
159                pkg::pgp::remove_key_by_fingerprint(&fingerprint)?;
160            }
161        }
162        PgpCommands::List => {
163            pkg::pgp::list_keys()?;
164        }
165        PgpCommands::Search(search_args) => {
166            pkg::pgp::search_keys(&search_args.term)?;
167        }
168        PgpCommands::Show(show_args) => {
169            pkg::pgp::show_key(&show_args.name)?;
170        }
171        PgpCommands::Verify(verify_args) => {
172            pkg::pgp::cli_verify_signature(
173                &verify_args.file,
174                &verify_args.sig,
175                &verify_args.key
176            )?;
177        }
178    }
179    Ok(())
180}