Skip to main content

syncular_testkit/
auth_lease.rs

1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2use base64::Engine as _;
3use p256::ecdsa::signature::{Signer, Verifier};
4use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
5use serde::de::DeserializeOwned;
6use syncular_runtime::protocol::{
7    AuthLeasePayload, AuthLeaseProtectedHeader, AuthLeaseValidationResult, AUTH_LEASE_ALG_ES256,
8    AUTH_LEASE_CODE_EXPIRED, AUTH_LEASE_CODE_INVALID, AUTH_LEASE_TYP,
9};
10
11#[derive(Clone)]
12pub struct TestAuthLeaseKeyPair {
13    kid: String,
14    signing_key: SigningKey,
15    verifying_key: VerifyingKey,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct VerifiedTestAuthLease {
20    pub header: AuthLeaseProtectedHeader,
21    pub payload: AuthLeasePayload,
22}
23
24impl TestAuthLeaseKeyPair {
25    pub fn deterministic(kid: impl Into<String>) -> Self {
26        let signing_key = SigningKey::from_slice(&[
27            7, 42, 19, 88, 193, 54, 21, 77, 99, 101, 12, 204, 33, 15, 76, 145, 9, 111, 7, 62, 188,
28            10, 222, 44, 72, 3, 170, 81, 94, 6, 23, 209,
29        ])
30        .expect("deterministic test auth lease key");
31        let verifying_key = *signing_key.verifying_key();
32        Self {
33            kid: kid.into(),
34            signing_key,
35            verifying_key,
36        }
37    }
38
39    pub fn kid(&self) -> &str {
40        &self.kid
41    }
42
43    pub fn verifying_key(&self) -> &VerifyingKey {
44        &self.verifying_key
45    }
46}
47
48impl Default for TestAuthLeaseKeyPair {
49    fn default() -> Self {
50        Self::deterministic("syncular-test-lease-key")
51    }
52}
53
54pub fn issue_test_auth_lease(payload: &AuthLeasePayload, key: &TestAuthLeaseKeyPair) -> String {
55    let header = AuthLeaseProtectedHeader::es256(key.kid());
56    let signing_input = format!(
57        "{}.{}",
58        encode_json_segment(&header),
59        encode_json_segment(payload)
60    );
61    let signature: Signature = key.signing_key.sign(signing_input.as_bytes());
62    format!(
63        "{}.{}",
64        signing_input,
65        URL_SAFE_NO_PAD.encode(signature.to_bytes())
66    )
67}
68
69pub fn verify_test_auth_lease(
70    token: &str,
71    verifying_key: &VerifyingKey,
72    now_ms: i64,
73) -> Result<VerifiedTestAuthLease, AuthLeaseValidationResult> {
74    let parts = token.split('.').collect::<Vec<_>>();
75    if parts.len() != 3 {
76        return Err(invalid("auth lease token must have three JWS segments"));
77    }
78
79    let header: AuthLeaseProtectedHeader = decode_json_segment(parts[0])?;
80    if header.alg != AUTH_LEASE_ALG_ES256 || header.typ != AUTH_LEASE_TYP {
81        return Err(invalid("auth lease token has unsupported protected header"));
82    }
83
84    let signature = URL_SAFE_NO_PAD
85        .decode(parts[2])
86        .ok()
87        .and_then(|bytes| Signature::from_slice(&bytes).ok())
88        .ok_or_else(|| invalid("auth lease signature segment is invalid"))?;
89    let signing_input = format!("{}.{}", parts[0], parts[1]);
90    verifying_key
91        .verify(signing_input.as_bytes(), &signature)
92        .map_err(|_| invalid("auth lease signature verification failed"))?;
93
94    let payload: AuthLeasePayload = decode_json_segment(parts[1])?;
95    let skew = payload.max_clock_skew_ms.max(0);
96    if now_ms + skew < payload.not_before_ms {
97        return Err(AuthLeaseValidationResult::rejected(
98            AUTH_LEASE_CODE_INVALID,
99            "auth lease is not valid yet",
100        ));
101    }
102    if now_ms - skew > payload.expires_at_ms {
103        let mut result =
104            AuthLeaseValidationResult::rejected(AUTH_LEASE_CODE_EXPIRED, "auth lease is expired");
105        result.lease_id = Some(payload.lease_id);
106        result.kid = Some(header.kid);
107        result.expires_at_ms = Some(payload.expires_at_ms);
108        return Err(result);
109    }
110
111    Ok(VerifiedTestAuthLease { header, payload })
112}
113
114fn encode_json_segment<T: serde::Serialize>(value: &T) -> String {
115    let json = serde_json::to_vec(value).expect("auth lease JSON segment");
116    URL_SAFE_NO_PAD.encode(json)
117}
118
119fn decode_json_segment<T: DeserializeOwned>(segment: &str) -> Result<T, AuthLeaseValidationResult> {
120    let bytes = URL_SAFE_NO_PAD
121        .decode(segment)
122        .map_err(|_| invalid("auth lease JSON segment is not base64url"))?;
123    serde_json::from_slice(&bytes).map_err(|_| invalid("auth lease JSON segment is invalid"))
124}
125
126fn invalid(message: impl Into<String>) -> AuthLeaseValidationResult {
127    AuthLeaseValidationResult::rejected(AUTH_LEASE_CODE_INVALID, message)
128}