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::{
12
    KeyMgr, KeyPathInfo, KeystoreEntry, KeystoreEntryResult, KeystoreId, UnrecognizedEntryError,
13
};
14
use tor_rtcompat::Runtime;
15

            
16
use crate::{ArtiConfig, subcommands::prompt};
17

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

            
21
/// The `keys` subcommands the arti CLI will be augmented with.
22
#[derive(Debug, Parser)]
23
pub(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)]
31
pub(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)]
54
pub(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)]
71
struct 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)]
81
pub(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)]
106
struct 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)]
119
struct 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.
128
30
pub(crate) fn run<R: Runtime>(
129
30
    runtime: R,
130
30
    keys_matches: &ArgMatches,
131
30
    config: &ArtiConfig,
132
30
    client_config: &TorClientConfig,
133
30
) -> Result<()> {
134
30
    let subcommand =
135
30
        KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
136
30
    let rt = runtime.clone();
137
30
    let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
138

            
139
30
    match subcommand {
140
24
        KeysSubcommand::List(args) => run_list_keys(args, &client_builder.create_inert()?),
141
6
        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
30
}
151

            
152
/// Print information about a valid keystore entry.
153
36
fn display_entry(entry: &(KeystoreEntry<'_>, KeyPathInfo), display_keystore_id: bool) {
154
36
    let (entry, info) = entry;
155
36
    if display_keystore_id {
156
24
        println!("Keystore ID: {}", entry.keystore_id());
157
24
    }
158
36
    println!("Role: {}", info.role());
159
36
    println!("Summary: {}", info.summary());
160
36
    println!("KeystoreItemType: {:?}", entry.key_type());
161
36
    println!("Location: {}", entry.raw_id());
162
36
    let extra_info = info.extra_info();
163
36
    println!("Extra info:");
164
36
    for (key, value) in extra_info {
165
36
        println!("- {key}: {value}");
166
36
    }
167
36
}
168

            
169
/// Print information about an unrecognized keystore entry.
170
42
fn display_unrecognized_entry(
171
42
    entry: &UnrecognizedEntryError,
172
42
    display_keystore_id: bool,
173
42
    compact_output: bool,
174
42
) {
175
42
    let raw_entry = entry.entry();
176
    #[allow(clippy::single_match)]
177
42
    match raw_entry.raw_id() {
178
42
        tor_keymgr::RawEntryId::Path(p) => {
179
42
            let path = p.to_string_lossy();
180
42
            if compact_output {
181
18
                println!("{path}");
182
18
            } else {
183
24
                if display_keystore_id {
184
18
                    println!("Keystore ID: {}", raw_entry.keystore_id());
185
18
                }
186
24
                println!("Location: {path}");
187
24
                println!("Error: {}", entry.error());
188
24
                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
42
}
199

            
200
/// Run the `keys list` subcommand.
201
24
fn run_list_keys(args: ListArgs, client: &InertTorClient) -> Result<()> {
202
24
    let keymgr = client.keymgr()?;
203
24
    let (display_keystore_id, entries) = if let Some(id) = args.keystore_id {
204
12
        let id = KeystoreId::from_str(&id)?;
205
12
        let entries = keymgr.list_by_id(&id)?;
206
6
        if entries.is_empty() {
207
            return Ok(());
208
6
        }
209
6
        (false, entries)
210
    } else {
211
12
        let entries = keymgr.list()?;
212
12
        if entries.is_empty() {
213
            return Ok(());
214
12
        }
215
12
        (true, entries)
216
    };
217

            
218
18
    let (mut valid_entries, mut unrecognized_entries, mut unrecognized_paths) =
219
18
        (vec![], vec![], vec![]);
220
120
    for entry in entries {
221
120
        match entry {
222
78
            Ok(e) => {
223
78
                if let Some(info) = keymgr.describe(e.key_path()) {
224
60
                    valid_entries.push((e, info));
225
60
                } else {
226
18
                    unrecognized_paths.push(e);
227
18
                }
228
            }
229
42
            Err(e) => {
230
42
                unrecognized_entries.push(e);
231
42
            }
232
        }
233
    }
234

            
235
    // Sort the entries to make the output deterministic
236
87
    valid_entries.sort_by_key(|(e, _info)| (e.keystore_id(), e.key_path().to_string()));
237
75
    unrecognized_entries.sort_by_key(|e| e.entry().raw_id().to_string());
238
18
    unrecognized_paths.sort_by_key(|e| e.key_path().to_string());
239

            
240
60
    for entry in valid_entries {
241
60
        if args.output_format.compact {
242
24
            println!("{}", entry.0.raw_id());
243
36
        } else {
244
36
            display_entry(&entry, display_keystore_id);
245
36
            println!();
246
36
        }
247
    }
248
18
    println!();
249

            
250
18
    if !unrecognized_entries.is_empty() || !unrecognized_paths.is_empty() {
251
18
        println!("Broken entries\n");
252
42
        for entry in unrecognized_entries {
253
42
            display_unrecognized_entry(&entry, display_keystore_id, args.output_format.compact);
254
42
        }
255
18
        for entry in unrecognized_paths {
256
18
            let raw_id = entry.raw_id();
257
18
            if args.output_format.compact {
258
6
                println!("{raw_id}");
259
6
            } else {
260
12
                if display_keystore_id {
261
6
                    println!("Keystore ID: *not available*");
262
6
                }
263
12
                println!("Location: {raw_id}");
264
12
                println!("Error: Unrecognized\n");
265
            }
266
        }
267
    }
268
18
    Ok(())
269
24
}
270

            
271
/// Run `keys list-keystores` subcommand.
272
6
fn run_list_keystores(client: &InertTorClient) -> Result<()> {
273
6
    let keymgr = client.keymgr()?;
274
6
    let entries = keymgr.list_keystores();
275

            
276
6
    if entries.is_empty() {
277
        println!("Currently there are no keystores available.");
278
    } else {
279
6
        println!("Keystores:\n");
280
6
        for entry in entries {
281
6
            // TODO: We need something similar to [`KeyPathInfo`](tor_keymgr::KeyPathInfo)
282
6
            // for `KeystoreId`
283
6
            println!("- {:?}\n", entry.as_ref());
284
6
        }
285
    }
286

            
287
6
    Ok(())
288
6
}
289

            
290
/// Run `keys check-integrity` subcommand.
291
fn 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.
402
fn 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")]
430
fn 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")]
450
fn 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.
477
fn 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.
524
fn 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
}