1
//! The `hsc` subcommand.
2

            
3
use crate::subcommands::prompt;
4
use crate::{Result, TorClient};
5

            
6
use anyhow::{Context, anyhow};
7
use arti_client::{HsClientDescEncKey, HsId, InertTorClient, KeystoreSelector, TorClientConfig};
8
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
9
use safelog::DisplayRedacted;
10
use tor_rtcompat::Runtime;
11
#[cfg(feature = "onion-service-cli-extra")]
12
use {
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

            
19
use std::fs::OpenOptions;
20
use std::io::{self, Write};
21
use std::str::FromStr;
22

            
23
/// The hsc subcommands the arti CLI will be augmented with.
24
#[derive(Parser, Debug)]
25
pub(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)]
33
pub(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)]
55
pub(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)]
71
enum 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)]
81
pub(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)]
102
enum 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)]
112
pub(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)]
129
pub(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)]
141
pub(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)]
153
pub(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")]
162
pub(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.
176
132
pub(crate) fn run<R: Runtime>(
177
132
    runtime: R,
178
132
    hsc_matches: &ArgMatches,
179
132
    config: &TorClientConfig,
180
132
) -> Result<()> {
181
    use KeyType::*;
182

            
183
132
    let subcommand =
184
132
        HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
185
132
    let client = TorClient::with_runtime(runtime)
186
132
        .config(config.clone())
187
132
        .create_inert()?;
188

            
189
132
    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
96
        HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
199
        #[cfg(feature = "onion-service-cli-extra")]
200
36
        HscSubcommand::CTorMigrate(args) => migrate_ctor_keys(&args, &client),
201
    }
202
132
}
203

            
204
/// Run the `hsc key` subcommand
205
96
fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
206
96
    match subcommand {
207
84
        KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
208
6
        KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
209
6
        KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
210
    }
211
96
}
212

            
213
/// Run the `hsc prepare-stealth-mode-key` subcommand.
214
84
fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
215
84
    let addr = get_onion_address(&args.common)?;
216
84
    let key = match args.generate {
217
        GenerateKey::IfNeeded => {
218
            // TODO: consider using get_or_generate in generate_service_discovery_key
219
18
            client
220
18
                .get_service_discovery_key(addr)?
221
18
                .map(Ok)
222
21
                .unwrap_or_else(|| {
223
18
                    client.generate_service_discovery_key(KeystoreSelector::Primary, addr)
224
18
                })?
225
        }
226
66
        GenerateKey::No => match client.get_service_discovery_key(addr)? {
227
42
            Some(key) => key,
228
            None => {
229
24
                return Err(anyhow!(
230
24
                    "Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
231
24
                ));
232
            }
233
        },
234
    };
235

            
236
60
    display_service_discovery_key(&args.keygen, &key)
237
84
}
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)
244
66
fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
245
    // Output the public key to the specified file, or to stdout.
246
66
    match args.output.as_str() {
247
66
        "-" => 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
66
    Ok(())
274
66
}
275

            
276
/// Write the public part of `key` to `f`.
277
66
fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
278
66
    writeln!(f, "{}", key)?;
279
66
    Ok(())
280
66
}
281

            
282
/// Run the `hsc rotate-key` subcommand.
283
6
fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
284
6
    let addr = get_onion_address(&args.common)?;
285
6
    let msg = format!(
286
6
        "rotate client restricted discovery key for {}?",
287
6
        addr.display_unredacted()
288
    );
289
6
    if !args.common.batch && !prompt(&msg)? {
290
        return Ok(());
291
6
    }
292

            
293
6
    let key = client.rotate_service_discovery_key(KeystoreSelector::default(), addr)?;
294

            
295
6
    display_service_discovery_key(&args.keygen, &key)
296
6
}
297

            
298
/// Run the `hsc remove-key` subcommand.
299
6
fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
300
6
    let addr = get_onion_address(&args.common)?;
301
6
    let msg = format!(
302
6
        "remove client restricted discovery key for {}?",
303
6
        addr.display_unredacted()
304
    );
305
6
    if !args.common.batch && !prompt(&msg)? {
306
        return Ok(());
307
6
    }
308

            
309
6
    let _key = client.remove_service_discovery_key(KeystoreSelector::default(), addr)?;
310

            
311
6
    Ok(())
312
6
}
313

            
314
/// Run the `hsc ctor-migrate` subcommand.
315
#[cfg(feature = "onion-service-cli-extra")]
316
36
fn migrate_ctor_keys(args: &CTorMigrateArgs, client: &InertTorClient) -> Result<()> {
317
36
    let keymgr = client.keymgr()?;
318
36
    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
18
    let arti_keystore_id = KeystoreId::from_str("arti")
323
18
        .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
324
54
    for (hsid, entry) in ctor_client_entries {
325
36
        if let Ok(Some(key)) = keymgr.get_entry::<HsClientDescEncKeypair>(&entry) {
326
36
            let key_exists = keymgr
327
36
                .get_from::<HsClientDescEncKeypair>(
328
36
                    &HsClientDescEncKeypairSpecifier::new(hsid),
329
36
                    &arti_keystore_id,
330
                )?
331
36
                .is_some();
332
36
            let proceed = if args.batch || !key_exists {
333
36
                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
36
            if proceed {
342
36
                let res = keymgr.insert(
343
36
                    key,
344
36
                    &HsClientDescEncKeypairSpecifier::new(hsid),
345
36
                    (&arti_keystore_id).into(),
346
                    true,
347
                );
348
36
                if let Err(e) = res {
349
                    eprintln!(
350
                        "Failed to insert key for service {}: {e}",
351
                        hsid.display_redacted()
352
                    );
353
36
                }
354
            }
355
        }
356
    }
357

            
358
18
    Ok(())
359
36
}
360

            
361
/// Prompt the user for an onion address.
362
96
fn get_onion_address(args: &CommonArgs) -> Result<HsId, anyhow::Error> {
363
96
    let mut addr = String::new();
364
96
    if !args.batch {
365
        print!("Enter an onion address: ");
366
        io::stdout().flush().map_err(|e| anyhow!(e))?;
367
96
    };
368
96
    io::stdin().read_line(&mut addr).map_err(|e| anyhow!(e))?;
369

            
370
96
    HsId::from_str(addr.trim_end()).map_err(|e| anyhow!(e))
371
96
}
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")]
383
36
fn read_ctor_keys<'a>(
384
36
    entries: &[KeystoreEntryResult<KeystoreEntry<'a>>],
385
36
    args: &CTorMigrateArgs,
386
36
) -> Result<HashMap<HsId, KeystoreEntry<'a>>> {
387
36
    let mut ctor_client_entries = HashMap::new();
388
54
    for entry in entries.iter().flatten() {
389
54
        if let KeyPath::CTor(CTorPath::HsClientDescEncKeypair { hs_id }) = entry.key_path() {
390
54
            match ctor_client_entries.entry(*hs_id) {
391
                Entry::Occupied(_) => {
392
6
                    return Err(anyhow!(
393
6
                        "Invalid C Tor keystore (multiple keys exist for service {})",
394
6
                        hs_id.display_redacted()
395
6
                    ));
396
                }
397
48
                Entry::Vacant(v) => {
398
48
                    v.insert(entry.clone());
399
48
                }
400
            }
401
        };
402
    }
403

            
404
30
    if ctor_client_entries.is_empty() {
405
12
        return Err(anyhow!(
406
12
            "No CTor client keys found in keystore {}",
407
12
            args.from,
408
12
        ));
409
18
    }
410

            
411
18
    Ok(ctor_client_entries)
412
36
}