Skip to main content

arti/subcommands/
hsc.rs

1//! The `hsc` subcommand.
2
3use crate::subcommands::prompt;
4use crate::{Result, TorClient};
5
6use anyhow::{Context, anyhow};
7use arti_client::{HsClientDescEncKey, HsId, InertTorClient, KeystoreSelector, TorClientConfig};
8use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
9use safelog::DisplayRedacted;
10use tor_rtcompat::Runtime;
11#[cfg(feature = "onion-service-cli-extra")]
12use {
13    std::collections::{HashMap, hash_map::Entry},
14    tor_hsclient::HsClientDescEncKeypairSpecifier,
15    tor_hscrypto::pk::HsClientDescEncKeypair,
16    tor_keymgr::{CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId},
17};
18
19use std::fs::OpenOptions;
20use std::io::{self, Write};
21use std::str::FromStr;
22
23/// The hsc subcommands the arti CLI will be augmented with.
24#[derive(Parser, Debug)]
25pub(crate) enum HscSubcommands {
26    /// Run state management commands for an Arti hidden service client.
27    #[command(subcommand)]
28    Hsc(HscSubcommand),
29}
30
31/// The `hsc` subcommand.
32#[derive(Debug, Subcommand)]
33pub(crate) enum HscSubcommand {
34    /// Prepare a service discovery key for connecting
35    /// to a service running in restricted discovery mode.
36    /// (Deprecated: use `arti hsc key get` instead)
37    ///
38    // TODO: use a clap deprecation attribute when clap supports it:
39    // <https://github.com/clap-rs/clap/issues/3321>
40    #[command(arg_required_else_help = true)]
41    GetKey(GetKeyArgs),
42    /// Key management subcommands.
43    #[command(subcommand)]
44    Key(KeySubcommand),
45
46    /// Migrate service discovery keys from a registered CTor keystore to the primary
47    /// keystore
48    #[cfg(feature = "onion-service-cli-extra")]
49    #[command(name = "ctor-migrate")]
50    CTorMigrate(CTorMigrateArgs),
51}
52
53/// The `hsc-key` subcommand.
54#[derive(Debug, Subcommand)]
55pub(crate) enum KeySubcommand {
56    /// Get or generate a hidden service client key
57    #[command(arg_required_else_help = true)]
58    Get(GetKeyArgs),
59
60    /// Rotate a hidden service client key
61    #[command(arg_required_else_help = true)]
62    Rotate(RotateKeyArgs),
63
64    /// Remove a hidden service client key
65    #[command(arg_required_else_help = true)]
66    Remove(RemoveKeyArgs),
67}
68
69/// A type of key
70#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
71enum KeyType {
72    /// A service discovery key for connecting to a service
73    /// running in restricted discovery mode.
74    #[default]
75    ServiceDiscovery,
76}
77
78/// The arguments of the [`GetKey`](HscSubcommand::GetKey)
79/// subcommand.
80#[derive(Debug, Clone, Args)]
81pub(crate) struct GetKeyArgs {
82    /// Arguments shared by all hsc subcommands.
83    #[command(flatten)]
84    common: CommonArgs,
85
86    /// Arguments for configuring keygen.
87    #[command(flatten)]
88    keygen: KeygenArgs,
89
90    /// Whether to generate the key if it is missing
91    #[arg(
92        long,
93        default_value_t = GenerateKey::IfNeeded,
94        value_enum
95    )]
96    generate: GenerateKey,
97    // TODO: add an option for selecting the keystore to generate the keypair in
98}
99
100/// Whether to generate the key if missing.
101#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
102enum GenerateKey {
103    /// Do not generate the key.
104    No,
105    /// Generate the key if it's missing.
106    #[default]
107    IfNeeded,
108}
109
110/// The common arguments of the key subcommands.
111#[derive(Debug, Clone, Args)]
112pub(crate) struct CommonArgs {
113    /// The type of the key.
114    #[arg(
115        long,
116        default_value_t = KeyType::ServiceDiscovery,
117        value_enum
118    )]
119    key_type: KeyType,
120
121    /// With this flag active no prompt will be shown
122    /// and no confirmation will be asked
123    #[arg(long, short, default_value_t = false)]
124    batch: bool,
125}
126
127/// The common arguments of the key subcommands.
128#[derive(Debug, Clone, Args)]
129pub(crate) struct KeygenArgs {
130    /// Write the public key to FILE. Use - to write to stdout
131    #[arg(long, name = "FILE")]
132    output: String,
133
134    /// Whether to overwrite the output file if it already exists
135    #[arg(long)]
136    overwrite: bool,
137}
138
139/// The arguments of the [`Rotate`](KeySubcommand::Rotate) subcommand.
140#[derive(Debug, Clone, Args)]
141pub(crate) struct RotateKeyArgs {
142    /// Arguments shared by all hsc subcommands.
143    #[command(flatten)]
144    common: CommonArgs,
145
146    /// Arguments for configuring keygen.
147    #[command(flatten)]
148    keygen: KeygenArgs,
149}
150
151/// The arguments of the [`Remove`](KeySubcommand::Remove) subcommand.
152#[derive(Debug, Clone, Args)]
153pub(crate) struct RemoveKeyArgs {
154    /// Arguments shared by all hsc subcommands.
155    #[command(flatten)]
156    common: CommonArgs,
157}
158
159/// The arguments of the [`CTorMigrate`](HscSubcommand::CTorMigrate) subcommand.
160#[derive(Debug, Clone, Args)]
161#[cfg(feature = "onion-service-cli-extra")]
162pub(crate) struct CTorMigrateArgs {
163    /// With this flag active no prompt will be shown
164    /// and no confirmation will be asked.
165    #[arg(long, short, default_value_t = false)]
166    batch: bool,
167
168    /// The ID of the keystore that should be migrated.
169    // TODO: The command should detect if the ID provided belongs to a CTor keystore and return an
170    // error if it does not.
171    #[arg(long, short)]
172    from: KeystoreId,
173}
174
175/// Run the `hsc` subcommand.
176pub(crate) fn run<R: Runtime>(
177    runtime: R,
178    hsc_matches: &ArgMatches,
179    config: &TorClientConfig,
180) -> Result<()> {
181    use KeyType::*;
182
183    let subcommand =
184        HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
185    let client = TorClient::with_runtime(runtime)
186        .config(config.clone())
187        .create_inert()?;
188
189    match subcommand {
190        HscSubcommand::GetKey(args) => {
191            eprintln!(
192                "warning: using deprecated command 'arti hsc key-get` (hint: use 'arti hsc key get' instead)"
193            );
194            match args.common.key_type {
195                ServiceDiscovery => prepare_service_discovery_key(&args, &client),
196            }
197        }
198        HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
199        #[cfg(feature = "onion-service-cli-extra")]
200        HscSubcommand::CTorMigrate(args) => migrate_ctor_keys(&args, &client),
201    }
202}
203
204/// Run the `hsc key` subcommand
205fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
206    match subcommand {
207        KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
208        KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
209        KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
210    }
211}
212
213/// Run the `hsc prepare-stealth-mode-key` subcommand.
214fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
215    let addr = get_onion_address(&args.common)?;
216    let key = match args.generate {
217        GenerateKey::IfNeeded => {
218            // TODO: consider using get_or_generate in generate_service_discovery_key
219            client
220                .get_service_discovery_key(addr)?
221                .map(Ok)
222                .unwrap_or_else(|| {
223                    client.generate_service_discovery_key(KeystoreSelector::Primary, addr)
224                })?
225        }
226        GenerateKey::No => match client.get_service_discovery_key(addr)? {
227            Some(key) => key,
228            None => {
229                return Err(anyhow!(
230                    "Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
231                ));
232            }
233        },
234    };
235
236    display_service_discovery_key(&args.keygen, &key)
237}
238
239/// Display the public part of a service discovery key.
240//
241// TODO: have a more principled implementation for displaying messages, etc.
242// For example, it would be nice to centralize the logic for writing to stdout/file,
243// and to add a flag for choosing the output format (human-readable or json)
244fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
245    // Output the public key to the specified file, or to stdout.
246    match args.output.as_str() {
247        "-" => write_public_key(io::stdout(), key)?,
248        filename => {
249            let res = OpenOptions::new()
250                .create(true)
251                .create_new(!args.overwrite)
252                .write(true)
253                .truncate(true)
254                .open(filename)
255                .and_then(|f| write_public_key(f, key));
256
257            if let Err(e) = res {
258                match e.kind() {
259                    io::ErrorKind::AlreadyExists => {
260                        return Err(anyhow!(
261                            "{filename} already exists. Move it, or rerun with --overwrite to overwrite it"
262                        ));
263                    }
264                    _ => {
265                        return Err(e)
266                            .with_context(|| format!("could not write public key to {filename}"));
267                    }
268                }
269            }
270        }
271    }
272
273    Ok(())
274}
275
276/// Write the public part of `key` to `f`.
277fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
278    writeln!(f, "{}", key)?;
279    Ok(())
280}
281
282/// Run the `hsc rotate-key` subcommand.
283fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
284    let addr = get_onion_address(&args.common)?;
285    let msg = format!(
286        "rotate client restricted discovery key for {}?",
287        addr.display_unredacted()
288    );
289    if !args.common.batch && !prompt(&msg)? {
290        return Ok(());
291    }
292
293    let key = client.rotate_service_discovery_key(KeystoreSelector::default(), addr)?;
294
295    display_service_discovery_key(&args.keygen, &key)
296}
297
298/// Run the `hsc remove-key` subcommand.
299fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
300    let addr = get_onion_address(&args.common)?;
301    let msg = format!(
302        "remove client restricted discovery key for {}?",
303        addr.display_unredacted()
304    );
305    if !args.common.batch && !prompt(&msg)? {
306        return Ok(());
307    }
308
309    let _key = client.remove_service_discovery_key(KeystoreSelector::default(), addr)?;
310
311    Ok(())
312}
313
314/// Run the `hsc ctor-migrate` subcommand.
315#[cfg(feature = "onion-service-cli-extra")]
316fn migrate_ctor_keys(args: &CTorMigrateArgs, client: &InertTorClient) -> Result<()> {
317    let keymgr = client.keymgr()?;
318    let ctor_client_entries = read_ctor_keys(&keymgr.list_by_id(&args.from)?, args)?;
319
320    // TODO: Simplify this logic when addressing issue #1359.
321    // See [!3390 (comment 3288090)](https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3390#note_3288090).
322    let arti_keystore_id = KeystoreId::from_str("arti")
323        .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
324    for (hsid, entry) in ctor_client_entries {
325        if let Ok(Some(key)) = keymgr.get_entry::<HsClientDescEncKeypair>(&entry) {
326            let key_exists = keymgr
327                .get_from::<HsClientDescEncKeypair>(
328                    &HsClientDescEncKeypairSpecifier::new(hsid),
329                    &arti_keystore_id,
330                )?
331                .is_some();
332            let proceed = if args.batch || !key_exists {
333                true
334            } else {
335                let p = format!(
336                    "Found key in the primary keystore for service {}, do you want to replace it? ",
337                    hsid.display_redacted()
338                );
339                prompt(&p)?
340            };
341            if proceed {
342                let res = keymgr.insert(
343                    key,
344                    &HsClientDescEncKeypairSpecifier::new(hsid),
345                    (&arti_keystore_id).into(),
346                    true,
347                );
348                if let Err(e) = res {
349                    eprintln!(
350                        "Failed to insert key for service {}: {e}",
351                        hsid.display_redacted()
352                    );
353                }
354            }
355        }
356    }
357
358    Ok(())
359}
360
361/// Prompt the user for an onion address.
362fn get_onion_address(args: &CommonArgs) -> Result<HsId, anyhow::Error> {
363    let mut addr = String::new();
364    if !args.batch {
365        print!("Enter an onion address: ");
366        io::stdout().flush().map_err(|e| anyhow!(e))?;
367    };
368    io::stdin().read_line(&mut addr).map_err(|e| anyhow!(e))?;
369
370    HsId::from_str(addr.trim_end()).map_err(|e| anyhow!(e))
371}
372
373/// Helper function for `migrate_ctor_keys`.
374///
375/// Parses and returns the client keys from the CTor keystore identified by `--from` CLI flag.
376/// Detects if there is a clash (different keys for the same hidden service within
377/// the CTor keystore).
378/// Such a situation is invalid, as each service must have a unique key.
379/// If a clash is found, an error is returned.
380/// If no clashes are detected, returns a `HashMap` of keystore entries, keyed
381/// by hidden service identifier.
382#[cfg(feature = "onion-service-cli-extra")]
383fn read_ctor_keys<'a>(
384    entries: &[KeystoreEntryResult<KeystoreEntry<'a>>],
385    args: &CTorMigrateArgs,
386) -> Result<HashMap<HsId, KeystoreEntry<'a>>> {
387    let mut ctor_client_entries = HashMap::new();
388    for entry in entries.iter().flatten() {
389        if let KeyPath::CTor(CTorPath::HsClientDescEncKeypair { hs_id }) = entry.key_path() {
390            match ctor_client_entries.entry(*hs_id) {
391                Entry::Occupied(_) => {
392                    return Err(anyhow!(
393                        "Invalid C Tor keystore (multiple keys exist for service {})",
394                        hs_id.display_redacted()
395                    ));
396                }
397                Entry::Vacant(v) => {
398                    v.insert(entry.clone());
399                }
400            }
401        };
402    }
403
404    if ctor_client_entries.is_empty() {
405        return Err(anyhow!(
406            "No CTor client keys found in keystore {}",
407            args.from,
408        ));
409    }
410
411    Ok(ctor_client_entries)
412}