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