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

            
5
use std::str::FromStr;
6

            
7
use anyhow::Result;
8

            
9
use arti_client::{InertTorClient, TorClient, TorClientConfig};
10
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand};
11
use tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreEntryResult, KeystoreId, UnrecognizedEntryError};
12
use tor_rtcompat::Runtime;
13

            
14
use crate::{ArtiConfig, subcommands::prompt};
15

            
16
#[cfg(feature = "onion-service-service")]
17
use tor_hsservice::OnionService;
18

            
19
/// Length of a line, used for formatting
20
// TODO: use COLUMNS instead of an arbitrary LINE_LEN
21
const LINE_LEN: usize = 80;
22

            
23
/// The `keys` subcommands the arti CLI will be augmented with.
24
#[derive(Debug, Parser)]
25
pub(crate) enum KeysSubcommands {
26
    /// Run keystore management commands.
27
    #[command(subcommand)]
28
    Keys(KeysSubcommand),
29
}
30

            
31
/// The `keys` subcommand.
32
#[derive(Subcommand, Debug, Clone)]
33
pub(crate) enum KeysSubcommand {
34
    /// List keys and certificates.
35
    ///
36
    /// Note: The output fields "Location" and "Keystore ID" represent,
37
    /// respectively, the raw identifier of an entry (e.g. <ARTI_PATH>.<ENTRY_TYPE>
38
    /// for `ArtiNativeKeystore`), and the identifier of the keystore that
39
    /// contains the entry.
40
    List(ListArgs),
41

            
42
    /// List keystores.
43
    ListKeystores,
44

            
45
    /// Validate the integrity of keystores.
46
    ///
47
    /// Detects and reports unrecognized entries and paths, as well as
48
    /// malformed or expired keys.
49
    ///
50
    /// Such entries will be removed if this command is invoked with `--sweep`.
51
    CheckIntegrity(CheckIntegrityArgs),
52
}
53

            
54
/// The arguments of the [`List`](KeysSubcommand::List) subcommand.
55
#[derive(Debug, Clone, Args)]
56
pub(crate) struct ListArgs {
57
    /// Identifier of the keystore.
58
    ///
59
    /// If omitted, keys and certificates
60
    /// from all the keystores will be returned.
61
    #[arg(short, long)]
62
    keystore_id: Option<String>,
63
}
64

            
65
/// The arguments of the [`CheckIntegrity`](KeysSubcommand::CheckIntegrity) subcommand.
66
#[derive(Debug, Clone, Args)]
67
pub(crate) struct CheckIntegrityArgs {
68
    /// Identifier of the keystore.
69
    ///
70
    /// If omitted, keys and certificates
71
    /// from all the keystores will be checked.
72
    #[arg(short, long)]
73
    keystore_id: Option<KeystoreId>,
74

            
75
    /// Remove the detected invalid keystore entries.
76
    #[arg(long, short, default_value_t = false)]
77
    sweep: bool,
78

            
79
    /// With this flag active no prompt will be shown
80
    /// and no confirmation will be asked.
81
    // TODO: Rephrase this and the `batch` flags of the
82
    // other commands in the present tense.
83
    #[arg(long, short, default_value_t = false)]
84
    batch: bool,
85
}
86

            
87
/// A set of invalid keystore entries associated with a keystore ID.
88
/// This struct is used solely to reduce type complexity; it does not
89
/// perform any validation (e.g., whether the entries actually belong
90
/// to the keystore indicated by the ID).
91
#[derive(Clone)]
92
struct InvalidKeystoreEntries<'a> {
93
    /// The `KeystoreId` that the entries are expected to belong to.
94
    keystore_id: KeystoreId,
95
    /// The list of invalid entries that logically belong to the keystore identified
96
    /// by `keystore_id`.
97
    entries: Vec<InvalidKeystoreEntry<'a>>,
