1
//! The `hss` subcommand.
2

            
3
#[cfg(feature = "onion-service-cli-extra")]
4
use {
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

            
12
use anyhow::anyhow;
13
use arti_client::{InertTorClient, TorClientConfig};
14
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
15
use safelog::DisplayRedacted;
16
use tor_hsservice::{HsId, HsNickname, OnionService};
17
use tor_rtcompat::Runtime;
18

            
19
use crate::{ArtiConfig, Result, TorClient};
20

            
21
/// The hss subcommands the arti CLI will be augmented with.
22
#[derive(Parser, Debug)]
23
pub(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)]
30
pub(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)]
42
pub(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)]
71
pub(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")]
84
pub(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)]
93
enum 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)]
103
enum 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)]
110
pub(crate) struct CommonArgs {
111
    /// The nickname of the service
112
    #[arg(short, long)]
113
    nickname: HsNickname,
114
}
115

            
116
/// Run the `hss` subcommand.
117
114
pub(crate) fn run<R: Runtime>(
118
114
    runtime: R,
119
114
    hss_matches: &ArgMatches,
120
114
    config: &ArtiConfig,
121
114
    client_config: &TorClientConfig,
122
114
) -> Result<()> {
123
114
    let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
124

            
125
114
    match hss.command {
126
54
        HssSubcommand::OnionAddress(args) => {
127
54
            run_onion_address(&hss.common, &args, config, client_config)
128
        }
129
        #[cfg(feature = "onion-service-cli-extra")]
130
60
        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
114
}
139

            
140
/// Create the OnionService configured with `nickname`.
141
54
fn create_svc(
142
54
    nickname: &HsNickname,
143
54
    config: &ArtiConfig,
144
54
    client_config: &TorClientConfig,
145
54
) -> Result<OnionService> {
146
54
    let Some(svc_config) = config
147
54
        .onion_services
148
54
        .iter()
149
75
        .find(|(n, _)| *n == nickname)
150
62
        .map(|(_, cfg)| cfg.svc_cfg.clone())
151
    else {
152
6
        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
48
        TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
163
48
            client_config,
164
48
            svc_config,
165
        )?,
166
    )
167
54
}
168

            
169
/// Display the onion address, if any, of the specified service.
170
48
fn 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
48
    if let Some(onion) = hsid {
174
42
        println!("{}", onion.display_unredacted());
175
42
    } else {
176
6
        return Err(anyhow!(
177
6
            "Service {nickname} does not exist, or does not have an K_hsid yet"
178
6
        ));
179
    }
180

            
181
42
    Ok(())
182
48
}
183

            
184
/// Run the `hss onion-address` subcommand.
185
54
fn onion_address(
186
54
    args: &CommonArgs,
187
54
    config: &ArtiConfig,
188
54
    client_config: &TorClientConfig,
189
54
) -> Result<()> {
190
54
    let onion_svc = create_svc(&args.nickname, config, client_config)?;
191
48
    let hsid = onion_svc.onion_address();
192
48
    display_onion_address(&args.nickname, hsid)?;
193

            
194
42
    Ok(())
195
54
}
196

            
197
/// Run the `hss onion-address` subcommand.
198
fn 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.
216
54
fn run_onion_address(
217
54
    args: &CommonArgs,
218
54
    get_key_args: &OnionAddressArgs,
219
54
    config: &ArtiConfig,
220
54
    client_config: &TorClientConfig,
221
54
) -> Result<()> {
222
54
    match get_key_args.generate {
223
54
        GenerateKey::No => onion_address(args, config, client_config),
224
        GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
225
    }
226
54
}
227

            
228
/// Run the `hss ctor-migrate` subcommand.
229
#[cfg(feature = "onion-service-cli-extra")]
230
60
fn run_migrate<R: Runtime>(
231
60
    runtime: R,
232
60
    client_config: &TorClientConfig,
233
60
    migrate_args: &CTorMigrateArgs,
234
60
    args: &CommonArgs,
235
60
) -> Result<()> {
236
60
    let ctor_keystore_id = find_ctor_keystore(client_config, args)?;
237

            
238
36
    let inert_client = TorClient::with_runtime(runtime)
239
36
        .config(client_config.clone())
240
36
        .create_inert()?;
241

            
242
36
    migrate_ctor_keys(migrate_args, args, &inert_client, &ctor_keystore_id)
243
60
}
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")]
257
36
fn migrate_ctor_keys(
258
36
    migrate_args: &CTorMigrateArgs,
259
36
    args: &CommonArgs,
260
36
    client: &InertTorClient,
261
36
    ctor_keystore_id: &KeystoreId,
262
36
) -> Result<()> {
263
36
    let keymgr = client.keymgr()?;
264
36
    let nickname = &args.nickname;
265
36
    let id_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
266
    // If no CTor identity key is found the migration can't continue.
267
36
    let ctor_id_key = keymgr
268
36
        .get_from::<HsIdKeypair>(&id_key_spec, ctor_keystore_id)?
269
36
        .ok_or_else(|| anyhow!("No identity key found in the provided C Tor keystore."))?;
270

            
271
36
    let arti_pat = tor_keymgr::KeyPathPattern::Arti(format!("hss/{}/**/*", nickname));
272
36
    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
36
    let arti_keystore_id = KeystoreId::from_str("arti")
277
36
        .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
278

            
279
36
    let is_empty = arti_entries.is_empty();
280

            
281
36
    if !is_empty {
282
52
        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
48
            keymgr
288
48
                .describe(k.key_path())
289
48
                .is_some_and(|info| info.role() == "ks_hs_id")
290
48
        });
