Skip to main content

pubky_common/
auth.rs

1//! Client-server Authentication using signed timesteps
2
3use std::sync::{Arc, Mutex};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    capabilities::Capabilities,
9    crypto::{Keypair, PublicKey, Signature},
10    namespaces::PUBKY_AUTH,
11    timestamp::Timestamp,
12};
13
14const CURRENT_VERSION: u8 = 0;
15// 3 minutes in the past or the future
16const TIMESTAMP_WINDOW: i64 = 180 * 1_000_000;
17
18#[derive(Debug, PartialEq, Serialize, Deserialize)]
19/// Implementation of the [Pubky Auth spec](https://pubky.github.io/pubky-core/spec/auth.html).
20pub struct AuthToken {
21    /// Signature over the token.
22    signature: Signature,
23    /// A namespace to ensure this signature can't be used for any
24    /// other purposes that share the same message structurea by accident.
25    namespace: [u8; 10],
26    /// Version of the [AuthToken], in case we need to upgrade it to support unforeseen usecases.
27    ///
28    /// Version 0:
29    /// - Signer is implicitly the same as the root keypair for
30    ///   the [AuthToken::public_key], without any delegation.
31    /// - Capabilities are only meant for resoucres on the homeserver.
32    version: u8,
33    /// Timestamp
34    timestamp: Timestamp,
35    /// The [PublicKey] of the owner of the resources being accessed by this token.
36    public_key: PublicKey,
37    // Variable length capabilities
38    capabilities: Capabilities,
39}
40
41impl AuthToken {
42    /// Sign a new AuthToken with given capabilities.
43    pub fn sign(keypair: &Keypair, capabilities: impl Into<Capabilities>) -> Self {
44        let timestamp = Timestamp::now();
45
46        let mut token = Self {
47            signature: Signature::from_bytes(&[0; 64]),
48            namespace: *PUBKY_AUTH,
49            version: 0,
50            timestamp,
51            public_key: keypair.public_key(),
52            capabilities: capabilities.into(),
53        };
54
55        let serialized = token.serialize();
56
57        token.signature = keypair.sign(&serialized[65..]);
58
59        token
60    }
61
62    // === Getters ===
63
64    /// Returns the public key that is providing this AuthToken
65    pub fn public_key(&self) -> &PublicKey {
66        &self.public_key
67    }
68
69    /// Returns the capabilities in this AuthToken.
70    pub fn capabilities(&self) -> &Capabilities {
71        &self.capabilities
72    }
73
74    /// Returns the timestamp of this AuthToken.
75    pub fn timestamp(&self) -> Timestamp {
76        self.timestamp
77    }
78
79    // === Public Methods ===
80
81    /// Parse and verify an AuthToken.
82    pub fn verify(bytes: &[u8]) -> Result<Self, Error> {
83        if bytes[74] > CURRENT_VERSION {
84            return Err(Error::UnknownVersion);
85        }
86
87        let token = AuthToken::deserialize(bytes)?;
88
89        match token.version {
90            0 => {
91                let now = Timestamp::now();
92
93                // Chcek timestamp;
94                let diff = token.timestamp.as_u64() as i64 - now.as_u64() as i64;
95                if diff > TIMESTAMP_WINDOW {
96                    return Err(Error::TooFarInTheFuture);
97                }
98                if diff < -TIMESTAMP_WINDOW {
99                    return Err(Error::Expired);
100                }
101
102                token
103                    .public_key
104                    .verify(AuthToken::signable(token.version, bytes), &token.signature)
105                    .map_err(|_| Error::InvalidSignature)?;
106
107                Ok(token)
108            }
109            _ => unreachable!(),
110        }
111    }
112
113    /// Serialize this AuthToken to its canonical binary representation.
114    pub fn serialize(&self) -> Vec<u8> {
115        postcard::to_allocvec(self).unwrap()
116    }
117
118    /// Deserialize an AuthToken from its canonical binary representation.
119    pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
120        Ok(postcard::from_bytes(bytes)?)
121    }
122
123    fn signable(version: u8, bytes: &[u8]) -> &[u8] {
124        match version {
125            0 => bytes[65..].into(),
126            _ => unreachable!(),
127        }
128    }
129}
130
131/// Uniquely identifies an [AuthToken] by its timestamp and public key.
132#[derive(Debug, Clone, PartialEq, Eq)]
133struct TokenId {
134    timestamp: Timestamp,
135    public_key: PublicKey,
136}
137
138impl Ord for TokenId {
139    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
140        self.timestamp
141            .cmp(&other.timestamp)
142            .then_with(|| self.public_key.as_bytes().cmp(other.public_key.as_bytes()))
143    }
144}
145
146impl PartialOrd for TokenId {
147    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
148        Some(self.cmp(other))
149    }
150}
151
152/// Sorted set of [TokenId]s that have already been used.
153///
154/// Prevents replay attacks by rejecting tokens that were already seen,
155/// and periodically garbage-collects entries that are too old to matter.
156#[derive(Debug, Clone, Default)]
157struct ReplayGuard {
158    seen: Vec<TokenId>,
159}
160
161impl ReplayGuard {
162    /// Record a token and reject it if already seen.
163    fn check_and_track(&mut self, id: TokenId) -> Result<(), Error> {
164        match self.seen.binary_search(&id) {
165            Ok(_) => Err(Error::AlreadyUsed),
166            Err(index) => {
167                self.seen.insert(index, id);
168                Ok(())
169            }
170        }
171    }
172
173    /// Remove entries older than twice the [TIMESTAMP_WINDOW],
174    /// since they can never be replayed.
175    fn gc(&mut self) {
176        let cutoff = Timestamp::now() - 2 * TIMESTAMP_WINDOW as u64;
177
178        let expired_count = self.seen.partition_point(|id| id.timestamp < cutoff);
179
180        self.seen.drain(..expired_count);
181    }
182}
183
184#[derive(Debug, Clone, Default)]
185/// Verifies [AuthToken]s and guards against replay attacks.
186pub struct AuthVerifier {
187    replay_guard: Arc<Mutex<ReplayGuard>>,
188}
189
190impl AuthVerifier {
191    /// Verify an [AuthToken] by parsing it from its canonical binary representation,
192    /// verifying its signature, and confirm it wasn't already used.
193    pub fn verify(&self, bytes: &[u8]) -> Result<AuthToken, Error> {
194        let token = AuthToken::verify(bytes)?;
195
196        let id = TokenId {
197            timestamp: token.timestamp,
198            public_key: token.public_key.clone(),
199        };
200
201        let mut guard = self.replay_guard.lock().unwrap();
202        guard.gc();
203        guard.check_and_track(id)?;
204
205        Ok(token)
206    }
207}
208
209#[derive(thiserror::Error, Debug, PartialEq, Eq)]
210/// Error verifying an [AuthToken]
211pub enum Error {
212    #[error("Unknown version")]
213    /// Unknown version
214    UnknownVersion,
215    #[error("AuthToken has a timestamp that is more than 3 minutes in the future")]
216    /// AuthToken has a timestamp that is more than 3 minutes in the future
217    TooFarInTheFuture,
218    #[error("AuthToken has a timestamp that is more than 3 minutes in the past")]
219    /// AuthToken has a timestamp that is more than 3 minutes in the past
220    Expired,
221    #[error("Invalid Signature")]
222    /// Invalid Signature
223    InvalidSignature,
224    #[error(transparent)]
225    /// Error parsing [AuthToken] using Postcard
226    Parsing(#[from] postcard::Error),
227    #[error("AuthToken already used")]
228    /// AuthToken already used
229    AlreadyUsed,
230}
231
232#[cfg(test)]
233mod tests {
234    use crate::{
235        auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp,
236    };
237
238    use super::*;
239
240    #[test]
241    fn sign_verify() {
242        let signer = Keypair::random();
243        let capabilities = vec![Capability::root()];
244
245        let verifier = AuthVerifier::default();
246
247        let token = AuthToken::sign(&signer, capabilities.clone());
248
249        let serialized = &token.serialize();
250
251        verifier.verify(serialized).unwrap();
252
253        assert_eq!(token.capabilities, capabilities.into());
254    }
255
256    #[test]
257    fn expired() {
258        let signer = Keypair::random();
259        let capabilities = Capabilities::from(vec![Capability::root()]);
260
261        let verifier = AuthVerifier::default();
262
263        let timestamp = (Timestamp::now()) - (TIMESTAMP_WINDOW as u64);
264
265        let mut signable = vec![];
266        signable.extend_from_slice(signer.public_key().as_bytes());
267        signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap());
268
269        let signature = signer.sign(&signable);
270
271        let token = AuthToken {
272            signature,
273            namespace: *PUBKY_AUTH,
274            version: 0,
275            timestamp,
276            public_key: signer.public_key(),
277            capabilities,
278        };
279
280        let serialized = token.serialize();
281
282        let result = verifier.verify(&serialized);
283
284        assert_eq!(result, Err(Error::Expired));
285    }
286
287    #[test]
288    fn already_used() {
289        let signer = Keypair::random();
290        let capabilities = vec![Capability::root()];
291
292        let verifier = AuthVerifier::default();
293
294        let token = AuthToken::sign(&signer, capabilities.clone());
295
296        let serialized = &token.serialize();
297
298        verifier.verify(serialized).unwrap();
299
300        assert_eq!(token.capabilities, capabilities.into());
301
302        assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed));
303    }
304
305    /// Build a validly signed AuthToken with an arbitrary timestamp.
306    fn sign_with_timestamp(signer: &Keypair, timestamp: Timestamp) -> AuthToken {
307        let mut token = AuthToken {
308            signature: Signature::from_bytes(&[0; 64]),
309            namespace: *PUBKY_AUTH,
310            version: 0,
311            timestamp,
312            public_key: signer.public_key(),
313            capabilities: Capabilities::from(vec![Capability::root()]),
314        };
315
316        let serialized = token.serialize();
317        token.signature = signer.sign(&serialized[65..]);
318
319        token
320    }
321
322    #[test]
323    fn too_far_in_future() {
324        let signer = Keypair::random();
325        let verifier = AuthVerifier::default();
326
327        let timestamp = Timestamp::now() + (TIMESTAMP_WINDOW as u64 + 5_000_000);
328        let token = sign_with_timestamp(&signer, timestamp);
329
330        assert_eq!(
331            verifier.verify(&token.serialize()),
332            Err(Error::TooFarInTheFuture)
333        );
334    }
335
336    #[test]
337    fn within_window() {
338        let signer = Keypair::random();
339        let verifier = AuthVerifier::default();
340
341        // Just inside the past boundary (TIMESTAMP_WINDOW minus 5 seconds)
342        let past_token = sign_with_timestamp(
343            &signer,
344            Timestamp::now() - (TIMESTAMP_WINDOW as u64 - 5_000_000),
345        );
346        verifier.verify(&past_token.serialize()).unwrap();
347
348        // Just inside the future boundary (TIMESTAMP_WINDOW minus 5 seconds)
349        let future_token = sign_with_timestamp(
350            &signer,
351            Timestamp::now() + (TIMESTAMP_WINDOW as u64 - 5_000_000),
352        );
353        verifier.verify(&future_token.serialize()).unwrap();
354    }
355
356    #[test]
357    fn replay_guard_gc() {
358        let mut guard = ReplayGuard::default();
359        let signer = Keypair::random();
360        let now = Timestamp::now();
361
362        // Insert an "old" token ID (well beyond 2x the window)
363        let old_id = TokenId {
364            timestamp: now - 3 * TIMESTAMP_WINDOW as u64,
365            public_key: signer.public_key(),
366        };
367        guard.check_and_track(old_id).unwrap();
368
369        // Insert a "recent" token ID
370        let recent_id = TokenId {
371            timestamp: now,
372            public_key: signer.public_key(),
373        };
374        guard.check_and_track(recent_id.clone()).unwrap();
375
376        assert_eq!(guard.seen.len(), 2);
377
378        // GC should remove the old entry but keep the recent one
379        guard.gc();
380
381        assert_eq!(guard.seen.len(), 1);
382        assert_eq!(guard.seen[0], recent_id);
383    }
384
385    #[test]
386    fn unknown_version() {
387        let signer = Keypair::random();
388        let token = AuthToken {
389            signature: Signature::from_bytes(&[0; 64]),
390            namespace: *PUBKY_AUTH,
391            version: 1,
392            timestamp: Timestamp::now(),
393            public_key: signer.public_key(),
394            capabilities: Capabilities::from(vec![Capability::root()]),
395        };
396        let serialized = token.serialize();
397
398        assert_eq!(AuthToken::verify(&serialized), Err(Error::UnknownVersion));
399    }
400}