98
}
99

            
100
/// An invalid keystore entry associated with the error that caused it to be
101
/// invalid. This struct is used solely to reduce type complexity; it does not
102
/// perform any validation (e.g., whether the `error_msg` actually corresponds
103
/// to the error that caused the invalid entry).
104
#[derive(Clone)]
105
struct InvalidKeystoreEntry<'a> {
106
    /// The entry
107
    entry: KeystoreEntryResult<KeystoreEntry<'a>>,
108
    /// The error message derived from the error that caused the entry to be invalid.
109
    /// This field is needed (even if `Err(UnrecognizedEntryError)` contains the error) because `Ok(KeystoreEntry)`s could be invalid too.
110
    error_msg: String,
111
}
112

            
113
/// Run the `keys` subcommand.
114
60
pub(crate) fn run<R: Runtime>(
115
60
    runtime: R,
116
60
    keys_matches: &ArgMatches,
117
60
    config: &ArtiConfig,
118
60
    client_config: &TorClientConfig,
119
60
) -> Result<()> {
120
60
    let subcommand =
121
60
        KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
122
60
    let rt = runtime.clone();
123
60
    let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
124

            
125
60
    match subcommand {
126
42
        KeysSubcommand::List(args) => run_list_keys(&args, &client_builder.create_inert()?),
127
18
        KeysSubcommand::ListKeystores => run_list_keystores(&client_builder.create_inert()?),
128
        KeysSubcommand::CheckIntegrity(args) => run_check_integrity(
129
            &args,
130
            rt.reenter_block_on(client_builder.create_bootstrapped())?
131
                .as_ref(),
132
            config,
133
            client_config,
134
        ),
135
    }
136
60
}
137

            
138
/// Print information about a keystore entry.
139
72
fn display_entry(entry: &KeystoreEntry, keymgr: &KeyMgr) {
140
72
    match keymgr.describe(entry.key_path()) {
141
60
        Some(e) => {
142
60
            println!(" Keystore ID: {}", entry.keystore_id());
143
60
            println!(" Role: {}", e.role());
144
60
            println!(" Summary: {}", e.summary());
145
60
            println!(" KeystoreItemType: {:?}", entry.key_type());
146
60
            println!(" Location: {}", entry.raw_id());
147
60
            let extra_info = e.extra_info();
148
60
            println!(" Extra info:");
149
60
            for (key, value) in extra_info {
150
60
                println!(" - {key}: {value}");
151
60
            }
152
        }
153
12
        None => {
154
12
            println!(" Unrecognized path {}", entry.raw_id());
155
12
        }
156
    }
157
72
    println!("\n {}", "-".repeat(LINE_LEN));
158
72
}
159

            
160
/// Print information about an unrecognized keystore entry.
161
48
fn display_unrecognized_entry(entry: &UnrecognizedEntryError) {
162
48
    let raw_entry = entry.entry();
163
48
    println!(" Unrecognized entry");
164
    #[allow(clippy::single_match)]
165
48
    match raw_entry.raw_id() {
166
48
        tor_keymgr::RawEntryId::Path(p) => {
167
48
            println!(" Keystore ID: {}", raw_entry.keystore_id());
168
48
            println!(" Location: {}", p.to_string_lossy());
169
48
            println!(" Error: {}", entry.error());
170
48
        }
171
        // NOTE: For the time being Arti only supports
172
        // on-disk keystores, but more supported medium
173
        // will be added.
174
        other => {
175
            panic!("Unhandled enum variant: {:?}", other);
176
        }
177
    }
178
48
    println!("\n {}\n", "-".repeat(LINE_LEN));
179
48
}
180

            
181
/// Run the `keys list` subcommand.
182
42
fn run_list_keys(args: &ListArgs, client: &InertTorClient) -> Result<()> {
183
42
    let keymgr = client.keymgr()?;
184
    // TODO: in the future we could group entries by their type
185
    // (recognized, unrecognized and unrecognized path).
186
    // That way we don't need to print "Unrecognized path",
187
    // "Unrecognized" entry etc. for each unrecognized entry.
188
42
    match &args.keystore_id {
189
24
        Some(s) => {
190
24
            let id = KeystoreId::from_str(s)?;
191
24
            let empty_err_msg = format!("Currently there are no entries in the keystore {}.", s);
192
18
            display_keystore_entries(
193
24
                &keymgr.list_by_id(&id)?,
194
18
                keymgr,
195
18
                "Keystore entries",
196
18
                &empty_err_msg,
197
            );
198
        }
199
        None => {
200
18
            display_keystore_entries(
201
18
                &keymgr.list()?,
202
18
                keymgr,
203
18
                "Keystore entries",
204
18
                "Currently there are no entries in any of the keystores.",
205
            );
206
        }
207
    }
208
36
    Ok(())
209
42
}
210

            
211
/// Run `keys list-keystores` subcommand.
212
18
fn run_list_keystores(client: &InertTorClient) -> Result<()> {
213
18
    let keymgr = client.keymgr()?;
214
18
    let entries = keymgr.list_keystores();
215

            
216
18
    if entries.is_empty() {
217
        println!("Currently there are no keystores available.");
218
    } else {
219
18
        println!(" Keystores:\n");
220
24
        for entry in entries {
221
24
            // TODO: We need something similar to [`KeyPathInfo`](tor_keymgr::KeyPathInfo)
222
24
            // for `KeystoreId`
223
24
            println!(" - {:?}\n", entry.as_ref());
224
24
        }
225
    }
226

            
227
18
    Ok(())
228
18
}
229

            
230
/// Run `keys check-integrity` subcommand.
231
fn run_check_integrity<R: Runtime>(
232
    args: &CheckIntegrityArgs,
233
    client: &TorClient<R>,
234
    config: &ArtiConfig,
235
    client_config: &TorClientConfig,
236
) -> Result<()> {
237
    let keymgr = client.keymgr()?;
238

            
239
    let keystore_ids = match &args.keystore_id {
240
        Some(id) => vec![id.to_owned()],
241
        None => keymgr.list_keystores(),
242
    };
243
    let keystores: Vec<(_, Vec<KeystoreEntryResult<KeystoreEntry>>)> = keystore_ids
244
        .into_iter()
245
        .map(|id| keymgr.list_by_id(&id).map(|entries| (id, entries)))
246
        .collect::<Result<Vec<_>, _>>()?;
247

            
248
    // Unlike `keystores`, which has type `Vec<(KeystoreId, Vec<KeystoreEntryResult<KeystoreEntry>>)>`,
249
    // `affected_keystores` has type `InvalidKeystoreEntries`. This distinction is
250
    // necessary because the entries in `keystores` will be evaluated, and if any are
251
    // found to be invalid, the associated error messages must be stored somewhere
252
    // for later display.
253
    let mut affected_keystores = Vec::new();
254
    cfg_if::cfg_if! {
255
        if #[cfg(feature = "onion-service-service")] {
256
            // `service` cannot be dropped as long as `expired_entries` is in use, since
257
            // `expired_entries` holds references to `services`.
258
            let services = create_all_services(config, client_config)?;
259
            let mut expired_entries: Vec<_> = get_expired_keys(&services, client)?;
260
        }
261
    }
