1use 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#[derive(Debug, Parser)]
23pub(crate) enum KeysSubcommands {
24 #[command(subcommand)]
26 Keys(KeysSubcommand),
27}
28
29#[derive(Subcommand, Debug, Clone)]
31pub(crate) enum KeysSubcommand {
32 List(ListArgs),
39
40 ListKeystores,
42
43 CheckIntegrity(CheckIntegrityArgs),
50}
51
52#[derive(Debug, Clone, Args)]
54pub(crate) struct ListArgs {
55 #[arg(short, long)]
60 keystore_id: Option<String>,
61
62 #[command(flatten)]
64 output_format: OutputFormat,
65}
66
67#[derive(Debug, Clone, Args)]
70#[group(multiple = false)]
71struct OutputFormat {
72 #[arg(long, default_value_t = false)]
76 compact: bool,
77}
78
79#[derive(Debug, Clone, Args)]
81pub(crate) struct CheckIntegrityArgs {
82 #[arg(short, long)]
87 keystore_id: Option<KeystoreId>,
88
89 #[arg(long, short, default_value_t = false)]
91 sweep: bool,
92
93 #[arg(long, short, default_value_t = false)]
98 batch: bool,
99}
100
101#[derive(Clone)]
106struct InvalidKeystoreEntries<'a> {
107 keystore_id: KeystoreId,
109 entries: Vec<InvalidKeystoreEntry<'a>>,
112}
113
114#[derive(Clone)]
119struct InvalidKeystoreEntry<'a> {
120 entry: KeystoreEntryResult<KeystoreEntry<'a>>,
122 error_msg: String,
125}
126
127pub(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
152fn 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
169fn 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 other => {
195 panic!("Unhandled enum variant: {:?}", other);
196 }
197 }
198}
199
200fn 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 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
271fn 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 println!("- {:?}\n", entry.as_ref());
284 }
285 }
286
287 Ok(())
288}
289
290fn 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 let mut affected_keystores = Vec::new();
314 cfg_if::cfg_if! {
315 if #[cfg(feature = "onion-service-service")] {
316 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 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 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
397fn 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#[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#[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
472fn 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
519fn 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}