Skip to main content

arti/subcommands/
keys.rs

1//! The `keys` subcommand.
2// TODO: The output of these subcommands needs improvement. Also, some of the `display_` functions
3// are repetitive and redundant.
4
5use std::ops::Deref;
6use std::str::FromStr;
7
8use anyhow::Result;
9
10use arti_client::{InertTorClient, TorClient, TorClientConfig};
11use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand};
12use tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreEntryResult, KeystoreId, UnrecognizedEntryError};
13use tor_rtcompat::Runtime;
14
15use crate::{ArtiConfig, subcommands::prompt};
16
17#[cfg(feature = "onion-service-service")]
18use tor_hsservice::OnionService;
19
20/// Length of a line, used for formatting
21// TODO: use COLUMNS instead of an arbitrary LINE_LEN
22const LINE_LEN: usize = 80;
23
24/// The `keys` subcommands the arti CLI will be augmented with.
25#[derive(Debug, Parser)]
26pub(crate) enum KeysSubcommands {
27    /// Run keystore management commands.
28    #[command(subcommand)]
29    Keys(KeysSubcommand),
30}
31
32/// The `keys` subcommand.
33#[derive(Subcommand, Debug, Clone)]
34pub(crate) enum KeysSubcommand {
35    /// List keys and certificates.
36    ///
37    /// Note: The output fields "Location" and "Keystore ID" represent,
38    /// respectively, the raw identifier of an entry (e.g. <ARTI_PATH>.<ENTRY_TYPE>
39    /// for `ArtiNativeKeystore`), and the identifier of the keystore that
40    /// contains the entry.
41    List(ListArgs),
42
43    /// List keystores.
44    ListKeystores,
45
46    /// Validate the integrity of keystores.
47    ///
48    /// Detects and reports unrecognized entries and paths, as well as
49    /// malformed or expired keys.
50    ///
51    /// Such entries will be removed if this command is invoked with `--sweep`.
52    CheckIntegrity(CheckIntegrityArgs),
53}
54
55/// The arguments of the [`List`](KeysSubcommand::List) subcommand.
56#[derive(Debug, Clone, Args)]
57pub(crate) struct ListArgs {
58    /// Identifier of the keystore.
59    ///
60    /// If omitted, keys and certificates
61    /// from all the keystores will be returned.
62    #[arg(short, long)]
63    keystore_id: Option<String>,
64}
65
66/// The arguments of the [`CheckIntegrity`](KeysSubcommand::CheckIntegrity) subcommand.
67#[derive(Debug, Clone, Args)]
68pub(crate) struct CheckIntegrityArgs {
69    /// Identifier of the keystore.
70    ///
71    /// If omitted, keys and certificates
72    /// from all the keystores will be checked.
73    #[arg(short, long)]
74    keystore_id: Option<KeystoreId>,
75
76    /// Remove the detected invalid keystore entries.
77    #[arg(long, short, default_value_t = false)]
78    sweep: bool,
79
80    /// With this flag active no prompt will be shown
81    /// and no confirmation will be asked.
82    // TODO: Rephrase this and the `batch` flags of the
83    // other commands in the present tense.
84    #[arg(long, short, default_value_t = false)]
85    batch: bool,
86}
87
88/// A set of invalid keystore entries associated with a keystore ID.
89/// This struct is used solely to reduce type complexity; it does not
90/// perform any validation (e.g., whether the entries actually belong
91/// to the keystore indicated by the ID).
92#[derive(Clone)]
93struct InvalidKeystoreEntries<'a> {
94    /// The `KeystoreId` that the entries are expected to belong to.
95    keystore_id: KeystoreId,
96    /// The list of invalid entries that logically belong to the keystore identified
97    /// by `keystore_id`.
98    entries: Vec<InvalidKeystoreEntry<'a>>,
99}
100
101/// An invalid keystore entry associated with the error that caused it to be
102/// invalid. This struct is used solely to reduce type complexity; it does not
103/// perform any validation (e.g., whether the `error_msg` actually corresponds
104/// to the error that caused the invalid entry).
105#[derive(Clone)]
106struct InvalidKeystoreEntry<'a> {
107    /// The entry
108    entry: KeystoreEntryResult<KeystoreEntry<'a>>,
109    /// The error message derived from the error that caused the entry to be invalid.
110    /// This field is needed (even if `Err(UnrecognizedEntryError)` contains the error) because `Ok(KeystoreEntry)`s could be invalid too.
111    error_msg: String,
112}
113
114/// Run the `keys` subcommand.
115pub(crate) fn run<R: Runtime>(
116    runtime: R,
117    keys_matches: &ArgMatches,
118    config: &ArtiConfig,
119    client_config: &TorClientConfig,
120) -> Result<()> {
121    let subcommand =
122        KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
123    let rt = runtime.clone();
124    let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
125
126    match subcommand {
127        KeysSubcommand::List(args) => run_list_keys(&args, &client_builder.create_inert()?),
128        KeysSubcommand::ListKeystores => run_list_keystores(&client_builder.create_inert()?),
129        KeysSubcommand::CheckIntegrity(args) => run_check_integrity(
130            &args,
131            &rt.reenter_block_on(client_builder.create_bootstrapped())?,
132            config,
133            client_config,
134        ),
135    }
136}
137
138/// Print information about a keystore entry.
139fn display_entry(entry: &KeystoreEntry, keymgr: &KeyMgr) {
140    let raw_entry = entry.raw_entry();
141    match keymgr.describe(entry.key_path()) {
142        Some(e) => {
143            println!(" Keystore ID: {}", entry.keystore_id());
144            println!(" Role: {}", e.role());
145            println!(" Summary: {}", e.summary());
146            println!(" KeystoreItemType: {:?}", entry.key_type());
147            println!(" Location: {}", raw_entry.raw_id());
148            let extra_info = e.extra_info();
149            println!(" Extra info:");
150            for (key, value) in extra_info {
151                println!(" - {key}: {value}");
152            }
153        }
154        None => {
155            println!(" Unrecognized path {}", raw_entry.raw_id());
156        }
157    }
158    println!("\n {}", "-".repeat(LINE_LEN));
159}
160
161/// Print information about an unrecognized keystore entry.
162fn display_unrecognized_entry(entry: &UnrecognizedEntryError) {
163    let raw_entry = entry.entry();
164    println!(" Unrecognized entry");
165    #[allow(clippy::single_match)]
166    match raw_entry.raw_id() {
167        tor_keymgr::RawEntryId::Path(p) => {
168            println!(" Keystore ID: {}", raw_entry.keystore_id());
169            println!(" Location: {}", p.to_string_lossy());
170            println!(" Error: {}", entry.error());
171        }
172        // NOTE: For the time being Arti only supports
173        // on-disk keystores, but more supported medium
174        // will be added.
175        other => {
176            panic!("Unhandled enum variant: {:?}", other);
177        }
178    }
179    println!("\n {}\n", "-".repeat(LINE_LEN));
180}
181
182/// Run the `keys list` subcommand.
183fn run_list_keys(args: &ListArgs, client: &InertTorClient) -> Result<()> {
184    let keymgr = client.keymgr()?;
185    // TODO: in the future we could group entries by their type
186    // (recognized, unrecognized and unrecognized path).
187    // That way we don't need to print "Unrecognized path",
188    // "Unrecognized" entry etc. for each unrecognized entry.
189    match &args.keystore_id {
190        Some(s) => {
191            let id = KeystoreId::from_str(s)?;
192            let empty_err_msg = format!("Currently there are no entries in the keystore {}.", s);
193            display_keystore_entries(
194                &keymgr.list_by_id(&id)?,
195                keymgr,
196                "Keystore entries",
197                &empty_err_msg,
198            );
199        }
200        None => {
201            display_keystore_entries(
202                &keymgr.list()?,
203                keymgr,
204                "Keystore entries",
205                "Currently there are no entries in any of the keystores.",
206            );
207        }
208    }
209    Ok(())
210}
211
212/// Run `keys list-keystores` subcommand.
213fn run_list_keystores(client: &InertTorClient) -> Result<()> {
214    let keymgr = client.keymgr()?;
215    let entries = keymgr.list_keystores();
216
217    if entries.is_empty() {
218        println!("Currently there are no keystores available.");
219    } else {
220        println!(" Keystores:\n");
221        for entry in entries {
222            // TODO: We need something similar to [`KeyPathInfo`](tor_keymgr::KeyPathInfo)
223            // for `KeystoreId`
224            println!(" - {:?}\n", entry.as_ref());
225        }
226    }
227
228    Ok(())
229}
230
231/// Run `keys check-integrity` subcommand.
232fn run_check_integrity<R: Runtime>(
233    args: &CheckIntegrityArgs,
234    client: &TorClient<R>,
235    config: &ArtiConfig,
236    client_config: &TorClientConfig,
237) -> Result<()> {
238    let keymgr = client.keymgr()?;
239
240    let keystore_ids = match &args.keystore_id {
241        Some(id) => vec![id.to_owned()],
242        None => keymgr.list_keystores(),
243    };
244    let keystores: Vec<(_, Vec<KeystoreEntryResult<KeystoreEntry>>)> = keystore_ids
245        .into_iter()
246        .map(|id| keymgr.list_by_id(&id).map(|entries| (id, entries)))
247        .collect::<Result<Vec<_>, _>>()?;
248
249    // Unlike `keystores`, which has type `Vec<(KeystoreId, Vec<KeystoreEntryResult<KeystoreEntry>>)>`,
250    // `affected_keystores` has type `InvalidKeystoreEntries`. This distinction is
251    // necessary because the entries in `keystores` will be evaluated, and if any are
252    // found to be invalid, the associated error messages must be stored somewhere
253    // for later display.
254    let mut affected_keystores = Vec::new();
255    cfg_if::cfg_if! {
256        if #[cfg(feature = "onion-service-service")] {
257            // `service` cannot be dropped as long as `expired_entries` is in use, since
258            // `expired_entries` holds references to `services`.
259            let services = create_all_services(config, client_config)?;
260            let mut expired_entries: Vec<_> = get_expired_keys(&services, client)?;
261        }
262    }
263
264    for (id, entries) in keystores {
265        let mut invalid_entries = entries
266            .into_iter()
267            .filter_map(|entry| match entry {
268                Ok(e) => keymgr
269                    .validate_entry_integrity(&e)
270                    .map_err(|err| InvalidKeystoreEntry {
271                        entry: Ok(e),
272                        error_msg: err.to_string(),
273                    })
274                    .err(),
275                Err(err) => {
276                    let error = err.error().to_string();
277                    Some(InvalidKeystoreEntry {
278                        entry: Err(err),
279                        error_msg: error,
280                    })
281                }
282            })
283            .collect::<Vec<_>>();
284
285        cfg_if::cfg_if! {
286            if #[cfg(feature = "onion-service-service")] {
287                // For the current keystore, transfer its expired keys from `expired_entries`
288                // to `invalid_entries`.
289                expired_entries.retain(|expired_entry| {
290                    match &expired_entry.entry {
291                        Ok(entry) => {
292                            if entry.keystore_id() == &id {
293                                invalid_entries.push(expired_entry.clone());
294                                return false;
295                            }
296                        }
297                        Err(err) => {
298                            eprintln!("WARNING: Unexpected invalid keystore entry encountered: {}", err);
299                        }
300                    }
301                    true
302                })
303            }
304        }
305
306        if invalid_entries.is_empty() {
307            println!("{}: OK.\n", id);
308            continue;
309        }
310
311        affected_keystores.push(InvalidKeystoreEntries {
312            keystore_id: id,
313            entries: invalid_entries,
314        });
315    }
316
317    // Expired entries are obtained from the registered keystore. Since we have iterated over every
318    // registered keystore and removed all entries associated with the current keystore, the
319    // collection `expired_entries` should be empty. If it is not, there is a bug (see
320    // [`OnionService::list_expired_keys`]).
321    cfg_if::cfg_if! {
322        if #[cfg(feature = "onion-service-service")] {
323            if !expired_entries.is_empty() {
324                return Err(anyhow::anyhow!(
325                    "Encountered an expired key that doesn't belong to a registered keystore."
326                ));
327            }
328        }
329    }
330
331    display_invalid_keystore_entries(&affected_keystores);
332
333    maybe_remove_invalid_entries(args, &affected_keystores, keymgr)?;
334
335    Ok(())
336}
337
338/// Helper function for `run_check_integrity` that reduces cognitive complexity.
339///
340/// Displays invalid keystore entries grouped by `KeystoreId`, showing the `raw_id`
341/// of each key and the associated error message in a unified report to the user.
342/// If no invalid entries are provided, nothing is printed.
343fn display_invalid_keystore_entries(affected_keystores: &[InvalidKeystoreEntries]) {
344    if affected_keystores.is_empty() {
345        return;
346    }
347
348    print_check_integrity_incipit(affected_keystores);
349
350    for InvalidKeystoreEntries {
351        keystore_id,
352        entries,
353    } in affected_keystores
354    {
355        println!("\nInvalid keystore entries in keystore {}:\n", keystore_id);
356        for InvalidKeystoreEntry { entry, error_msg } in entries {
357            let raw_entry = match entry {
358                Ok(e) => e.raw_entry(),
359                Err(e) => e.entry().into(),
360            };
361            println!("{}", raw_entry.raw_id());
362            println!("\tError: {}", error_msg);
363        }
364    }
365}
366
367/// Helper function of `run_list_keys`, reduces cognitive complexity.
368fn display_keystore_entries(
369    entries: &[KeystoreEntryResult<KeystoreEntry>],
370    keymgr: &KeyMgr,
371    header: &str,
372    empty_err_msg: &str,
373) {
374    if entries.is_empty() {
375        println!("{empty_err_msg}");
376        return;
377    }
378    println!(" ===== {} =====\n\n", header);
379    for entry in entries {
380        match entry {
381            Ok(entry) => {
382                display_entry(entry, keymgr);
383            }
384            Err(entry) => {
385                display_unrecognized_entry(entry);
386            }
387        }
388    }
389}
390
391/// Helper function for `run_check_integrity`.
392///
393/// Creates an [`OnionService`] for each configured hidden service.
394#[cfg(feature = "onion-service-service")]
395fn create_all_services(
396    config: &ArtiConfig,
397    client_config: &TorClientConfig,
398) -> Result<Vec<OnionService>> {
399    let mut services = Vec::new();
400    for (_, cfg) in config.onion_services.iter() {
401        services.push(
402            TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
403                client_config,
404                cfg.svc_cfg.clone(),
405            )?,
406        );
407    }
408    Ok(services)
409}
410
411/// Helper function for `run_check_integrity`.
412///
413/// Gathers all expired keys from the provided hidden services.
414#[cfg(feature = "onion-service-service")]
415fn get_expired_keys<'a, R: Runtime>(
416    services: &'a Vec<OnionService>,
417    client: &TorClient<R>,
418) -> Result<Vec<InvalidKeystoreEntry<'a>>> {
419    let netdir = client.dirmgr().timely_netdir()?;
420
421    let mut expired_keys = Vec::new();
422    for service in services {
423        expired_keys.append(
424            &mut service
425                .list_expired_keys(&netdir)?
426                .into_iter()
427                .map(|entry| InvalidKeystoreEntry {
428                    entry: Ok(entry),
429                    error_msg: "The entry is expired.".to_string(),
430                })
431                .collect(),
432        );
433    }
434    Ok(expired_keys)
435}
436
437/// Helper function for `run_check_integrity`.
438///
439/// Removes invalid keystore entries.
440/// Prints an error message if one or more entries fail to be removed.
441/// Returns `Err` if an I/O error occurs.
442fn maybe_remove_invalid_entries(
443    args: &CheckIntegrityArgs,
444    affected_keystores: &[InvalidKeystoreEntries],
445    keymgr: &KeyMgr,
446) -> Result<()> {
447    if affected_keystores.is_empty() || !args.sweep {
448        return Ok(());
449    }
450
451    let should_remove = args.batch || prompt("Remove all invalid entries?")?;
452
453    if !should_remove {
454        return Ok(());
455    }
456
457    for InvalidKeystoreEntries {
458        keystore_id: _,
459        entries,
460    } in affected_keystores
461    {
462        for InvalidKeystoreEntry {
463            entry,
464            error_msg: _,
465        } in entries.iter()
466        {
467            let raw_entry = match entry {
468                Ok(e) => &e.raw_entry(),
469                Err(e) => e.entry().deref(),
470            };
471
472            if keymgr
473                .remove_unchecked(&raw_entry.raw_id().to_string(), raw_entry.keystore_id())
474                .is_err()
475            {
476                eprintln!("Failed to remove entry at location: {}", raw_entry.raw_id());
477            }
478        }
479    }
480
481    Ok(())
482}
483
484/// Helper function for `display_invalid_keystore_entries` that reduces cognitive complexity.
485///
486/// Produces and displays the opening section of the final output, given a list of keystores
487/// containing invalid entries and their IDs. This function does not check whether
488/// `affected_keystores` or the inner collections are empty.
489fn print_check_integrity_incipit(affected_keystores: &[InvalidKeystoreEntries]) {
490    let len = affected_keystores.len();
491
492    let mut incipit = "Found problems in keystore".to_string();
493    if len > 1 {
494        incipit.push('s');
495    }
496    incipit.push_str(": ");
497
498    let keystore_names: Vec<_> = affected_keystores
499        .iter()
500        .map(|x| x.keystore_id.to_string())
501        .collect();
502    incipit.push_str(&keystore_names.join(", "));
503    incipit.push('.');
504
505    println!("{}", incipit);
506}