262

            
263
    for (id, entries) in keystores {
264
        let mut invalid_entries = entries
265
            .into_iter()
266
            .filter_map(|entry| match entry {
267
                Ok(e) => keymgr
268
                    .validate_entry_integrity(&e)
269
                    .map_err(|err| InvalidKeystoreEntry {
270
                        entry: Ok(e),
271
                        error_msg: err.to_string(),
272
                    })
273
                    .err(),
274
                Err(err) => {
275
                    let error = err.error().to_string();
276
                    Some(InvalidKeystoreEntry {
277
                        entry: Err(err),
278
                        error_msg: error,
279
                    })
280
                }
281
            })
282
            .collect::<Vec<_>>();
283

            
284
        cfg_if::cfg_if! {
285
            if #[cfg(feature = "onion-service-service")] {
286
                // For the current keystore, transfer its expired keys from `expired_entries`
287
                // to `invalid_entries`.
288
                expired_entries.retain(|expired_entry| {
289
                    match &expired_entry.entry {
290
                        Ok(entry) => {
291
                            if entry.keystore_id() == &id {
292
                                invalid_entries.push(expired_entry.clone());
293
                                return false;
294
                            }
295
                        }
296
                        Err(err) => {
297
                            eprintln!("WARNING: Unexpected invalid keystore entry encountered: {}", err);
298
                        }
299
                    }
300
                    true
301
                })
302
            }
303
        }
