Skip to main content

pubky_common/auth/
auth_token.rs

1//! Client-server Authentication using signed timesteps
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    capabilities::Capabilities,
7    crypto::{Keypair, PublicKey, Signature},
8    namespaces::PUBKY_AUTH,
9    timestamp::Timestamp,
10};
11
12const CURRENT_VERSION: u8 = 0;
13// 3 minutes in the past or the future
14const TIMESTAMP_WINDOW: i64 = 180 * 1_000_000;
15
16mod signature_serde {
17    use core::fmt;
18
19    use serde::{
20        de::{self, SeqAccess, Visitor},
21        ser::SerializeTuple,
22        Deserializer, Serializer,
23    };
24
25    use crate::crypto::Signature;
26
27    pub fn serialize<S: Serializer>(
28        signature: &Signature,
29        serializer: S,
30    ) -> Result<S::Ok, S::Error> {
31        let mut tuple = serializer.serialize_tuple(Signature::BYTE_SIZE)?;
32
33        for byte in signature.to_bytes() {
34            tuple.serialize_element(&byte)?;
35        }
36
37        tuple.end()
38    }
39
40    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Signature, D::Error> {
41        struct SignatureVisitor;
42
43        impl<'de> Visitor<'de> for SignatureVisitor {
44            type Value = Signature;
45
46            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47                formatter.write_str("a 64-byte Ed25519 signature")
48            }
49
50            fn visit_seq<A: SeqAccess<'de>>(
51                self,
52                mut sequence: A,
53            ) -> Result<Self::Value, A::Error> {
54                let mut bytes = [0; Signature::BYTE_SIZE];
55
56                for (index, byte) in bytes.iter_mut().enumerate() {
57                    *byte = sequence
58                        .next_element()?
59                        .ok_or_else(|| de::Error::invalid_length(index, &self))?;
60                }
61
62                Ok(Signature::from_bytes(&bytes))
63            }
64        }
65
66        deserializer.deserialize_tuple(Signature::BYTE_SIZE, SignatureVisitor)
67    }
68}
69
70#[derive(Debug, PartialEq, Serialize, Deserialize)]
71/// Authentication token used by the Pubky Auth protocol.
72pub struct AuthToken {
73    /// Signature over the token.
74    #[serde(with = "signature_serde")]
75    signature: Signature,
76    /// A namespace to ensure this signature can't be used for any
77    /// other purposes that share the same message structurea by accident.
78    namespace: [u8; 10],
79    /// Version of the [AuthToken], in case we need to upgrade it to support unforeseen usecases.
80    ///
81    /// Version 0:
82    /// - Signer is implicitly the same as the root keypair for
83    ///   the [AuthToken::public_key], without any delegation.
84    /// - Capabilities are only meant for resoucres on the homeserver.
85    version: u8,
86    /// Timestamp
87    timestamp: Timestamp,
88    /// The [PublicKey] of the owner of the resources being accessed by this token.
89    public_key: PublicKey,
90    // Variable length capabilities
91    capabilities: Capabilities,
92}
93
94impl AuthToken {
95    /// Sign a new AuthToken with given capabilities.
96    pub fn sign(keypair: &Keypair, capabilities: impl Into<Capabilities>) -> Self {
97        let timestamp = Timestamp::now();
98
99        let mut token = Self {
100            signature: Signature::from_bytes(&[0; 64]),
101            namespace: *PUBKY_AUTH,
102            version: 0,
103            timestamp,
104            public_key: keypair.public_key(),
105            capabilities: capabilities.into(),
106        };
107
108        let serialized = token.serialize();
109
110        token.signature = keypair.sign(&serialized[65..]);
111
112        token
113    }
114
115    // === Getters ===
116
117    /// Returns the public key that is providing this AuthToken
118    pub fn public_key(&self) -> &PublicKey {
119        &self.public_key
120    }
121
122    /// Returns the capabilities in this AuthToken.
123    pub fn capabilities(&self) -> &Capabilities {
124        &self.capabilities
125    }
126
127    /// Returns the timestamp of this AuthToken.
128    pub fn timestamp(&self) -> Timestamp {
129        self.timestamp
130    }
131
132    // === Public Methods ===
133
134    /// Parse and verify an AuthToken.
135    pub fn verify(bytes: &[u8]) -> Result<Self, Error> {
136        if bytes[74] > CURRENT_VERSION {
137            return Err(Error::UnknownVersion);
138        }
139
140        let token = AuthToken::deserialize(bytes)?;
141
142        match token.version {
143            0 => {
144                let now = Timestamp::now();
145
146                // Chcek timestamp;
147                let diff = token.timestamp.as_u64() as i64 - now.as_u64() as i64;
148                if diff > TIMESTAMP_WINDOW {
149                    return Err(Error::TooFarInTheFuture);
150                }
151                if diff < -TIMESTAMP_WINDOW {
152                    return Err(Error::Expired);
153                }
154
155                token
156                    .public_key
157                    .verify(AuthToken::signable(token.version, bytes), &token.signature)
158                    .map_err(|_| Error::InvalidSignature)?;
159
160                Ok(token)
161            }
162            _ => unreachable!(),
163        }
164    }
165
166    /// Serialize this AuthToken to its canonical binary representation.
167    pub fn serialize(&self) -> Vec<u8> {
168        postcard::to_allocvec(self).unwrap()
169    }
170
171    /// Deserialize an AuthToken from its canonical binary representation.
172    pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
173        Ok(postcard::from_bytes(bytes)?)
174    }
175
176    fn signable(version: u8, bytes: &[u8]) -> &[u8] {
177        match version {
178            0 => bytes[65..].into(),
179            _ => unreachable!(),
180        }
181    }
182}
183
184#[derive(thiserror::Error, Debug, PartialEq, Eq)]
185/// Error verifying an [AuthToken]
186pub enum Error {
187    #[error("Unknown version")]
188    /// Unknown version
189    UnknownVersion,
190    #[error("AuthToken has a timestamp that is more than 3 minutes in the future")]
191    /// AuthToken has a timestamp that is more than 3 minutes in the future
192    TooFarInTheFuture,
193    #[error("AuthToken has a timestamp that is more than 3 minutes in the past")]
194    /// AuthToken has a timestamp that is more than 3 minutes in the past
195    Expired,
196    #[error("Invalid Signature")]
197    /// Invalid Signature
198    InvalidSignature,
199    #[error(transparent)]
200    /// Error parsing [AuthToken] using Postcard
201    Parsing(#[from] postcard::Error),
202    #[error("AuthToken already used")]
203    /// AuthToken already used
204    AlreadyUsed,
205}
206
207#[cfg(test)]
208mod tests {
209    use crate::{
210        auth::auth_token::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair,
211        timestamp::Timestamp,
212    };
213
214    use super::*;
215
216    #[test]
217    fn sign_verify() {
218        let signer = Keypair::random();
219        let capabilities = vec![Capability::root()];
220
221        let token = AuthToken::sign(&signer, capabilities.clone());
222
223        let serialized = &token.serialize();
224        assert_eq!(serialized[..64], token.signature.to_bytes());
225        assert_eq!(&serialized[64..74], PUBKY_AUTH);
226        assert_eq!(serialized[74], CURRENT_VERSION);
227
228        let verified = AuthToken::verify(serialized).unwrap();
229
230        assert_eq!(verified.capabilities, capabilities.into());
231    }
232
233    #[test]
234    fn expired() {
235        let signer = Keypair::random();
236        let timestamp = (Timestamp::now()) - (TIMESTAMP_WINDOW as u64);
237        let token = sign_with_timestamp(&signer, timestamp);
238
239        let result = AuthToken::verify(&token.serialize());
240
241        assert_eq!(result, Err(Error::Expired));
242    }
243
244    /// Build a validly signed AuthToken with an arbitrary timestamp.
245    fn sign_with_timestamp(signer: &Keypair, timestamp: Timestamp) -> AuthToken {
246        let mut token = AuthToken {
247            signature: Signature::from_bytes(&[0; 64]),
248            namespace: *PUBKY_AUTH,
249            version: 0,
250            timestamp,
251            public_key: signer.public_key(),
252            capabilities: Capabilities::from(vec![Capability::root()]),
253        };
254
255        let serialized = token.serialize();
256        token.signature = signer.sign(&serialized[65..]);
257
258        token
259    }
260
261    #[test]
262    fn too_far_in_future() {
263        let signer = Keypair::random();
264
265        let timestamp = Timestamp::now() + (TIMESTAMP_WINDOW as u64 + 5_000_000);
266        let token = sign_with_timestamp(&signer, timestamp);
267
268        assert_eq!(
269            AuthToken::verify(&token.serialize()),
270            Err(Error::TooFarInTheFuture)
271        );
272    }
273
274    #[test]
275    fn within_window() {
276        let signer = Keypair::random();
277
278        // Just inside the past boundary (TIMESTAMP_WINDOW minus 5 seconds)
279        let past_token = sign_with_timestamp(
280            &signer,
281            Timestamp::now() - (TIMESTAMP_WINDOW as u64 - 5_000_000),
282        );
283        AuthToken::verify(&past_token.serialize()).unwrap();
284
285        // Just inside the future boundary (TIMESTAMP_WINDOW minus 5 seconds)
286        let future_token = sign_with_timestamp(
287            &signer,
288            Timestamp::now() + (TIMESTAMP_WINDOW as u64 - 5_000_000),
289        );
290        AuthToken::verify(&future_token.serialize()).unwrap();
291    }
292
293    #[test]
294    fn unknown_version() {
295        let signer = Keypair::random();
296        let token = AuthToken {
297            signature: Signature::from_bytes(&[0; 64]),
298            namespace: *PUBKY_AUTH,
299            version: 1,
300            timestamp: Timestamp::now(),
301            public_key: signer.public_key(),
302            capabilities: Capabilities::from(vec![Capability::root()]),
303        };
304        let serialized = token.serialize();
305
306        assert_eq!(AuthToken::verify(&serialized), Err(Error::UnknownVersion));
307    }
308}