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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
|
# Live reloading the restricted discovery config
Given a hidden service restricted discovery config:
```toml
[onion_services."allium-cepa".restricted_discovery]
enabled = true
[onion_services."allium-cepa".restricted_discovery.static_keys]
alice = "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA"
bob = "descriptor:x25519:B5ZQGTPERMMUDA6VC63LHJUF5IHPOKJMUK26LY2XKSF7VG52AESQ"
[[onion_services."allium-cepa".restricted_discovery.key_dirs]]
path = "/var/lib/tor/hidden_service/authorized_clients"
```
there are multiple ways to update the list of authorized clients:
* Case 0: toggling `enabled` on or off.
* Case 1: updating the `static_keys` mapping by adding/removing a key:
```diff
[onion_services."allium-cepa".restricted_discovery.static_keys]
alice = "descriptor:x25519:PU63REQUH4PP464E2Y7AVQ35HBB5DXDH5XEUVUNP3KCPNOXZGIBA"
bob = "descriptor:x25519:B5ZQGTPERMMUDA6VC63LHJUF5IHPOKJMUK26LY2XKSF7VG52AESQ"
+carol = "descriptor:x25519:dz4q5xqlb4ldnbs72iarrml4ephk3du4i7o2cgiva5lwr6wkquja"
[[onion_services."allium-cepa".restricted_discovery.key_dirs]]
path = "/var/lib/tor/hidden_service/authorized_clients"
```
* Case 2: updating the `key_dirs` list by adding/removing a key directory:
```diff
[[onion_services."allium-cepa".restricted_discovery.key_dirs]]
path = "/var/lib/tor/hidden_service/authorized_clients"
+
+[[onion_services."allium-cepa".restricted_discovery.key_dirs]]
+path = "/home/foo/hidden_service/authorized_clients"
```
* Case 3: updating the contents of one of the configured `key_dirs`, either by
adding/removing a key file, or by modifying an existing one
In all three cases, the hidden service should be able to detect that a change
has happened, and respond by generating and publishing a new descriptor.
## Updating the `authorized_clients`
Currently, the `RunningOnionServices` contains an `Arc`'d `authorized_clients`
list, shared with the descriptor publisher. It would be better if we moved it
inside the publisher, as it's not really used anywhere else.
**Suggested resolution**:
* move `authorized_clients` from `RunningOnionService` to `Publisher`
* make `restricted_discovery: simply_update` (it's currently `unchangeable`)
* make the publisher's `handle_svc_config_change` recompute the
`authorized_clients` using `new_config.restricted_discovery.read_keys()`. We
need to be careful here though: `handle_svc_config_change` receives the new
svc config sent from `RunningOnionService::reconfigure`. But checking if the
new config is different from the old one is no longer enough, we now also
need to check if the contents of any of the `key_dirs` have changed. This is
not something we can currently find out via the `reconfigure()` mechanism)
### Handling `static_keys` changes (case 1)
Case 1 is the simplest to handle, because any changes to `static_keys` will be
detected by the existing config watching mechanism
### Watching for changes within the configured `key_dirs` (cases 2 and 3)
This will involve adding a watcher for each `key_dirs` directory, and generating
and publishing a new descriptor on change. We most likely don't want to
republish the descriptor on *every* change though. Consider a hidden service
operator copying client keys to the key directory one by one while the service
is running: updating on *every* change would trigger a burst of descriptor
uploads (this is covered in the `When to republish?` section below).
The directories from `key_dirs` will need to be watched for changes similarly to
how we watch `ConfigurationSource::Dir`s. Additionally, if a directory is added
or removed, we will need to update the watch list.
Some of the existing directory watching logic can be reused. We have several
options wrt which parts we reuse and how we reuse them.
#### Option 1: Move `FileWatcher` to `tor-config`
One option would be to move `FileWatcher` from the `arti` crate to `tor-config`,
so it can be reused by `tor-hsservice` to watch the
`restricted_discovery.key_dirs` for changes. We will also need to decouple it
from `ConfigurationSources`, because `ConfigurationSources` is geared towards
TOML configuration.
This option is kludgey, because it will involve spawning an extra, ad-hoc
directory watcher thread from within `tor-hsservice` (the thread would probably
be spawned by the descriptor publisher, as that is the only part that cares
about the authorized clients list changing).
With this option, the publisher would
* spawn a thread for watching for changes in the contents of the configured
`key_dirs`
* watch for changes to `static_keys` and `key_dirs` via
`handle_svc_config_change` (which receives the new config via the usual
`reconfigure()` mechanism)
* if the `key_dirs` change, it will update its `FileWatcher` by calling
`watch_dir`/`unwatch_dir` for each of the added/removed dirs
In other words, the publisher will watch for restricted discovery config changes
from two different places (the directory watcher thread and
`handle_svc_config_change`), which is quite fiddly and error-prone.
**Missing features**:
1. `FileWatcher::unwatch_{file, dir}` functionality
OTOH, because the client authorization keys from `key_dirs` are, in a sense, an
extension of the service's configuration, the `key_dirs` could conceivably be
viewed as a special kind of `ConfigurationSource`s, so perhaps they *should* be
watched by the main configuration watching logic. However, `ConfigurationSource`
was designed for watching and loading *TOML files*, so it will require some
modification if we want to repurpose it for watching `key_dirs`. This is `Option
2` described below.
> Note: the "client authorization" terminology is deprecated.
> "Client authorizatio" is now known as "restricted discovery".
#### Option 2: Make each dir in `key_dirs` a `ConfigurationSource`
For this we'll need to extend the configuration watching mechanism to support
watching for changes to files that *aren't* TOML configs. This way, we can let
the existing config watching mechanism from `reload_cfg` watch for changes to
`restricted_discovery.key_dirs` (so unlike `Option #1`, this option wouldn't
involve spawning any additional watcher threads)
**Missing features**:
1. the ability to watch `ConfigurationSource::Dir`s for changes to non-TOML
files
2. the ability to add or remove `ConfigurationSource::Dir`s on-the-fly, based
on the contents of the config (in this case, based on the value of
`restricted_discovery.key_dirs`)
3. `ConfiurationSource`s with custom mistrust settings: currently, a
`ConfiurationSources` applies the same `mistrust` to of its all
`ConfiurationSource`, whereas in `restricted_discovery.key_dirs`, each
directory has its own mistrust settings
4. somehow extend the `reconfigure()` machinery to support this (it currently
cannot convey this sort of change, as it doesn't affect the value of the
`TOML` config).
For 1:
We will modify the `ConfigurationSource::Dir` variant to support watching for
files that don't necessarily end in `.toml`:
```diff
diff --git a/crates/tor-config/src/sources.rs b/crates/tor-config/src/sources.rs
index 32a2a7a42..618fc7b26 100644
--- a/crates/tor-config/src/sources.rs
+++ b/crates/tor-config/src/sources.rs
@@ -71,7 +71,12 @@ pub enum ConfigurationSource {
File(PathBuf),
/// A directory
- Dir(PathBuf),
+ Dir {
+ /// The path
+ path: PathBuf,
+ /// The extension of the files to watch
+ extension: &'static str,
+ }
/// A verbatim TOML file
Verbatim(Arc<String>),
```
When scanning for files in `ConfigurationSources::scan`, we will look for the
specified `extension`:
```diff
use ConfigurationSource as CS;
match &source {
- CS::Dir(dirname) => {
+ CS::Dir { path: dirname, extension } => {
let dir = match fs::read_dir(dirname) {
Ok(y) => y,
Err(e) => {
@@ -307,7 +312,7 @@ impl ConfigurationSources {
let leaf = found.file_name();
let leaf: &Path = leaf.as_ref();
match leaf.extension() {
- Some(e) if e == "toml" => {}
+ Some(e) if e == *extension => {}
_ => continue,
}
entries.push(found.path());
```
`FoundConfigFiles` will need to be adapted to support (or, rather, ignore)
non-TOML files.
Each `FoundConfigFile` will have a `file_kind: ConfigFileKind`, where
`ConfigFileKind` will be
```rust
pub enum ConfigFileKind {
Toml,
Other { extension: &'static str },
}
```
`FoundConfigFiles::load` will skip over any non-toml files, and
`FoundConfigFiles::add_sources` will ignore the `FoundConfigFiles` that have
`file_kind != ConfigFileKind::Toml`.
For 2:
If the `key_dirs` list changes, we will need to somehow update the
`ConfigurationSources` list. We could, for example, let the reconfigurable
modules somehow return instructions about which `ConfigurationSources` to keep.
**Suggested resolution**:
* do not implement any of this (this option doesn't seem worth the added
complexity)
* in the future, if we have other use cases for this, consider adding a more
general-purpose directory watching mechanism
## When to republish?
* if `restricted_discovery.enabled` is `false`, we will generate a new
descriptor and republish immediately
* if `restricted_discovery.enabled` is set to `true`, or if the authorized
clients have changed, we will only republish the descriptor after
`CLIENT_CHANGE_REPUBLISH_DEBOUNCE_INTERVAL` seconds of inactivity
Initially `CLIENT_CHANGE_REPUBLISH_DEBOUNCE_INTERVAL` will be set to 60s.
(Perhaps we will eventually want to make it configurable?)
### Should we also rotate IPTs?
If an authorized client is removed, we might also want to also rotate the IPTs
as part of this process, to prevent any no-longer-authorized clients from
reaching the service.
If so, we will need the IPT manager to watch for changes in the
`restricted_discovery` config, and to react by rotating the IPTs.
Alternatively, we could keep the existing IPTs and make it very clear that
removing one of the client keys does not necessarily revoke the access of that
client.
**Suggested resolution**:
* do not rotate the IPTs
* make sure the restricted discovery documentation is clear about it not being
a substitute for real client authorization mechanism
|