1use std::collections::HashMap;
7use std::sync::Mutex;
8
9use base64::Engine as _;
10use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
11use jiff::Timestamp;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15use crate::Error;
16use crate::key::{PublicKey, SigningKey};
17
18const VERSION: &str = "okp1";
19pub const MAX_LIFETIME_SECS: i64 = 300;
21pub const SKEW_SECS: i64 = 60;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Claims {
27 pub team: String,
29 pub iss: String,
31 pub aud: String,
33 pub iat: i64,
35 pub exp: i64,
37 pub jti: String,
39 pub body: String,
41 pub ver: u64,
43}
44
45pub fn body_hash(body: &[u8]) -> String {
47 hex::encode(Sha256::digest(body))
48}
49
50impl Claims {
51 pub fn new(team: &str, iss: &str, aud: &str, ver: u64, body: &[u8], now: Timestamp) -> Self {
53 let iat = now.as_second();
54 Self {
55 team: team.to_string(),
56 iss: iss.to_string(),
57 aud: aud.to_string(),
58 iat,
59 exp: iat + MAX_LIFETIME_SECS,
60 jti: hex::encode(rand::random::<[u8; 16]>()),
61 body: body_hash(body),
62 ver,
63 }
64 }
65}
66
67pub fn sign(key: &SigningKey, claims: &Claims) -> String {
69 let payload = B64.encode(serde_json::to_vec(claims).expect("claims serialize"));
70 let signed = format!("{VERSION}.{payload}");
71 let signature = B64.encode(key.sign(signed.as_bytes()));
72 format!("{signed}.{signature}")
73}
74
75pub fn peek(token: &str) -> Result<Claims, Error> {
77 let bad = || Error::Token("malformed token".into());
78 let mut parts = token.trim().split('.');
79 let (Some(VERSION), Some(payload), Some(_), None) =
80 (parts.next(), parts.next(), parts.next(), parts.next())
81 else {
82 return Err(bad());
83 };
84 serde_json::from_slice(&B64.decode(payload).map_err(|_| bad())?).map_err(|_| bad())
85}
86
87pub fn verify(
90 token: &str,
91 signer: &PublicKey,
92 body: &[u8],
93 now: Timestamp,
94) -> Result<Claims, Error> {
95 let claims = peek(token)?;
96 let token = token.trim();
97 let (signed, signature) = token
98 .rsplit_once('.')
99 .ok_or_else(|| Error::Token("malformed token".into()))?;
100 let signature = B64
101 .decode(signature)
102 .map_err(|_| Error::Token("malformed signature".into()))?;
103 if !signer.verify(signed.as_bytes(), &signature) {
104 return Err(Error::Token("bad signature".into()));
105 }
106 let now = now.as_second();
107 if claims.exp <= claims.iat || claims.exp - claims.iat > MAX_LIFETIME_SECS {
108 return Err(Error::Token("invalid lifetime".into()));
109 }
110 if claims.iat > now + SKEW_SECS || claims.exp + SKEW_SECS < now {
111 return Err(Error::Token(
112 "expired or not yet valid (check the clocks)".into(),
113 ));
114 }
115 if claims.body != body_hash(body) {
116 return Err(Error::Token("the body does not match the token".into()));
117 }
118 Ok(claims)
119}
120
121#[derive(Debug, Default)]
123pub struct Replays(Mutex<HashMap<String, i64>>);
124
125impl Replays {
126 pub fn first_use(&self, claims: &Claims, now: Timestamp) -> bool {
128 let mut seen = self
129 .0
130 .lock()
131 .unwrap_or_else(std::sync::PoisonError::into_inner);
132 let now = now.as_second();
133 seen.retain(|_, exp| *exp + SKEW_SECS >= now);
134 let key = format!("{}/{}/{}", claims.team, claims.iss, claims.jti);
135 seen.insert(key, claims.exp).is_none()
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn now() -> Timestamp {
144 Timestamp::from_second(1_800_000_000).unwrap()
145 }
146
147 #[test]
148 fn signed_tokens_verify_and_bind_the_body() {
149 let (key, _) = SigningKey::generate().unwrap();
150 let claims = Claims::new("acme", "lead", "ops", 3, b"{}", now());
151 let token = sign(&key, &claims);
152 assert_eq!(peek(&token).unwrap(), claims);
153 assert_eq!(verify(&token, &key.public(), b"{}", now()).unwrap(), claims);
154 assert!(
155 verify(&token, &key.public(), b"{ }", now()).is_err(),
156 "body"
157 );
158 let (other, _) = SigningKey::generate().unwrap();
159 assert!(
160 verify(&token, &other.public(), b"{}", now()).is_err(),
161 "signer"
162 );
163 }
164
165 #[test]
166 fn times_are_checked_with_skew() {
167 let (key, _) = SigningKey::generate().unwrap();
168 let token = sign(&key, &Claims::new("t", "a", "b", 1, b"", now()));
169 let at = |secs: i64| {
170 now()
171 .checked_add(jiff::SignedDuration::from_secs(secs))
172 .unwrap()
173 };
174 assert!(verify(&token, &key.public(), b"", at(-SKEW_SECS)).is_ok());
175 assert!(verify(&token, &key.public(), b"", at(-SKEW_SECS - 1)).is_err());
176 assert!(
177 verify(
178 &token,
179 &key.public(),
180 b"",
181 at(MAX_LIFETIME_SECS + SKEW_SECS)
182 )
183 .is_ok()
184 );
185 assert!(
186 verify(
187 &token,
188 &key.public(),
189 b"",
190 at(MAX_LIFETIME_SECS + SKEW_SECS + 1)
191 )
192 .is_err()
193 );
194 let mut long = Claims::new("t", "a", "b", 1, b"", now());
195 long.exp = long.iat + MAX_LIFETIME_SECS + 1;
196 assert!(verify(&sign(&key, &long), &key.public(), b"", now()).is_err());
197 }
198
199 #[test]
200 fn altered_tokens_are_refused() {
201 let (key, _) = SigningKey::generate().unwrap();
202 let token = sign(&key, &Claims::new("t", "a", "b", 1, b"", now()));
203 let mut forged = Claims::new("t", "a", "admin", 1, b"", now());
204 forged.jti = peek(&token).unwrap().jti;
205 let payload = B64.encode(serde_json::to_vec(&forged).unwrap());
206 let signature = token.rsplit_once('.').unwrap().1;
207 let altered = format!("okp1.{payload}.{signature}");
208 assert!(verify(&altered, &key.public(), b"", now()).is_err());
209 assert!(peek("okp2.a.b").is_err());
210 assert!(peek("okp1.a").is_err());
211 }
212
213 #[test]
214 fn replays_are_refused() {
215 let claims = Claims::new("t", "a", "b", 1, b"", now());
216 let replays = Replays::default();
217 assert!(replays.first_use(&claims, now()));
218 assert!(!replays.first_use(&claims, now()));
219 let later = now()
220 .checked_add(jiff::SignedDuration::from_secs(
221 MAX_LIFETIME_SECS + SKEW_SECS + 1,
222 ))
223 .unwrap();
224 assert!(replays.first_use(&Claims::new("t", "a", "b", 1, b"", later), later));
225 }
226}