//! Helpers for building and representing hidden service descriptors. use super::*; use tor_cell::chancell::msg::HandshakeType; /// Build the descriptor. /// /// The `now` argument is used for computing the expiry of the `intro_{auth, enc}_key_cert` /// certificates included in the descriptor. The expiry will be set to 54 hours from `now`. /// /// Note: `blind_id_kp` is the blinded hidden service signing keypair used to sign descriptor /// signing keys (KP_hs_blind_id, KS_hs_blind_id). pub(super) fn build_sign( keymgr: &Arc, config: &Arc, ipt_set: &IptSet, period: TimePeriod, revision_counter: RevisionCounter, rng: &mut Rng, now: SystemTime, ) -> Result { // TODO: should this be configurable? If so, we should read it from the svc config. // /// The CREATE handshake type we support. const CREATE2_FORMATS: &[HandshakeType] = &[HandshakeType::NTOR]; /// Lifetime of the intro_{auth, enc}_key_cert certificates in the descriptor. /// /// From C-Tor src/feature/hs/hs_descriptor.h: /// /// "This defines the lifetime of the descriptor signing key and the cross certification cert of /// that key. It is set to 54 hours because a descriptor can be around for 48 hours and because /// consensuses are used after the hour, add an extra 6 hours to give some time for the service /// to stop using it." const HS_DESC_CERT_LIFETIME_SEC: Duration = Duration::from_secs(54 * 60 * 60); let intro_points = ipt_set .ipts .iter() .map(|ipt_in_set| ipt_in_set.ipt.clone()) .collect::>(); let nickname = &config.nickname; let svc_key_spec = HsIdKeypairSpecifier::new(nickname.clone()); let hsid_kp = keymgr .get::(&svc_key_spec)? .ok_or_else(|| FatalError::MissingHsIdKeypair(nickname.clone()))?; let hsid = HsIdKey::from(&hsid_kp); let blind_id_key_spec = BlindIdKeypairSpecifier::new(nickname.clone(), period); // TODO: make the keystore selector configurable let keystore_selector = Default::default(); let blind_id_kp = read_blind_id_keypair(keymgr, nickname, period)? .ok_or_else(|| internal!("hidden service offline mode not supported"))?; let blind_id_key = HsBlindIdKey::from(&blind_id_kp); let subcredential = hsid.compute_subcredential(&blind_id_key, period); let hs_desc_sign_key_spec = DescSigningKeypairSpecifier::new(nickname.clone(), period); let hs_desc_sign = keymgr.get_or_generate::( &hs_desc_sign_key_spec, keystore_selector, rng, )?; // TODO #1028: support introduction-layer authentication. let auth_required = None; let is_single_onion_service = matches!(config.anonymity, crate::Anonymity::DangerouslyNonAnonymous); // TODO (#955): perhaps the certificates should be read from the keystore, rather than created // when building the descriptor. See #1048 let intro_auth_key_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC; let intro_enc_key_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC; let hs_desc_sign_cert_expiry = now + HS_DESC_CERT_LIFETIME_SEC; // TODO (#1206): Temporarily disabled while we figure out how we want the client auth config to // work; see #1028 /* let auth_clients: Option> = config.encrypt_descriptor .map(|auth_clients| build_auth_clients(&auth_clients)); */ let auth_clients: Option> = None; let desc_signing_key_cert = create_desc_sign_key_cert( &hs_desc_sign.as_ref().verifying_key(), &blind_id_kp, hs_desc_sign_cert_expiry, ) .map_err(into_bad_api_usage!( "failed to sign the descriptor signing key" ))?; let desc = HsDescBuilder::default() .blinded_id(&(&blind_id_kp).into()) .hs_desc_sign(hs_desc_sign.as_ref()) .hs_desc_sign_cert(desc_signing_key_cert) .create2_formats(CREATE2_FORMATS) .auth_required(auth_required) .is_single_onion_service(is_single_onion_service) .intro_points(&intro_points[..]) .intro_auth_key_cert_expiry(intro_auth_key_cert_expiry) .intro_enc_key_cert_expiry(intro_enc_key_cert_expiry) .lifetime(((ipt_set.lifetime.as_secs() / 60) as u16).into()) .revision_counter(revision_counter) .subcredential(subcredential) .auth_clients(auth_clients.as_deref()) .build_sign(rng) .map_err(|e| into_internal!("failed to build descriptor")(e))?; Ok(VersionedDescriptor { desc, revision_counter, }) } /// Decode an encoded curve25519 key. fn decode_curve25519_str(key: &str) -> Result { use base64ct::{Base64, Encoding}; let Some(enc_key) = key.strip_prefix("curve25519:") else { return Err(AuthorizedClientConfigError::MalformedKey); }; let key = Base64::decode_vec(enc_key.trim_end()) .map_err(AuthorizedClientConfigError::Base64Decode)?; let bytes: [u8; 32] = key .try_into() .map_err(|_| AuthorizedClientConfigError::MalformedKey)?; Ok(curve25519::PublicKey::from(bytes)) } /// Return the keys in a directory or an error if the directory is malformed fn read_key_dir( dir: &std::path::Path, ) -> Result, AuthorizedClientConfigError> { // TODO (#1206): We will eventually need to validate the key file names and // extensions. std::fs::read_dir(dir) .map_err(|e| AuthorizedClientConfigError::KeyDir { action: "traversing a directory", path: dir.into(), error: e.into(), })? .map(|entry| { let file = entry.map_err(|e| AuthorizedClientConfigError::KeyDir { action: "invalid key dir entry", path: dir.into(), error: e.into(), })?; let meta = file .metadata() .map_err(|e| AuthorizedClientConfigError::KeyDir { action: "read metadata", path: file.path(), error: e.into(), })?; if !meta.is_file() { return Err(AuthorizedClientConfigError::MalformedFile { path: file.path() }); } let buffer = std::fs::read_to_string(file.path()).map_err(|e| { AuthorizedClientConfigError::KeyDir { action: "read", path: file.path(), error: e.into(), } })?; decode_curve25519_str(buffer.as_str()) }) .collect::, _>>() } /// Return the list of authorized public keys from the specified [`DescEncryptionConfig`]. fn build_auth_clients( auth_clients: &DescEncryptionConfig, ) -> Result, AuthorizedClientConfigError> { use crate::config::AuthorizedClientConfig::{Curve25519Key, DirectoryOfKeys}; Ok(auth_clients .authorized_client .iter() .map(|client| match client { Curve25519Key(key) => Ok(vec![**key]), DirectoryOfKeys(dir) => read_key_dir(dir.as_ref()), }) .collect::, _>>()? .into_iter() .flatten() .collect::>()) } /// The freshness status of a descriptor at a particular HsDir. #[derive(Copy, Clone, Debug, Default, PartialEq)] pub(super) enum DescriptorStatus { #[default] /// Dirty, needs to be (re)uploaded. Dirty, /// Clean, does not need to be reuploaded. Clean, } /// A descriptor and its revision. #[derive(Clone)] pub(super) struct VersionedDescriptor { /// The serialized descriptor. pub(super) desc: String, /// The revision counter. pub(super) revision_counter: RevisionCounter, } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! use crate::config::AuthorizedClientConfig::Curve25519Key; use crate::publish::descriptor::{ build_auth_clients, decode_curve25519_str, DescEncryptionConfig, }; use tor_basic_utils::test_rng::testing_rng; use tor_llcrypto::pk::curve25519::{PublicKey, StaticSecret}; #[test] fn build_auth_clients_curve25519() { let a: PublicKey = (&StaticSecret::random_from_rng(testing_rng())).into(); let b: PublicKey = (&StaticSecret::random_from_rng(testing_rng())).into(); let a_ck = Curve25519Key(a.into()); let b_ck = Curve25519Key(b.into()); let desc_enc_cfg = DescEncryptionConfig { authorized_client: vec![a_ck, b_ck], }; let auth_clients = build_auth_clients(&desc_enc_cfg); let auth_clients_ref = vec![a, b]; assert_eq!(auth_clients.unwrap(), auth_clients_ref); } #[test] fn build_auth_clients_keydir() { use crate::config::AuthorizedClientConfig::DirectoryOfKeys; use std::path::PathBuf; fn create_file(path: PathBuf, buf: &str) { use std::io::Write; let mut file = std::fs::File::create(path).unwrap(); file.write_all(buf.as_bytes()).unwrap(); } let a_base64: &str = "curve25519:NRzb4zeU4t5t2pSTW8E4DhRKmL9OiGRQrObslME08G8="; let a_dir = tempfile::tempdir().unwrap(); create_file(a_dir.path().to_path_buf().join("client_a"), a_base64); let b_base64: &str = "curve25519:HpyxYe2ODbwZdjx2VAFDO86mrjygc5lnIMnwJUOB9ww="; let b_dir = tempfile::tempdir().unwrap(); create_file(b_dir.path().to_path_buf().join("client_b"), b_base64); let desc_enc_cfg = DescEncryptionConfig { authorized_client: vec![ DirectoryOfKeys(a_dir.path().to_str().unwrap().to_owned()), DirectoryOfKeys(b_dir.path().to_str().unwrap().to_owned()), ], }; let auth_clients = build_auth_clients(&desc_enc_cfg).unwrap(); let a = decode_curve25519_str(a_base64.to_string().as_str()); let b = decode_curve25519_str(b_base64.to_string().as_str()); let auth_clients_ref = vec![a.unwrap(), b.unwrap()]; assert_eq!(auth_clients, auth_clients_ref); } }