304

            
305
        if invalid_entries.is_empty() {
306
            println!("{}: OK.\n", id);
307
            continue;
308
        }
309

            
310
        affected_keystores.push(InvalidKeystoreEntries {
311
            keystore_id: id,
312
            entries: invalid_entries,
313
        });
314
    }
315

            
316
    // Expired entries are obtained from the registered keystore. Since we have iterated over every
317
    // registered keystore and removed all entries associated with the current keystore, the
318
    // collection `expired_entries` should be empty. If it is not, there is a bug (see
319
    // [`OnionService::list_expired_keys`]).
320
    cfg_if::cfg_if! {
321
        if #[cfg(feature = "onion-service-service")] {
322
            if !expired_entries.is_empty() {
323
                return Err(anyhow::anyhow!(
324
                    "Encountered an expired key that doesn't belong to a registered keystore."
325
                ));
326
            }
327
        }
328
    }
329

            
330
    display_invalid_keystore_entries(&affected_keystores);
331

            
332
    maybe_remove_invalid_entries(args, &affected_keystores, keymgr)?;
333

            
334
    Ok(())
335
}
336

            
337
/// Helper function for `run_check_integrity` that reduces cognitive complexity.
338
///
339
/// Displays invalid keystore entries grouped by `KeystoreId`, showing the `raw_id`
340
/// of each key and the associated error message in a unified report to the user.
341
/// If no invalid entries are provided, nothing is printed.
342
fn display_invalid_keystore_entries(affected_keystores: &[InvalidKeystoreEntries]) {
343
    if affected_keystores.is_empty() {
344
        return;
345
    }
346

            
347
    print_check_integrity_incipit(affected_keystores);
348

            
349
    for InvalidKeystoreEntries {
350
        keystore_id,
351
        entries,
352
    } in affected_keystores
353
    {
354
        println!("\nInvalid keystore entries in keystore {}:\n", keystore_id);
355
        for InvalidKeystoreEntry { entry, error_msg } in entries {
356
            let raw_id = match entry {
357
                Ok(e) => e.raw_id(),
358
                Err(e) => e.entry().raw_id(),
359
            };
360
            println!("{raw_id}");
361
            println!("\tError: {}", error_msg);
362
        }
363
    }
364
}
365

            
366
/// Helper function of `run_list_keys`, reduces cognitive complexity.
367
36
fn display_keystore_entries(
368
36
    entries: &[KeystoreEntryResult<KeystoreEntry>],
369
36
    keymgr: &KeyMgr,
370
36
    header: &str,
371
36
    empty_err_msg: &str,
372
36
) {
373
36
    if entries.is_empty() {
374
12
        println!("{empty_err_msg}");
375
12
        return;
376
24
    }
377
24
    println!(" ===== {} =====\n\n", header);
378
120
    for entry in entries {
379
120
        match entry {
380
72
            Ok(entry) => {
381
72
                display_entry(entry, keymgr);
382
72
            }
383
48
            Err(entry) => {
384
48
                display_unrecognized_entry(entry);
385
48
            }
386
        }
387
    }
388
36
}
389

            
390
/// Helper function for `run_check_integrity`.
391
///
392
/// Creates an [`OnionService`] for each configured hidden service.
393
#[cfg(feature = "onion-service-service")]
394
fn create_all_services(
395
    config: &ArtiConfig,
396
    client_config: &TorClientConfig,
397
) -> Result<Vec<OnionService>> {
398
    let mut services = Vec::new();
399
    for (_, cfg) in config.onion_services.iter() {
400
        services.push(
401
            TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
402
                client_config,
403
                cfg.svc_cfg.clone(),
404
            )?,
405
        );
406
    }
