1use std::ops::Deref;
6use std::str::FromStr;
7
8use anyhow::Result;
9
10use arti_client::{InertTorClient, TorClient, TorClientConfig};
11use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand};
12use tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreEntryResult, KeystoreId, UnrecognizedEntryError};
13use tor_rtcompat::Runtime;
14
15use crate::{ArtiConfig, subcommands::prompt};
16
17#[cfg(feature = "onion-service-service")]
18use tor_hsservice::OnionService;
19
20const LINE_LEN: usize = 80;
23
24#[derive(Debug, Parser)]
26pub(crate) enum KeysSubcommands {
27 #[command(subcommand)]
29 Keys(KeysSubcommand),
30}
31
32#[derive(Subcommand, Debug, Clone)]
34pub(crate) enum KeysSubcommand {
35 List(ListArgs),
42
43 ListKeystores,
45
46 CheckIntegrity(CheckIntegrityArgs),
53}
54
55#[derive(Debug, Clone, Args)]
57pub(crate) struct ListArgs {
58 #[arg(short, long)]
63 keystore_id: Option<String>,
64}
65
66#[derive(Debug, Clone, Args)]
68pub(crate) struct CheckIntegrityArgs {
69 #[arg(short, long)]
74 keystore_id: Option<KeystoreId>,
75
76 #[arg(long, short, default_value_t = false)]
78 sweep: bool,
79
80 #[arg(long, short, default_value_t = false)]
85 batch: bool,
86}
87
88#[derive(Clone)]
93struct InvalidKeystoreEntries<'a> {
94 keystore_id: KeystoreId,
96 entries: Vec<InvalidKeystoreEntry<'a>>,
99}
100
101#[derive(Clone)]
106struct InvalidKeystoreEntry<'a> {
107 entry: KeystoreEntryResult<KeystoreEntry<'a>>,
109 error_msg: String,
112}
113
114pub(crate) fn run<R: Runtime>(
116 runtime: R,
117 keys_matches: &ArgMatches,
118 config: &ArtiConfig,
119 client_config: &TorClientConfig,
120) -> Result<()> {
121 let subcommand =
122 KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
123 let rt = runtime.clone();
124 let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
125
126 match subcommand {
127 KeysSubcommand::List(args) => run_list_keys(&args, &client_builder.create_inert()?),
128 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}
137
138fn display_entry(entry: &KeystoreEntry, keymgr: &KeyMgr) {
140 let raw_entry = entry.raw_entry();
141 match keymgr.describe(entry.key_path()) {
142 Some(e) => {
143 println!(" Keystore ID: {}", entry.keystore_id());
144 println!(" Role: {}", e.role());
145 println!(" Summary: {}", e.summary());
146 println!(" KeystoreItemType: {:?}", entry.key_type());
147 println!(" Location: {}", raw_entry.raw_id());
148 let extra_info = e.extra_info();
149 println!(" Extra info:");
150 for (key, value) in extra_info {
151 println!(" - {key}: {value}");
152 }
153 }
154 None => {
155 println!(" Unrecognized path {}", raw_entry.raw_id());
156 }
157 }
158 println!("\n {}", "-".repeat(LINE_LEN));
159}
160
161fn display_unrecognized_entry(entry: &UnrecognizedEntryError) {
163 let raw_entry = entry.entry();
164 println!(" Unrecognized entry");
165 #[allow(clippy::single_match)]
166 match raw_entry.raw_id() {
167 tor_keymgr::RawEntryId::Path(p) => {
168 println!(" Keystore ID: {}", raw_entry.keystore_id());
169 println!(" Location: {}", p.to_string_lossy());
170 println!(" Error: {}", entry.error());
171 }
172 other => {
176 panic!("Unhandled enum variant: {:?}", other);
177 }
178 }
179 println!("\n {}\n", "-".repeat(LINE_LEN));
180}
181
182fn run_list_keys(args: &ListArgs, client: &InertTorClient) -> Result<()> {
184 let keymgr = client.keymgr()?;
185 match &args.keystore_id {
190 Some(s) => {
191 let id = KeystoreId::from_str(s)?;
192 let empty_err_msg = format!("Currently there are no entries in the keystore {}.", s);
193 display_keystore_entries(
194 &keymgr.list_by_id(&id)?,
195 keymgr,
196 "Keystore entries",
197 &empty_err_msg,
198 );
199 }
200 None => {
201 display_keystore_entries(
202 &keymgr.list()?,
203 keymgr,
204 "Keystore entries",
205 "Currently there are no entries in any of the keystores.",
206 );
207 }
208 }
209 Ok(())
210}
211
212fn run_list_keystores(client: &InertTorClient) -> Result<()> {
214 let keymgr = client.keymgr()?;
215 let entries = keymgr.list_keystores();
216
217 if entries.is_empty() {
218 println!("Currently there are no keystores available.");
219 } else {
220 println!(" Keystores:\n");
221 for entry in entries {
222 println!(" - {:?}\n", entry.as_ref());
225 }
226 }
227
228 Ok(())
229}
230
231fn 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 let mut affected_keystores = Vec::new();
255 cfg_if::cfg_if! {
256 if #[cfg(feature = "onion-service-service")] {
257 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 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 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
338fn 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
367fn display_keystore_entries(
369 entries: &[KeystoreEntryResult<KeystoreEntry>],
370 keymgr: &KeyMgr,
371 header: &str,
372 empty_err_msg: &str,
373) {
374 if entries.is_empty() {
375 println!("{empty_err_msg}");
376 return;
377 }
378 println!(" ===== {} =====\n\n", header);
379 for entry in entries {
380 match entry {
381 Ok(entry) => {
382 display_entry(entry, keymgr);
383 }
384 Err(entry) => {
385 display_unrecognized_entry(entry);
386 }
387 }
388 }
389}
390
391#[cfg(feature = "onion-service-service")]
395fn 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#[cfg(feature = "onion-service-service")]
415fn 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
437fn 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
484fn 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}