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::ops::Deref;
6
use std::str::FromStr;
7

            
8
use anyhow::Result;
9

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

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

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

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

            
24
/// The `keys` subcommands the arti CLI will be augmented with.
25
#[derive(Debug, Parser)]
26
pub(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)]
34
pub(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)]
57
pub(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)]
68
pub(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)]
93
struct 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)]
106
struct 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.
115
60
pub(crate) fn run<R: Runtime>(
116
60
    runtime: R,
117
60
    keys_matches: &ArgMatches,
118
60
    config: &ArtiConfig,
119
60
    client_config: &TorClientConfig,
120
60
) -> Result<()> {
121
60
    let subcommand =
122
60
        KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
123
60
    let rt = runtime.clone();
124
60
    let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
125

            
126
60
    match subcommand {
127
42
        KeysSubcommand::List(args) => run_list_keys(&args, &client_builder.create_inert()?),
128
18
        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
60
}
137

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

            
161
/// Print information about an unrecognized keystore entry.
162
48
fn display_unrecognized_entry(entry: &UnrecognizedEntryError) {
163
48
    let raw_entry = entry.entry();
164
48
    println!(" Unrecognized entry");
165
    #[allow(clippy::single_match)]
166
48
    match raw_entry.raw_id() {
167
48
        tor_keymgr::RawEntryId::Path(p) => {
168
48
            println!(" Keystore ID: {}", raw_entry.keystore_id());
169
48
            println!(" Location: {}", p.to_string_lossy());
170
48
            println!(" Error: {}", entry.error());
171
48
        }
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
48
    println!("\n {}\n", "-".repeat(LINE_LEN));
180
48
}
181

            
182
/// Run the `keys list` subcommand.
183
42
fn run_list_keys(args: &ListArgs, client: &InertTorClient) -> Result<()> {
184
42
    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
42
    match &args.keystore_id {
190
24
        Some(s) => {
191
24
            let id = KeystoreId::from_str(s)?;
192
24
            let empty_err_msg = format!("Currently there are no entries in the keystore {}.", s);
193
18
            display_keystore_entries(
194
24
                &keymgr.list_by_id(&id)?,
195
18
                keymgr,
196
18
                "Keystore entries",
197
18
                &empty_err_msg,
198
            );
199
        }
200
        None => {
201
18
            display_keystore_entries(
202
18
                &keymgr.list()?,
203
18
                keymgr,
204
18
                "Keystore entries",
205
18
                "Currently there are no entries in any of the keystores.",
206
            );
207
        }
208
    }
209
36
    Ok(())
210
42
}
211

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

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

            
228
18
    Ok(())
229
18
}
230

            
231
/// Run `keys check-integrity` subcommand.
232
fn 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.
343
fn 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.
368
36
fn display_keystore_entries(
369
36
    entries: &[KeystoreEntryResult<KeystoreEntry>],
370
36
    keymgr: &KeyMgr,
371
36
    header: &str,
372
36
    empty_err_msg: &str,
373
36
) {
374
36
    if entries.is_empty() {
375
12
        println!("{empty_err_msg}");
376
12
        return;
377
24
    }
378
24
    println!(" ===== {} =====\n\n", header);
379
144
    for entry in entries {
380
120
        match entry {
381
72
            Ok(entry) => {
382
72
                display_entry(entry, keymgr);
383
72
            }
384
48
            Err(entry) => {
385
48
                display_unrecognized_entry(entry);
386
48
            }
387
        }
388
    }
389
36
}
390

            
391
/// Helper function for `run_check_integrity`.
392
///
393
/// Creates an [`OnionService`] for each configured hidden service.
394
#[cfg(feature = "onion-service-service")]
395
fn 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")]
415
fn 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.
442
fn 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.
489
fn 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
}