407
    Ok(services)
408
}
409

            
410
/// Helper function for `run_check_integrity`.
411
///
412
/// Gathers all expired keys from the provided hidden services.
413
#[cfg(feature = "onion-service-service")]
414
fn get_expired_keys<'a, R: Runtime>(
415
    services: &'a Vec<OnionService>,
416
    client: &TorClient<R>,
417
) -> Result<Vec<InvalidKeystoreEntry<'a>>> {
418
    let netdir = client.dirmgr()?.timely_netdir()?;
419

            
420
    let mut expired_keys = Vec::new();
421
    for service in services {
422
        expired_keys.append(
423
            &mut service
424
                .list_expired_keys(&netdir)?
425
                .into_iter()
426
                .map(|entry| InvalidKeystoreEntry {
427
                    entry: Ok(entry),
428
                    error_msg: "The entry is expired.".to_string(),
429
                })
430
                .collect(),
431
        );
432
    }
433
    Ok(expired_keys)
434
}
435

            
436
/// Helper function for `run_check_integrity`.
437
///
438
/// Removes invalid keystore entries.
439
/// Prints an error message if one or more entries fail to be removed.
440
/// Returns `Err` if an I/O error occurs.
441
fn maybe_remove_invalid_entries(
442
    args: &CheckIntegrityArgs,
443
    affected_keystores: &[InvalidKeystoreEntries],
444
    keymgr: &KeyMgr,
445
) -> Result<()> {
446
    if affected_keystores.is_empty() || !args.sweep {
447
        return Ok(());
448
    }
449

            
450
    let should_remove = args.batch || prompt("Remove all invalid entries?")?;
451

            
452
    if !should_remove {
453
        return Ok(());
454
    }
455

            
456
    for InvalidKeystoreEntries {
457
        keystore_id: _,
458
        entries,
459
    } in affected_keystores
460
    {
461
        for InvalidKeystoreEntry {
462
            entry,
463
            error_msg: _,
464
        } in entries.iter()
465
        {
466
            let (raw_id, keystore_id) = match entry {
467
                Ok(e) => (e.raw_id(), e.keystore_id()),
468
                Err(e) => (e.entry().raw_id(), e.entry().keystore_id()),
469
            };
470

            
471
            if keymgr
472
                .remove_unchecked(&raw_id.to_string(), keystore_id)
473
                .is_err()
474
            {
475
                eprintln!("Failed to remove entry at location: {raw_id}");
476
            }
477
        }
478
    }
479

            
480
    Ok(())
481
}
482

            
483
/// Helper function for `display_invalid_keystore_entries` that reduces cognitive complexity.
484
///
485
/// Produces and displays the opening section of the final output, given a list of keystores
486
/// containing invalid entries and their IDs. This function does not check whether
487
/// `affected_keystores` or the inner collections are empty.
488
fn print_check_integrity_incipit(affected_keystores: &[InvalidKeystoreEntries]) {
489
    let len = affected_keystores.len();
490

            
491
    let mut incipit = "Found problems in keystore".to_string();
492
    if len > 1 {
493
        incipit.push('s');
494
    }
495
    incipit.push_str(": ");
496

            
497
    let keystore_names: Vec<_> = affected_keystores
498
        .iter()
499
        .map(|x| x.keystore_id.to_string())
500
        .collect();
501
    incipit.push_str(&keystore_names.join(", "));
502
    incipit.push('.');
503

            
504
    println!("{}", incipit);
505
}