291
24
        if let Some(arti_id_entry) = arti_id_entry_opt {
292
24
            let arti_id_key: HsIdKeypair = match keymgr.get_entry(arti_id_entry)? {
293
24
                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
24
            if arti_id_key.as_ref().public() == ctor_id_key.as_ref().public() {
301
12
                return Err(anyhow!("Service {nickname} was already migrated."));
302
12
            }
303
        }
304
12
    }
305

            
306
24
    if is_empty || migrate_args.batch || prompt(&build_prompt(&arti_entries))? {
307
18
        remove_arti_entries(keymgr, &arti_entries);
308
18
        keymgr.insert(ctor_id_key, &id_key_spec, (&arti_keystore_id).into(), true)?;
309
6
    } else {
310
6
        println!("Aborted.");
311
6
    }
312

            
313
24
    Ok(())
314
36
}
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")]
322
60
fn find_ctor_keystore(client_config: &TorClientConfig, args: &CommonArgs) -> Result<KeystoreId> {
323
60
    let keystore_config = client_config.keystore();
324
60
    let ctor_services = keystore_config.ctor().services();
325
60
    if ctor_services.is_empty() {
326
12
        return Err(anyhow!("No CTor keystore are configured."));
327
48
    }
328

            
329
48
    let Some((_, service_config)) = ctor_services
330
48
        .iter()
331
56
        .find(|(hs_nick, _)| *hs_nick == &args.nickname)
332
    else {
333
12
        return Err(anyhow!(
334
12
            "The service identified using `--nickname {}` is not configured with any recognized CTor keystore.",
335
12
            &args.nickname,
336
12
        ));
337
    };
338

            
339
36
    Ok(service_config.id().clone())
340
60
}
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")]
346
18
fn remove_arti_entries(keymgr: &KeyMgr, arti_entries: &Vec<KeystoreEntry<'_>>) {
347
72
    for entry in arti_entries {
348
54
        if let Err(e) = keymgr.remove_entry(entry) {
349
            eprintln!("Failed to remove entry {} ({e})", entry.key_path(),);
350
54
        }
351
    }
352
18
}
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")]
357
6
fn build_prompt(entries: &Vec<KeystoreEntry<'_>>) -> String {
358
6
    let mut p = "WARNING: the following keys will be deleted\n".to_string();
359
54
    for k in entries.iter() {
360
54
        p.push('\t');
361
54
        p.push_str(&k.key_path().to_string());
362
54
        p.push('\n');
363
54
    }
364
6
    p.push('\n');
365
6
    p.push_str("Proceed anyway?");
366
6
    p
367
6
}