Skip to main content

didbot_serve/tree/
parked.rs

1//! A key held for the operator to admit later.
2//!
3//! A caller with no account yet parks a public key under a name; the
4//! operator creating that name presents the same key and the park is spent.
5//! Nothing lists the parks: one is looked up by the name the operator was
6//! told.
7
8use std::collections::BTreeMap;
9use std::sync::Mutex;
10
11use base64::prelude::*;
12use didbot_pds::AccountKind;
13use sha2::{Digest, Sha256};
14use time::{Duration, OffsetDateTime};
15
16/// How long a parked key is held, absent configuration: ten minutes.
17///
18/// **Reasonable range: 60 to 3600 seconds.** The key waits for a person to
19/// run `didbot operate` on another machine, so it has to outlast reading a
20/// fingerprint off one screen and typing on another; past an hour a park
21/// nobody admitted is holding a name against the next machine to register
22/// it.
23pub const DEFAULT_PARK_TTL: Duration = Duration::minutes(10);
24
25/// How many `bot.did.parkKey` and `describeParkedKey` calls one caller
26/// address may make per [`PARK_RATE_WINDOW`], absent configuration.
27///
28/// **Reasonable range: 1 to 1000.** A machine parks once; six covers one
29/// retrying through a flaky first boot. A lab of machines behind one
30/// address that come up together waits out the window rather than being
31/// refused for good.
32pub const PARK_RATE_LIMIT: u32 = 6;
33
34/// The window [`PARK_RATE_LIMIT`] is measured over, absent configuration.
35///
36/// **Reasonable range: 1 to 86400 seconds.** Ten minutes matches
37/// [`DEFAULT_PARK_TTL`], so one address gets as many parks per window as
38/// could be live at once.
39pub const PARK_RATE_WINDOW: std::time::Duration = std::time::Duration::from_secs(10 * 60);
40
41/// A public key parked at this server, waiting for the operator to admit
42/// it under a name.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParkedKey {
45    /// The hostname the key was parked for.
46    pub name: String,
47    /// What the account will be.
48    pub kind: AccountKind,
49    /// The key, multibase.
50    pub key: String,
51    /// `SHA256:<base64url of sha256(key)>`, over the multibase text.
52    pub fingerprint: String,
53    /// When it was parked.
54    pub parked_at: OffsetDateTime,
55    /// When it lapses.
56    pub expires_at: OffsetDateTime,
57}
58
59/// The fingerprint a parked key is shown by: `SHA256:` and the base64url
60/// digest of the multibase text, the way OpenSSH shows a key.
61pub fn fingerprint(multibase: &str) -> String {
62    format!(
63        "SHA256:{}",
64        BASE64_URL_SAFE_NO_PAD.encode(Sha256::digest(multibase.as_bytes()))
65    )
66}
67
68/// Keys parked at this server, in memory, keyed by name and fingerprint.
69///
70/// Several keys may be parked under one name at once, so parking is not a
71/// way to hold a name against the machine that wants it: the operator
72/// admits one by its fingerprint, which is what the registering machine
73/// printed.
74///
75/// Nothing lists them: a park is looked up by the name the operator was
76/// told, and a name nobody was told is not guessable from here.
77#[derive(Debug)]
78pub struct ParkedKeys {
79    ttl: Duration,
80    held: Mutex<BTreeMap<(String, String), ParkedKey>>,
81}
82
83impl ParkedKeys {
84    /// Holds each park for `ttl`.
85    pub fn new(ttl: Duration) -> Self {
86        Self {
87            ttl,
88            held: Mutex::new(BTreeMap::new()),
89        }
90    }
91
92    /// Parks `key` under `name`, or answers when the park this same key
93    /// already has there lapses.
94    ///
95    /// Another key parked under the same name is no obstacle: both wait,
96    /// and the operator admits the fingerprint they were shown.
97    pub fn park(
98        &self,
99        name: &str,
100        kind: AccountKind,
101        key: &str,
102        now: OffsetDateTime,
103    ) -> Result<ParkedKey, OffsetDateTime> {
104        let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
105        held.retain(|_, parked| parked.expires_at > now);
106        let fingerprint = fingerprint(key);
107        let at = (name.to_owned(), fingerprint.clone());
108        if let Some(existing) = held.get(&at) {
109            return Err(existing.expires_at);
110        }
111        let parked = ParkedKey {
112            name: name.to_owned(),
113            kind,
114            key: key.to_owned(),
115            fingerprint,
116            parked_at: now,
117            expires_at: now + self.ttl,
118        };
119        held.insert(at, parked.clone());
120        Ok(parked)
121    }
122
123    /// The live park under `name` whose fingerprint is `fingerprint`, or,
124    /// with none given, the one made most recently.
125    ///
126    /// One answer, because `bot.did.describeParkedKey` returns one park.
127    /// A caller that knows which key it means names it; the newest is what
128    /// a caller that names none is shown.
129    pub fn describe(
130        &self,
131        name: &str,
132        fingerprint: Option<&str>,
133        now: OffsetDateTime,
134    ) -> Option<ParkedKey> {
135        let held = self.held.lock().unwrap_or_else(|p| p.into_inner());
136        let live = |parked: &&ParkedKey| parked.expires_at > now;
137        match fingerprint {
138            Some(fingerprint) => held
139                .get(&(name.to_owned(), fingerprint.to_owned()))
140                .filter(live)
141                .cloned(),
142            None => held
143                .values()
144                .filter(|parked| parked.name == name)
145                .filter(live)
146                .max_by_key(|parked| parked.parked_at)
147                .cloned(),
148        }
149    }
150
151    /// Takes the live park under `name` whose fingerprint is `fingerprint`,
152    /// so the key is admitted once.
153    pub fn take(&self, name: &str, fingerprint: &str, now: OffsetDateTime) -> Option<ParkedKey> {
154        let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
155        let at = (name.to_owned(), fingerprint.to_owned());
156        match held.get(&at) {
157            Some(parked) if parked.expires_at > now => held.remove(&at),
158            _ => None,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn one_key_is_parked_once_per_name_until_it_lapses() {
169        let parked = ParkedKeys::new(Duration::minutes(10));
170        let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
171        let first = parked
172            .park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now)
173            .expect("parks");
174        assert_eq!(first.fingerprint, fingerprint("zQ3shKey"));
175        assert_eq!(
176            parked.park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now),
177            Err(now + Duration::minutes(10)),
178            "the same key parked twice waits for its own park to lapse"
179        );
180        assert!(parked
181            .describe("kestrel.pds.example", None, now + Duration::minutes(11))
182            .is_none());
183        assert!(parked
184            .park(
185                "kestrel.pds.example",
186                AccountKind::Host,
187                "zQ3shKey",
188                now + Duration::minutes(11)
189            )
190            .is_ok());
191    }
192
193    /// A park does not hold a name: two keys wait under one name, and each
194    /// is described and admitted by its own fingerprint.
195    #[test]
196    fn two_keys_park_under_one_name_and_each_is_taken_by_its_fingerprint() {
197        let parked = ParkedKeys::new(Duration::minutes(10));
198        let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
199        let squatter = parked
200            .park("kestrel.pds.example", AccountKind::Host, "zQ3shSquat", now)
201            .expect("parks");
202        let machine = parked
203            .park(
204                "kestrel.pds.example",
205                AccountKind::Host,
206                "zQ3shMachine",
207                now + Duration::seconds(1),
208            )
209            .expect("a second key parks under the same name");
210
211        let later = now + Duration::seconds(2);
212        let described = |fingerprint: Option<&str>| {
213            parked
214                .describe("kestrel.pds.example", fingerprint, later)
215                .map(|found| found.fingerprint)
216        };
217        assert_eq!(
218            described(None),
219            Some(machine.fingerprint.clone()),
220            "with no fingerprint, the newest park is the one described"
221        );
222        assert_eq!(
223            described(Some(&squatter.fingerprint)),
224            Some(squatter.fingerprint.clone()),
225            "each fingerprint describes its own park"
226        );
227        assert_eq!(
228            described(Some(&machine.fingerprint)),
229            Some(machine.fingerprint.clone())
230        );
231        assert_eq!(described(Some("SHA256:unknown")), None);
232        assert_eq!(
233            parked
234                .describe(
235                    "kestrel.pds.example",
236                    Some(&squatter.fingerprint),
237                    now + Duration::minutes(11)
238                )
239                .map(|found| found.fingerprint),
240            None,
241            "a lapsed park is not described by its fingerprint"
242        );
243        assert_eq!(
244            parked.take(
245                "kestrel.pds.example",
246                &machine.fingerprint,
247                now + Duration::seconds(2)
248            ),
249            Some(machine)
250        );
251        assert_eq!(
252            parked
253                .take(
254                    "kestrel.pds.example",
255                    &squatter.fingerprint,
256                    now + Duration::seconds(2)
257                )
258                .map(|found| found.fingerprint),
259            Some(squatter.fingerprint),
260            "the other park is still there to admit"
261        );
262    }
263
264    #[test]
265    fn a_park_is_taken_once_and_only_by_its_fingerprint() {
266        let parked = ParkedKeys::new(Duration::minutes(10));
267        let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
268        let first = parked
269            .park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now)
270            .expect("parks");
271        assert!(parked
272            .take("kestrel.pds.example", "SHA256:wrong", now)
273            .is_none());
274        assert_eq!(
275            parked.take("kestrel.pds.example", &first.fingerprint, now),
276            Some(first.clone())
277        );
278        assert!(parked
279            .take("kestrel.pds.example", &first.fingerprint, now)
280            .is_none());
281    }
282}