1use std::{
4 fmt,
5 time::{Duration, SystemTime},
6};
7
8use base64::Engine;
9use lexe_crypto::ed25519::{self, Signed};
10use lexe_std::array;
11#[cfg(any(test, feature = "test-utils"))]
12use proptest_derive::Arbitrary;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use super::user::{NodePkProof, UserPk};
17use crate::byte_str::ByteStr;
18#[cfg(any(test, feature = "test-utils"))]
19use crate::test_utils::arbitrary;
20
21#[derive(Debug, Error)]
22pub enum Error {
23 #[error("error verifying signed bearer auth request: {0}")]
24 UserVerifyError(#[source] ed25519::Error),
25
26 #[error("Decoded bearer auth token appears malformed")]
27 MalformedToken,
28
29 #[error("issued timestamp is too far from current auth server clock")]
30 ClockDrift,
31
32 #[error("auth token or auth request is expired")]
33 Expired,
34
35 #[error("timestamp is not a valid unix timestamp")]
36 InvalidTimestamp,
37
38 #[error("requested token lifetime is too long")]
39 InvalidLifetime,
40
41 #[error("user not signed up yet")]
42 NoUser,
43
44 #[error("bearer auth token is not valid base64")]
45 Base64Decode,
46
47 #[error("bearer auth token was not provided")]
48 Missing,
49
50 #[error(
53 "auth token's granted scope ({granted:?}) is not sufficient for \
54 requested scope ({requested:?})"
55 )]
56 InsufficientScope { granted: Scope, requested: Scope },
57}
58
59#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub enum UserSignupRequestWire {
74 V2(UserSignupRequestWireV2),
75}
76
77#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct UserSignupRequestWireV2 {
80 pub v1: UserSignupRequestWireV1,
81
82 pub partner: Option<UserPk>,
85}
86
87#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
88#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
89pub struct UserSignupRequestWireV1 {
90 pub node_pk_proof: NodePkProof,
92 #[cfg_attr(
97 any(test, feature = "test-utils"),
98 proptest(strategy = "arbitrary::any_option_string()")
99 )]
100 _signup_code: Option<String>,
101}
102
103impl UserSignupRequestWireV1 {
104 pub fn new(node_pk_proof: NodePkProof) -> Self {
105 Self {
106 node_pk_proof,
107 _signup_code: None,
108 }
109 }
110}
111
112#[derive(Clone, Debug)]
116pub struct BearerAuthRequest {
117 pub request_timestamp_secs: u64,
124
125 pub lifetime_secs: u32,
129
130 pub scope: Option<Scope>,
135}
136
137#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
142#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
143pub enum BearerAuthRequestWire {
144 V1(BearerAuthRequestWireV1),
145 V2(BearerAuthRequestWireV2),
147}
148
149#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
151#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
152pub struct BearerAuthRequestWireV1 {
153 request_timestamp_secs: u64,
154 lifetime_secs: u32,
155}
156
157#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
159#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
160pub struct BearerAuthRequestWireV2 {
161 v1: BearerAuthRequestWireV1,
163 scope: Option<Scope>,
164}
165
166#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
168#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
169pub enum Scope {
170 All,
172
173 NodeConnect,
176 }
181
182#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct BearerAuthResponse {
184 pub bearer_auth_token: BearerAuthToken,
185}
186
187#[derive(Clone, Serialize, Deserialize)]
193#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq))]
194pub struct BearerAuthToken(pub ByteStr);
195
196impl fmt::Debug for BearerAuthToken {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 f.write_str("BearerAuthToken(..)")
199 }
200}
201
202#[derive(Clone)]
205pub struct TokenWithExpiration {
206 pub token: BearerAuthToken,
207 pub expiration: Option<SystemTime>,
208}
209
210impl UserSignupRequestWire {
213 pub fn node_pk_proof(&self) -> &NodePkProof {
214 match self {
215 UserSignupRequestWire::V2(v2) => &v2.v1.node_pk_proof,
216 }
217 }
218
219 pub fn partner(&self) -> Option<&UserPk> {
220 match self {
221 UserSignupRequestWire::V2(v2) => v2.partner.as_ref(),
222 }
223 }
224}
225
226impl ed25519::Signable for UserSignupRequestWire {
227 const DOMAIN_SEPARATOR: [u8; 32] =
229 array::pad(*b"LEXE-REALM::UserSignupRequestWir");
230}
231
232impl UserSignupRequestWireV1 {
235 pub fn deserialize_verify(
236 serialized: &[u8],
237 ) -> Result<Signed<Self>, Error> {
238 ed25519::verify_signed_struct(ed25519::accept_any_signer, serialized)
241 .map_err(Error::UserVerifyError)
242 }
243}
244
245impl ed25519::Signable for UserSignupRequestWireV1 {
246 const DOMAIN_SEPARATOR: [u8; 32] =
248 array::pad(*b"LEXE-REALM::UserSignupRequest");
249}
250
251impl From<UserSignupRequestWireV1> for UserSignupRequestWireV2 {
254 fn from(v1: UserSignupRequestWireV1) -> Self {
255 Self { v1, partner: None }
256 }
257}
258
259impl BearerAuthRequest {
262 pub fn new(
263 now: SystemTime,
264 token_lifetime_secs: u32,
265 scope: Option<Scope>,
266 ) -> Self {
267 Self {
268 request_timestamp_secs: now
269 .duration_since(SystemTime::UNIX_EPOCH)
270 .expect("Something is very wrong with our clock")
271 .as_secs(),
272 lifetime_secs: token_lifetime_secs,
273 scope,
274 }
275 }
276
277 pub fn request_timestamp(&self) -> Result<SystemTime, Error> {
281 let t_secs = self.request_timestamp_secs;
282 let t_dur_secs = Duration::from_secs(t_secs);
283 SystemTime::UNIX_EPOCH
284 .checked_add(t_dur_secs)
285 .ok_or(Error::InvalidTimestamp)
286 }
287}
288
289impl From<BearerAuthRequestWire> for BearerAuthRequest {
290 fn from(wire: BearerAuthRequestWire) -> Self {
291 match wire {
292 BearerAuthRequestWire::V1(v1) => Self {
293 request_timestamp_secs: v1.request_timestamp_secs,
294 lifetime_secs: v1.lifetime_secs,
295 scope: None,
296 },
297 BearerAuthRequestWire::V2(v2) => Self {
298 request_timestamp_secs: v2.v1.request_timestamp_secs,
299 lifetime_secs: v2.v1.lifetime_secs,
300 scope: v2.scope,
301 },
302 }
303 }
304}
305
306impl From<BearerAuthRequest> for BearerAuthRequestWire {
307 fn from(req: BearerAuthRequest) -> Self {
308 Self::V2(BearerAuthRequestWireV2 {
309 v1: BearerAuthRequestWireV1 {
310 request_timestamp_secs: req.request_timestamp_secs,
311 lifetime_secs: req.lifetime_secs,
312 },
313 scope: req.scope,
314 })
315 }
316}
317
318impl BearerAuthRequestWire {
321 pub fn deserialize_verify(
322 serialized: &[u8],
323 ) -> Result<Signed<Self>, Error> {
324 ed25519::verify_signed_struct(ed25519::accept_any_signer, serialized)
327 .map_err(Error::UserVerifyError)
328 }
329}
330
331impl ed25519::Signable for BearerAuthRequestWire {
332 const DOMAIN_SEPARATOR: [u8; 32] =
334 array::pad(*b"LEXE-REALM::BearerAuthRequest");
335}
336
337impl BearerAuthToken {
340 pub fn encode_from_raw_bytes(signed_token_bytes: &[u8]) -> Self {
342 let b64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD
343 .encode(signed_token_bytes);
344 Self(ByteStr::from(b64_token))
345 }
346
347 pub fn decode_into_raw_bytes(&self) -> Result<Vec<u8>, Error> {
349 Self::decode_slice_into_raw_bytes(self.0.as_bytes())
350 }
351
352 pub fn decode_slice_into_raw_bytes(bytes: &[u8]) -> Result<Vec<u8>, Error> {
354 base64::engine::general_purpose::URL_SAFE_NO_PAD
355 .decode(bytes)
356 .map_err(|_| Error::Base64Decode)
357 }
358}
359
360impl fmt::Display for BearerAuthToken {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 f.write_str(self.0.as_str())
363 }
364}
365
366#[cfg(any(test, feature = "test-utils"))]
367mod arbitrary_impl {
368 use proptest::{
369 arbitrary::{Arbitrary, any},
370 strategy::{BoxedStrategy, Strategy},
371 };
372
373 use super::*;
374
375 impl Arbitrary for BearerAuthToken {
376 type Parameters = ();
377 type Strategy = BoxedStrategy<Self>;
378
379 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
380 any::<Vec<u8>>()
383 .prop_map(|bytes| {
384 BearerAuthToken::encode_from_raw_bytes(&bytes)
385 })
386 .boxed()
387 }
388 }
389}
390
391impl Scope {
394 pub fn has_permission_for(&self, requested_scope: &Self) -> bool {
397 let granted_scope = self;
398 match (granted_scope, requested_scope) {
399 (Scope::All, _) => true,
400 (Scope::NodeConnect, Scope::All) => false,
401 (Scope::NodeConnect, Scope::NodeConnect) => true,
402 }
403 }
404}
405
406#[cfg(test)]
407mod test {
408 use base64::Engine;
409 use lexe_hex::hex;
410
411 use super::*;
412 use crate::test_utils::roundtrip::{
413 bcs_roundtrip_ok, bcs_roundtrip_proptest, signed_roundtrip_proptest,
414 };
415
416 #[test]
417 fn test_user_signup_request_wire_canonical() {
418 bcs_roundtrip_proptest::<UserSignupRequestWire>();
419 }
420
421 #[test]
422 fn test_user_signed_request_wire_sign_verify() {
423 signed_roundtrip_proptest::<UserSignupRequestWire>();
424 }
425
426 #[test]
427 fn test_bearer_auth_request_wire_canonical() {
428 bcs_roundtrip_proptest::<BearerAuthRequestWire>();
429 }
430
431 #[test]
432 fn test_bearer_auth_request_wire_sign_verify() {
433 signed_roundtrip_proptest::<BearerAuthRequestWire>();
434 }
435
436 #[test]
437 fn test_bearer_auth_request_wire_snapshot() {
438 let input = "00d20296490000000058020000";
439 let req = BearerAuthRequestWire::V1(BearerAuthRequestWireV1 {
440 request_timestamp_secs: 1234567890,
441 lifetime_secs: 10 * 60,
442 });
443 bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
444
445 let input = "01d2029649000000005802000000";
446 let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
447 v1: BearerAuthRequestWireV1 {
448 request_timestamp_secs: 1234567890,
449 lifetime_secs: 10 * 60,
450 },
451 scope: None,
452 });
453 bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
454
455 let input = "01d202964900000000580200000101";
456 let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
457 v1: BearerAuthRequestWireV1 {
458 request_timestamp_secs: 1234567890,
459 lifetime_secs: 10 * 60,
460 },
461 scope: Some(Scope::NodeConnect),
462 });
463 bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
464 }
465
466 #[test]
467 fn test_auth_scope_canonical() {
468 bcs_roundtrip_proptest::<Scope>();
469 }
470
471 #[test]
472 fn test_auth_scope_snapshot() {
473 let input = b"\x00";
474 let scope = Scope::All;
475 bcs_roundtrip_ok(input, &scope);
476
477 let input = b"\x01";
478 let scope = Scope::NodeConnect;
479 bcs_roundtrip_ok(input, &scope);
480 }
481
482 #[test]
487 fn test_user_signup_request_wire_v1_snapshot() {
488 let b64 = base64::engine::general_purpose::STANDARD;
489
490 let input_with_code = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLb\
492 RjBEAiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMy\
493 npScmW9joOortpvHul9bHyojSj3Im70BCUFCQ0QtMTIzNA==";
494 let bytes_with_code = b64.decode(input_with_code).unwrap();
495 let req: UserSignupRequestWireV1 =
496 bcs::from_bytes(&bytes_with_code).unwrap();
497 assert!(req.node_pk_proof.verify().is_ok());
498
499 let input_none = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLbRjBE\
501 AiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMynpSc\
502 mW9joOortpvHul9bHyojSj3Im70A";
503 let bytes_none = b64.decode(input_none).unwrap();
504 let req: UserSignupRequestWireV1 =
505 bcs::from_bytes(&bytes_none).unwrap();
506 assert!(req.node_pk_proof.verify().is_ok());
507 }
508}