Lines
85 %
Functions
41.67 %
Branches
100 %
//! The `hss` subcommand.
#[cfg(feature = "onion-service-cli-extra")]
use {
crate::subcommands::prompt,
std::str::FromStr,
tor_hscrypto::pk::HsIdKeypair,
tor_hsservice::HsIdKeypairSpecifier,
tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreId},
};
use anyhow::anyhow;
use arti_client::{InertTorClient, TorClientConfig};
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
use safelog::DisplayRedacted;
use tor_hsservice::{HsId, HsNickname, OnionService};
use tor_rtcompat::Runtime;
use crate::{ArtiConfig, Result, TorClient};
/// The hss subcommands the arti CLI will be augmented with.
#[derive(Parser, Debug)]
pub(crate) enum HssSubcommands {
/// Run state management commands for an Arti hidden service.
Hss(Hss),
}
/// The `hss` subcommand and args.
#[derive(Debug, Parser)]
pub(crate) struct Hss {
/// Arguments shared by all hss subcommands.
#[command(flatten)]
common: CommonArgs,
/// The `hss` subcommand to run.
#[command(subcommand)]
command: HssSubcommand,
/// The `hss` subcommand.
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum HssSubcommand {
/// Print the .onion address of a hidden service
OnionAddress(OnionAddressArgs),
/// (Deprecated) Print the .onion address of a hidden service
#[command(hide = true)] // This hides the command from the help message
OnionName(OnionAddressArgs),
/// Migrate the identity key of a specified hidden service from a
/// CTor-compatible keystore to the native Arti keystore.
///
/// If the service with the specified nickname
/// already has some keys in the Arti keystore,
/// they will be deleted as part of the migration,
/// its identity key being replaced with the identity
/// key obtained from the C Tor keystore.
/// Authorized restricted discovery keys (authorized_clients)
/// will not be migrated as part of this process.
/// Important: This tool should only be used when no other process
/// is accessing either keystore.
#[command(name = "ctor-migrate")]
CTorMigrate(CTorMigrateArgs),
/// The arguments of the [`OnionAddress`](HssSubcommand::OnionAddress) subcommand.
#[derive(Debug, Clone, Args)]
pub(crate) struct OnionAddressArgs {
/// Whether to generate the key if it is missing
#[arg(
long,
default_value_t = GenerateKey::No,
value_enum
)]
generate: GenerateKey,
/// The arguments of the [`CTorMigrate`](HssSubcommand::CTorMigrate) subcommand.
pub(crate) struct CTorMigrateArgs {
/// With this flag active no prompt will be shown
/// and no confirmation will be asked
#[arg(long, short, default_value_t = false)]
batch: bool,
/// Whether to generate the key if missing.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
enum GenerateKey {
/// Do not generate the key.
#[default]
No,
/// Generate the key if it's missing.
IfNeeded,
/// A type of key
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
enum KeyType {
/// The identity key of the service
OnionAddress,
/// The arguments shared by all [`HssSubcommand`]s.
pub(crate) struct CommonArgs {
/// The nickname of the service
#[arg(short, long)]
nickname: HsNickname,
/// Run the `hss` subcommand.
pub(crate) fn run<R: Runtime>(
runtime: R,
hss_matches: &ArgMatches,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
match hss.command {
HssSubcommand::OnionAddress(args) => {
run_onion_address(&hss.common, &args, config, client_config)
HssSubcommand::CTorMigrate(args) => run_migrate(runtime, client_config, &args, &hss.common),
HssSubcommand::OnionName(args) => {
eprintln!(
"warning: using deprecated command 'onion-name', (hint: use 'onion-address' instead)"
);
/// Create the OnionService configured with `nickname`.
fn create_svc(
nickname: &HsNickname,
) -> Result<OnionService> {
let Some(svc_config) = config
.onion_services
.iter()
.find(|(n, _)| *n == nickname)
.map(|(_, cfg)| cfg.svc_cfg.clone())
else {
return Err(anyhow!("Service {nickname} is not configured"));
// TODO: PreferredRuntime was arbitrarily chosen and is entirely unused
// (we have to specify a concrete type for the runtime when calling
// TorClient::create_onion_service).
//
// Maybe this suggests TorClient is not the right place for
// create_onion_service()
Ok(
TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
client_config,
svc_config,
)?,
)
/// Display the onion address, if any, of the specified service.
fn display_onion_address(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
// TODO: instead of the printlns here, we should have a formatter type that
// decides how to display the output
if let Some(onion) = hsid {
println!("{}", onion.display_unredacted());
} else {
return Err(anyhow!(
"Service {nickname} does not exist, or does not have an K_hsid yet"
));
Ok(())
/// Run the `hss onion-address` subcommand.
fn onion_address(
args: &CommonArgs,
let onion_svc = create_svc(&args.nickname, config, client_config)?;
let hsid = onion_svc.onion_address();
display_onion_address(&args.nickname, hsid)?;
fn get_or_generate_onion_address(
let svc = create_svc(&args.nickname, config, client_config)?;
let hsid = svc.onion_address();
match hsid {
Some(hsid) => display_onion_address(&args.nickname, Some(hsid)),
None => {
let selector = Default::default();
let hsid = svc.generate_identity_key(selector)?;
display_onion_address(&args.nickname, Some(hsid))
fn run_onion_address(
get_key_args: &OnionAddressArgs,
match get_key_args.generate {
GenerateKey::No => onion_address(args, config, client_config),
GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
/// Run the `hss ctor-migrate` subcommand.
fn run_migrate<R: Runtime>(
migrate_args: &CTorMigrateArgs,
let ctor_keystore_id = find_ctor_keystore(client_config, args)?;
let inert_client = TorClient::with_runtime(runtime)
.config(client_config.clone())
.create_inert()?;
migrate_ctor_keys(migrate_args, args, &inert_client, &ctor_keystore_id)
/// Migrate the keys of the specified C Tor service to the Arti keystore.
/// Performs key migration for the service identified by the [`HsNickname`] provided
/// via `--nickname`, copying keys from the CTor keystore configured for the service
/// to the default Arti native keystore.
/// If the service with the specified nickname had some keys in the Arti keystore
/// prior to the migration, those keys will be removed.
/// If `args.batch` is false, the user will be prompted for the deletion of
/// the existing entries from the original Arti keystore.
fn migrate_ctor_keys(
client: &InertTorClient,
ctor_keystore_id: &KeystoreId,
let keymgr = client.keymgr()?;
let nickname = &args.nickname;
let id_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
// If no CTor identity key is found the migration can't continue.
let ctor_id_key = keymgr
.get_from::<HsIdKeypair>(&id_key_spec, ctor_keystore_id)?
.ok_or_else(|| anyhow!("No identity key found in the provided C Tor keystore."))?;
let arti_pat = tor_keymgr::KeyPathPattern::Arti(format!("hss/{}/**/*", nickname));
let arti_entries = keymgr.list_matching(&arti_pat)?;
// NOTE: Currently, there can only be one `ArtiNativeKeystore` with a hard-coded
// `KeystoreId`, which is used as the `primary_keystore`.
let arti_keystore_id = KeystoreId::from_str("arti")
.map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
let is_empty = arti_entries.is_empty();
if !is_empty {
let arti_id_entry_opt = arti_entries.iter().find(|k| {
// TODO: this relies on the stringly-typed info.role()
// to find the identity key. We should consider exporting
// HsIdKeypairSpecifierPattern from tor-hsservice,
// and using it here.
keymgr
.describe(k.key_path())
.is_some_and(|info| info.role() == "ks_hs_id")
});
if let Some(arti_id_entry) = arti_id_entry_opt {
let arti_id_key: HsIdKeypair = match keymgr.get_entry(arti_id_entry)? {
Some(aik) => aik,
"Identity key disappeared during migration (is another process using the keystore?)"
if arti_id_key.as_ref().public() == ctor_id_key.as_ref().public() {
return Err(anyhow!("Service {nickname} was already migrated."));
if is_empty || migrate_args.batch || prompt(&build_prompt(&arti_entries))? {
remove_arti_entries(keymgr, &arti_entries);
keymgr.insert(ctor_id_key, &id_key_spec, (&arti_keystore_id).into(), true)?;
println!("Aborted.");
/// Checks if the service identified by the [`HsNickname`] provided by the user
/// is configured with any of the recognized CTor keystores.
/// Returns different errors messages to indicate specific failure conditions if the
/// procedure cannot continue, `Ok(())` otherwise.
fn find_ctor_keystore(client_config: &TorClientConfig, args: &CommonArgs) -> Result<KeystoreId> {
let keystore_config = client_config.keystore();
let ctor_services = keystore_config.ctor().services();
if ctor_services.is_empty() {
return Err(anyhow!("No CTor keystore are configured."));
let Some((_, service_config)) = ctor_services
.find(|(hs_nick, _)| *hs_nick == &args.nickname)
"The service identified using `--nickname {}` is not configured with any recognized CTor keystore.",
&args.nickname,
Ok(service_config.id().clone())
/// Helper function for `migrate_ctor_keys`.
/// Removes all the Arti keystore entries provided.
/// Prints an error for each failed removal attempt.
fn remove_arti_entries(keymgr: &KeyMgr, arti_entries: &Vec<KeystoreEntry<'_>>) {
for entry in arti_entries {
if let Err(e) = keymgr.remove_entry(entry) {
eprintln!("Failed to remove entry {} ({e})", entry.key_path(),);
/// Builds a prompt that will be passed to the [`prompt`] function.
fn build_prompt(entries: &Vec<KeystoreEntry<'_>>) -> String {
let mut p = "WARNING: the following keys will be deleted\n".to_string();
for k in entries.iter() {
p.push('\t');
p.push_str(&k.key_path().to_string());
p.push('\n');
p.push_str("Proceed anyway?");
p