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
                .as_ref(),
133
            config,
134
            client_config,
135
        ),
136
    }
137
60
}
138

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

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

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

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

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

            
229
18
    Ok(())
230
18
}
231

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

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

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

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

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

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

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

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

            
332
    display_invalid_keystore_entries(&affected_keystores);
333

            
334
    maybe_remove_invalid_entries(args, &affected_keystores, keymgr)?;
335

            
336
    Ok(())
337
}
338

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

            
349
    print_check_integrity_incipit(affected_keystores);
350

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

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

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

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

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

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

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

            
454
    if !should_remove {
455
        return Ok(());
456
    }
457

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

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

            
482
    Ok(())
483
}
484

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

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

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

            
506
    println!("{}", incipit);
507
}