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
14// 30 seconds
15const TIME_INTERVAL: u64 = 30 * 1_000_000;
16
17const CURRENT_VERSION: u8 = 0;
18// 45 seconds in the past or the future
19const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000;
20
21#[derive(Debug, PartialEq, Serialize, Deserialize)]
22/// Implementation of the [Pubky Auth spec](https://pubky.github.io/pubky-core/spec/auth.html).
23pub struct AuthToken {
24    /// Signature over the token.
25    signature: Signature,
26    /// A namespace to ensure this signature can't be used for any
27    /// other purposes that share the same message structurea by accident.
28    namespace: [u8; 10],
29    /// Version of the [AuthToken], in case we need to upgrade it to support unforeseen usecases.
30    ///
31    /// Version 0:
32    /// - Signer is implicitly the same as the root keypair for
33    ///   the [AuthToken::public_key], without any delegation.
34    /// - Capabilities are only meant for resoucres on the homeserver.
35    version: u8,
36    /// Timestamp
37    timestamp: Timestamp,
38    /// The [PublicKey] of the owner of the resources being accessed by this token.
39    public_key: PublicKey,
40    // Variable length capabilities
41    capabilities: Capabilities,
42}
43
44impl AuthToken {
45    /// Sign a new AuthToken with given capabilities.
46    pub fn sign(keypair: &Keypair, capabilities: impl Into<Capabilities>) -> Self {
47        let timestamp = Timestamp::now();
48
49        let mut token = Self {
50            signature: Signature::from_bytes(&[0; 64]),
51            namespace: *PUBKY_AUTH,
52            version: 0,
53            timestamp,
54            public_key: keypair.public_key(),
55            capabilities: capabilities.into(),
56        };
57
58        let serialized = token.serialize();
59
60        token.signature = keypair.sign(&serialized[65..]);
61
62        token
63    }
64
65    // === Getters ===
66
67    /// Returns the public key that is providing this AuthToken
68    pub fn public_key(&self) -> &PublicKey {
69        &self.public_key
70    }
71
72    /// Returns the capabilities in this AuthToken.
73    pub fn capabilities(&self) -> &Capabilities {
74        &self.capabilities
75    }
76
77    // === Public Methods ===
78
79    /// Parse and verify an AuthToken.
80    pub fn verify(bytes: &[u8]) -> Result<Self, Error> {
81        if bytes[75] > CURRENT_VERSION {
82            return Err(Error::UnknownVersion);
83        }
84
85        let token = AuthToken::deserialize(bytes)?;
86
87        match token.version {
88            0 => {
89                let now = Timestamp::now();
90
91                // Chcek timestamp;
92                let diff = token.timestamp.as_u64() as i64 - now.as_u64() as i64;
93                if diff > TIMESTAMP_WINDOW {
94                    return Err(Error::TooFarInTheFuture);
95                }
96                if diff < -TIMESTAMP_WINDOW {
97                    return Err(Error::Expired);
98                }
99
100                token
101                    .public_key
102                    .verify(AuthToken::signable(token.version, bytes), &token.signature)
103                    .map_err(|_| Error::InvalidSignature)?;
104
105                Ok(token)
106            }
107            _ => unreachable!(),
108        }
109    }
110
111    /// Serialize this AuthToken to its canonical binary representation.
112    pub fn serialize(&self) -> Vec<u8> {
113        postcard::to_allocvec(self).unwrap()
114    }
115
116    /// Deserialize an AuthToken from its canonical binary representation.
117    pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
118        Ok(postcard::from_bytes(bytes)?)
119    }
120
121    /// Returns the unique ID for this [AuthToken], which is a concatenation of
122    /// [AuthToken::public_key] and [AuthToken::timestamp].
123    ///
124    /// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::public_key].
125    fn id(version: u8, bytes: &[u8]) -> Box<[u8]> {
126        match version {
127            0 => bytes[75..115].into(),
128            _ => unreachable!(),
129        }
130    }
131
132    fn signable(version: u8, bytes: &[u8]) -> &[u8] {
133        match version {
134            0 => bytes[65..].into(),
135            _ => unreachable!(),
136        }
137    }
138}
139
140#[derive(Debug, Clone, Default)]
141/// Keeps track of used AuthToken until they expire.
142pub struct AuthVerifier {
143    seen: Arc<Mutex<Vec<Box<[u8]>>>>,
144}
145
146impl AuthVerifier {
147    /// Verify an [AuthToken] by parsing it from its canonical binary representation,
148    /// verifying its signature, and confirm it wasn't already used.
149    pub fn verify(&self, bytes: &[u8]) -> Result<AuthToken, Error> {
150        self.gc();
151
152        let token = AuthToken::verify(bytes)?;
153
154        let mut seen = self.seen.lock().unwrap();
155
156        let id = AuthToken::id(token.version, bytes);
157
158        match seen.binary_search_by(|element| element.cmp(&id)) {
159            Ok(_) => Err(Error::AlreadyUsed),
160            Err(index) => {
161                seen.insert(index, id);
162                Ok(token)
163            }
164        }
165    }
166
167    // === Private Methods ===
168
169    /// Remove all tokens older than two time intervals in the past.
170    fn gc(&self) {
171        let threshold = ((Timestamp::now().as_u64() / TIME_INTERVAL) - 2).to_be_bytes();
172
173        let mut inner = self.seen.lock().unwrap();
174
175        match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) {
176            Ok(index) | Err(index) => {
177                inner.drain(0..index);
178            }
179        }
180    }
181}
182
183#[derive(thiserror::Error, Debug, PartialEq, Eq)]
184/// Error verifying an [AuthToken]
185pub enum Error {
186    #[error("Unknown version")]
187    /// Unknown version
188    UnknownVersion,
189    #[error("AuthToken has a timestamp that is more than 45 seconds in the future")]
190    /// AuthToken has a timestamp that is more than 45 seconds in the future
191    TooFarInTheFuture,
192    #[error("AuthToken has a timestamp that is more than 45 seconds in the past")]
193    /// AuthToken has a timestamp that is more than 45 seconds in the past
194    Expired,
195    #[error("Invalid Signature")]
196    /// Invalid Signature
197    InvalidSignature,
198    #[error(transparent)]
199    /// Error parsing [AuthToken] using Postcard
200    Parsing(#[from] postcard::Error),
201    #[error("AuthToken already used")]
202    /// AuthToken already used
203    AlreadyUsed,
204}
205
206#[cfg(test)]
207mod tests {
208    use crate::{
209        auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp,
210    };
211
212    use super::*;
213
214    #[test]
215    fn v0_id_signable() {
216        let signer = Keypair::random();
217        let capabilities = vec![Capability::root()];
218
219        let token = AuthToken::sign(&signer, capabilities.clone());
220
221        let serialized = &token.serialize();
222
223        let mut id = vec![];
224        id.extend_from_slice(&token.timestamp.to_bytes());
225        id.extend_from_slice(signer.public_key().as_bytes());
226
227        assert_eq!(AuthToken::id(token.version, serialized), id.into());
228
229        assert_eq!(
230            AuthToken::signable(token.version, serialized),
231            &serialized[65..]
232        )
233    }
234
235    #[test]
236    fn sign_verify() {
237        let signer = Keypair::random();
238        let capabilities = vec![Capability::root()];
239
240        let verifier = AuthVerifier::default();
241
242        let token = AuthToken::sign(&signer, capabilities.clone());
243
244        let serialized = &token.serialize();
245
246        verifier.verify(serialized).unwrap();
247
248        assert_eq!(token.capabilities, capabilities.into());
249    }
250
251    #[test]
252    fn expired() {
253        let signer = Keypair::random();
254        let capabilities = Capabilities(vec![Capability::root()]);
255
256        let verifier = AuthVerifier::default();
257
258        let timestamp = (Timestamp::now()) - (TIMESTAMP_WINDOW as u64);
259
260        let mut signable = vec![];
261        signable.extend_from_slice(signer.public_key().as_bytes());
262        signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap());
263
264        let signature = signer.sign(&signable);
265
266        let token = AuthToken {
267            signature,
268            namespace: *PUBKY_AUTH,
269            version: 0,
270            timestamp,
271            public_key: signer.public_key(),
272            capabilities,
273        };
274
275        let serialized = token.serialize();
276
277        let result = verifier.verify(&serialized);
278
279        assert_eq!(result, Err(Error::Expired));
280    }
281
282    #[test]
283    fn already_used() {
284        let signer = Keypair::random();
285        let capabilities = vec![Capability::root()];
286
287        let verifier = AuthVerifier::default();
288
289        let token = AuthToken::sign(&signer, capabilities.clone());
290
291        let serialized = &token.serialize();
292
293        verifier.verify(serialized).unwrap();
294
295        assert_eq!(token.capabilities, capabilities.into());
296
297        assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed));
298    }
299}