1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
//! Configuration information for onion services.
//
// TODO HSS: We may want rename some of the types and members here!
use base64ct::{Base64Unpadded, Encoding as _};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tor_config::ConfigBuildError;
use tor_hscrypto::pk::HsClientDescEncKey;
use tor_llcrypto::pk::curve25519;
use crate::HsNickname;
/// Configuration for one onion service.
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(error = "ConfigBuildError", validate = "Self::validate"))]
#[builder(derive(Serialize, Deserialize))]
pub struct OnionServiceConfig {
/// The nickname used to look up this service's keys, state, configuration, etc,
//
// TODO HSS: It's possible that instead of having this be _part_ of the
// service's configuration, we want this to be the key for a map in
// which the service's configuration is stored. We'll see how the code
// evolves.
// (^ ipt_mgr::IptManager contains a copy of this nickname, that should be fixed too)
pub(crate) name: HsNickname,
// TODO HSS: Perhaps this belongs at a higher level.
// enabled: bool,
/// Whether we want this to be a non-anonymous "single onion service".
/// We could skip this in v1. We should make sure that our state
/// is built to make it hard to accidentally set this.
pub(crate) anonymity: crate::Anonymity,
/// Number of intro points; defaults to 3; max 20.
#[builder(default = "3")]
pub(crate) num_intro_points: u8,
/// A rate-limit on the acceptable rate of introduction requests.
///
/// We send this to the send to the introduction point to configure how many
/// introduction requests it sends us.
rate_limit_at_intro: Option<TokenBucketConfig>,
/// How many streams will we allow to be open at once for a single circuit on
/// this service?
#[builder(default = "65535")]
max_concurrent_streams_per_circuit: u32,
/// If true, we will require proof-of-work when we're under heavy load.
enable_pow: bool,
/// Disable the compiled backend for proof-of-work.
disable_pow_compilation: bool,
// TODO HSS: C tor has this, but I don't know if we want it.
//
// TODO HSS: It's possible that we want this to relate, somehow, to our
// rate_limit_at_intro settings.
//
// /// A rate-limit on dispatching requests from the request queue when
// /// our proof-of-work defense is enabled.
// pow_queue_rate: TokenBucketConfig,
// ...
// /// Configure descriptor-based client authorization.
// ///
// /// When this is enabled, we encrypt our list of introduction point and keys
// /// so that only clients holding one of the listed keys can decrypt it.
//
// TODO HSS: we'd like this to be an Option, but that doesn't work well with
// sub_builder. We need to figure out what to do there.
//
// TODO HSS: Temporarily disabled while we figure out how we want it to work;
// see #1028
//
// pub(crate) encrypt_descriptor: Option<DescEncryptionConfig>,
//
// TODO HSS: Do we want a "descriptor_lifetime" setting? C tor doesn't have
// one.
}
impl OnionServiceConfigBuilder {
/// Builder helper: check wither the options in this builder are consistent.
fn validate(&self) -> Result<(), ConfigBuildError> {
/// Largest supported number of introduction points
//
// TODO HSS Is this a consensus parameter or anything? What does C tor do?
const MAX_INTRO_POINTS: u8 = 20;
if let Some(ipts) = self.num_intro_points {
if !(1..=MAX_INTRO_POINTS).contains(&ipts) {
return Err(ConfigBuildError::Invalid {
field: "num_intro_points".into(),
problem: "Out of range 1..20".into(),
});
}
}
Ok(())
}
}
/// Configure a token-bucket style limit on some process.
//
// TODO HSS: possibly lower this; it will be used in far more places.
//
// TODO: Do we want to parameterize this, or make it always u32? Do we want to
// specify "per second"?
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenBucketConfig {
/// The maximum number of items to process per second.
rate: u32,
/// The maximum number of items to process in a single burst.
burst: u32,
}
impl TokenBucketConfig {
/// Create a new token-bucket configuration to rate-limit some action.
///
/// The "bucket" will have a maximum capacity of `burst`, and will fill at a
/// rate of `rate` per second. New actions are permitted if the bucket is nonempty;
/// each action removes one token from the bucket.
pub fn new(rate: u32, burst: u32) -> Self {
Self { rate, burst }
}
}
/// Configuration for descriptor encryption.
#[derive(Debug, Clone, Builder, PartialEq)]
#[builder(derive(Serialize, Deserialize))]
pub struct DescEncryptionConfig {
/// A list of our authorized clients.
///
/// Note that if this list is empty, no clients can connect.
//
// TODO HSS: It might be good to replace this with a trait or something, so that
// we can let callers give us a ClientKeyProvider or some plug-in that reads
// keys from somewhere else. On the other hand, we might have this configure
// our default ClientKeyProvider, and only allow programmatic ClientKeyProviders
authorized_client: Vec<AuthorizedClientConfig>,
}
/// A single client (or a collection of clients) authorized using the descriptor encryption mechanism.
#[derive(Debug, Clone, PartialEq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay)]
#[non_exhaustive]
pub enum AuthorizedClientConfig {
/// A directory full of authorized public keys.
DirectoryOfKeys(PathBuf),
/// A single authorized public key.
Curve25519Key(HsClientDescEncKey),
}
impl std::fmt::Display for AuthorizedClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DirectoryOfKeys(pb) => write!(f, "dir:{}", pb.display()),
Self::Curve25519Key(key) => write!(
f,
"curve25519:{}",
Base64Unpadded::encode_string(key.as_bytes())
),
}
}
}
/// A problem encountered while parsing an AuthorizedClientConfig.
#[derive(thiserror::Error, Clone, Debug)]
#[non_exhaustive]
pub enum AuthorizedClientParseError {
/// Didn't recognize the type of this [`AuthorizedClientConfig`].
///
/// Recognized types are `dir` and `curve25519`.
#[error("Unrecognized authorized client type")]
InvalidType,
/// Couldn't parse a curve25519 key.
#[error("Invalid curve25519 key")]
InvalidKey,
}
impl std::str::FromStr for AuthorizedClientConfig {
type Err = AuthorizedClientParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((tp, val)) = s.split_once(':') else {
return Err(Self::Err::InvalidType);
};
if tp == "dir" {
Ok(Self::DirectoryOfKeys(val.into()))
} else if tp == "curve25519" {
let bytes: [u8; 32] = Base64Unpadded::decode_vec(val)
.map_err(|_| Self::Err::InvalidKey)?
.try_into()
.map_err(|_| Self::Err::InvalidKey)?;
Ok(Self::Curve25519Key(HsClientDescEncKey::from(
curve25519::PublicKey::from(bytes),
)))
} else {
Err(Self::Err::InvalidType)
}
}
}