Skip to main content

arti/subcommands/
hss.rs

1//! The `hss` subcommand.
2
3#[cfg(feature = "onion-service-cli-extra")]
4use {
5    crate::subcommands::prompt,
6    std::str::FromStr,
7    tor_hscrypto::pk::HsIdKeypair,
8    tor_hsservice::HsIdKeypairSpecifier,
9    tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreId},
10};
11
12use anyhow::anyhow;
13use arti_client::{InertTorClient, TorClientConfig};
14use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
15use safelog::DisplayRedacted;
16use tor_hsservice::{HsId, HsNickname, OnionService};
17use tor_rtcompat::Runtime;
18
19use crate::{ArtiConfig, Result, TorClient};
20
21/// The hss subcommands the arti CLI will be augmented with.
22#[derive(Parser, Debug)]
23pub(crate) enum HssSubcommands {
24    /// Run state management commands for an Arti hidden service.
25    Hss(Hss),
26}
27
28/// The `hss` subcommand and args.
29#[derive(Debug, Parser)]
30pub(crate) struct Hss {
31    /// Arguments shared by all hss subcommands.
32    #[command(flatten)]
33    common: CommonArgs,
34
35    /// The `hss` subcommand to run.
36    #[command(subcommand)]
37    command: HssSubcommand,
38}
39
40/// The `hss` subcommand.
41#[derive(Subcommand, Debug, Clone)]
42pub(crate) enum HssSubcommand {
43    /// Print the .onion address of a hidden service
44    OnionAddress(OnionAddressArgs),
45
46    /// (Deprecated) Print the .onion address of a hidden service
47    #[command(hide = true)] // This hides the command from the help message
48    OnionName(OnionAddressArgs),
49
50    /// Migrate the identity key of a specified hidden service from a
51    /// CTor-compatible keystore to the native Arti keystore.
52    ///
53    /// If the service with the specified nickname
54    /// already has some keys in the Arti keystore,
55    /// they will be deleted as part of the migration,
56    /// its identity key being replaced with the identity
57    /// key obtained from the C Tor keystore.
58    ///
59    /// Authorized restricted discovery keys (authorized_clients)
60    /// will not be migrated as part of this process.
61    ///
62    /// Important: This tool should only be used when no other process
63    /// is accessing either keystore.
64    #[cfg(feature = "onion-service-cli-extra")]
65    #[command(name = "ctor-migrate")]
66    CTorMigrate(CTorMigrateArgs),
67}
68
69/// The arguments of the [`OnionAddress`](HssSubcommand::OnionAddress) subcommand.
70#[derive(Debug, Clone, Args)]
71pub(crate) struct OnionAddressArgs {
72    /// Whether to generate the key if it is missing
73    #[arg(
74        long,
75        default_value_t = GenerateKey::No,
76        value_enum
77    )]
78    generate: GenerateKey,
79}
80
81/// The arguments of the [`CTorMigrate`](HssSubcommand::CTorMigrate) subcommand.
82#[derive(Debug, Clone, Args)]
83#[cfg(feature = "onion-service-cli-extra")]
84pub(crate) struct CTorMigrateArgs {
85    /// With this flag active no prompt will be shown
86    /// and no confirmation will be asked
87    #[arg(long, short, default_value_t = false)]
88    batch: bool,
89}
90
91/// Whether to generate the key if missing.
92#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
93enum GenerateKey {
94    /// Do not generate the key.
95    #[default]
96    No,
97    /// Generate the key if it's missing.
98    IfNeeded,
99}
100
101/// A type of key
102#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
103enum KeyType {
104    /// The identity key of the service
105    OnionAddress,
106}
107
108/// The arguments shared by all [`HssSubcommand`]s.
109#[derive(Debug, Clone, Args)]
110pub(crate) struct CommonArgs {
111    /// The nickname of the service
112    #[arg(short, long)]
113    nickname: HsNickname,
114}
115
116/// Run the `hss` subcommand.
117pub(crate) fn run<R: Runtime>(
118    runtime: R,
119    hss_matches: &ArgMatches,
120    config: &ArtiConfig,
121    client_config: &TorClientConfig,
122) -> Result<()> {
123    let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
124
125    match hss.command {
126        HssSubcommand::OnionAddress(args) => {
127            run_onion_address(&hss.common, &args, config, client_config)
128        }
129        #[cfg(feature = "onion-service-cli-extra")]
130        HssSubcommand::CTorMigrate(args) => run_migrate(runtime, client_config, &args, &hss.common),
131        HssSubcommand::OnionName(args) => {
132            eprintln!(
133                "warning: using deprecated command 'onion-name', (hint: use 'onion-address' instead)"
134            );
135            run_onion_address(&hss.common, &args, config, client_config)
136        }
137    }
138}
139
140/// Create the OnionService configured with `nickname`.
141fn create_svc(
142    nickname: &HsNickname,
143    config: &ArtiConfig,
144    client_config: &TorClientConfig,
145) -> Result<OnionService> {
146    let Some(svc_config) = config
147        .onion_services
148        .iter()
149        .find(|(n, _)| *n == nickname)
150        .map(|(_, cfg)| cfg.svc_cfg.clone())
151    else {
152        return Err(anyhow!("Service {nickname} is not configured"));
153    };
154
155    // TODO: PreferredRuntime was arbitrarily chosen and is entirely unused
156    // (we have to specify a concrete type for the runtime when calling
157    // TorClient::create_onion_service).
158    //
159    // Maybe this suggests TorClient is not the right place for
160    // create_onion_service()
161    Ok(
162        TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
163            client_config,
164            svc_config,
165        )?,
166    )
167}
168
169/// Display the onion address, if any, of the specified service.
170fn display_onion_address(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
171    // TODO: instead of the printlns here, we should have a formatter type that
172    // decides how to display the output
173    if let Some(onion) = hsid {
174        println!("{}", onion.display_unredacted());
175    } else {
176        return Err(anyhow!(
177            "Service {nickname} does not exist, or does not have an K_hsid yet"
178        ));
179    }
180
181    Ok(())
182}
183
184/// Run the `hss onion-address` subcommand.
185fn onion_address(
186    args: &CommonArgs,
187    config: &ArtiConfig,
188    client_config: &TorClientConfig,
189) -> Result<()> {
190    let onion_svc = create_svc(&args.nickname, config, client_config)?;
191    let hsid = onion_svc.onion_address();
192    display_onion_address(&args.nickname, hsid)?;
193
194    Ok(())
195}
196
197/// Run the `hss onion-address` subcommand.
198fn get_or_generate_onion_address(
199    args: &CommonArgs,
200    config: &ArtiConfig,
201    client_config: &TorClientConfig,
202) -> Result<()> {
203    let svc = create_svc(&args.nickname, config, client_config)?;
204    let hsid = svc.onion_address();
205    match hsid {
206        Some(hsid) => display_onion_address(&args.nickname, Some(hsid)),
207        None => {
208            let selector = Default::default();
209            let hsid = svc.generate_identity_key(selector)?;
210            display_onion_address(&args.nickname, Some(hsid))
211        }
212    }
213}
214
215/// Run the `hss onion-address` subcommand.
216fn run_onion_address(
217    args: &CommonArgs,
218    get_key_args: &OnionAddressArgs,
219    config: &ArtiConfig,
220    client_config: &TorClientConfig,
221) -> Result<()> {
222    match get_key_args.generate {
223        GenerateKey::No => onion_address(args, config, client_config),
224        GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
225    }
226}
227
228/// Run the `hss ctor-migrate` subcommand.
229#[cfg(feature = "onion-service-cli-extra")]
230fn run_migrate<R: Runtime>(
231    runtime: R,
232    client_config: &TorClientConfig,
233    migrate_args: &CTorMigrateArgs,
234    args: &CommonArgs,
235) -> Result<()> {
236    let ctor_keystore_id = find_ctor_keystore(client_config, args)?;
237
238    let inert_client = TorClient::with_runtime(runtime)
239        .config(client_config.clone())
240        .create_inert()?;
241
242    migrate_ctor_keys(migrate_args, args, &inert_client, &ctor_keystore_id)
243}
244
245/// Migrate the keys of the specified C Tor service to the Arti keystore.
246///
247/// Performs key migration for the service identified by the [`HsNickname`] provided
248/// via `--nickname`, copying keys from the CTor keystore configured for the service
249/// to the default Arti native keystore.
250///
251/// If the service with the specified nickname had some keys in the Arti keystore
252/// prior to the migration, those keys will be removed.
253///
254/// If `args.batch` is false, the user will be prompted for the deletion of
255/// the existing entries from the original Arti keystore.
256#[cfg(feature = "onion-service-cli-extra")]
257fn migrate_ctor_keys(
258    migrate_args: &CTorMigrateArgs,
259    args: &CommonArgs,
260    client: &InertTorClient,
261    ctor_keystore_id: &KeystoreId,
262) -> Result<()> {
263    let keymgr = client.keymgr()?;
264    let nickname = &args.nickname;
265    let id_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
266    // If no CTor identity key is found the migration can't continue.
267    let ctor_id_key = keymgr
268        .get_from::<HsIdKeypair>(&id_key_spec, ctor_keystore_id)?
269        .ok_or_else(|| anyhow!("No identity key found in the provided C Tor keystore."))?;
270
271    let arti_pat = tor_keymgr::KeyPathPattern::Arti(format!("hss/{}/**/*", nickname));
272    let arti_entries = keymgr.list_matching(&arti_pat)?;
273
274    // NOTE: Currently, there can only be one `ArtiNativeKeystore` with a hard-coded
275    // `KeystoreId`, which is used as the `primary_keystore`.
276    let arti_keystore_id = KeystoreId::from_str("arti")
277        .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
278
279    let is_empty = arti_entries.is_empty();
280
281    if !is_empty {
282        let arti_id_entry_opt = arti_entries.iter().find(|k| {
283            // TODO: this relies on the stringly-typed info.role()
284            // to find the identity key. We should consider exporting
285            // HsIdKeypairSpecifierPattern from tor-hsservice,
286            // and using it here.
287            keymgr
288                .describe(k.key_path())
289                .is_some_and(|info| info.role() == "ks_hs_id")
290        });
291        if let Some(arti_id_entry) = arti_id_entry_opt {
292            let arti_id_key: HsIdKeypair = match keymgr.get_entry(arti_id_entry)? {
293                Some(aik) => aik,
294                None => {
295                    return Err(anyhow!(
296                        "Identity key disappeared during migration (is another process using the keystore?)"
297                    ));
298                }
299            };
300            if arti_id_key.as_ref().public() == ctor_id_key.as_ref().public() {
301                return Err(anyhow!("Service {nickname} was already migrated."));
302            }
303        }
304    }
305
306    if is_empty || migrate_args.batch || prompt(&build_prompt(&arti_entries))? {
307        remove_arti_entries(keymgr, &arti_entries);
308        keymgr.insert(ctor_id_key, &id_key_spec, (&arti_keystore_id).into(), true)?;
309    } else {
310        println!("Aborted.");
311    }
312
313    Ok(())
314}
315
316/// Checks if the service identified by the [`HsNickname`] provided by the user
317/// is configured with any of the recognized CTor keystores.
318///
319/// Returns different errors messages to indicate specific failure conditions if the
320/// procedure cannot continue, `Ok(())` otherwise.
321#[cfg(feature = "onion-service-cli-extra")]
322fn find_ctor_keystore(client_config: &TorClientConfig, args: &CommonArgs) -> Result<KeystoreId> {
323    let keystore_config = client_config.keystore();
324    let ctor_services = keystore_config.ctor().services();
325    if ctor_services.is_empty() {
326        return Err(anyhow!("No CTor keystore are configured."));
327    }
328
329    let Some((_, service_config)) = ctor_services
330        .iter()
331        .find(|(hs_nick, _)| *hs_nick == &args.nickname)
332    else {
333        return Err(anyhow!(
334            "The service identified using `--nickname {}` is not configured with any recognized CTor keystore.",
335            &args.nickname,
336        ));
337    };
338
339    Ok(service_config.id().clone())
340}
341
342/// Helper function for `migrate_ctor_keys`.
343/// Removes all the Arti keystore entries provided.
344/// Prints an error for each failed removal attempt.
345#[cfg(feature = "onion-service-cli-extra")]
346fn remove_arti_entries(keymgr: &KeyMgr, arti_entries: &Vec<KeystoreEntry<'_>>) {
347    for entry in arti_entries {
348        if let Err(e) = keymgr.remove_entry(entry) {
349            eprintln!("Failed to remove entry {} ({e})", entry.key_path(),);
350        }
351    }
352}
353
354/// Helper function for `migrate_ctor_keys`.
355/// Builds a prompt that will be passed to the [`prompt`] function.
356#[cfg(feature = "onion-service-cli-extra")]
357fn build_prompt(entries: &Vec<KeystoreEntry<'_>>) -> String {
358    let mut p = "WARNING: the following keys will be deleted\n".to_string();
359    for k in entries.iter() {
360        p.push('\t');
361        p.push_str(&k.key_path().to_string());
362        p.push('\n');
363    }
364    p.push('\n');
365    p.push_str("Proceed anyway?");
366    p
367}