1use std::collections::BTreeMap;
4
5use base64::Engine;
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7use ed25519_dalek::{Signature, VerifyingKey};
8use serde::Deserialize;
9
10use crate::Error;
11use crate::rights::{Claims, Decision, Mode, Right, audience_owns, in_prefix};
12
13pub const CLOCK_SKEW: u64 = 5;
18
19fn check_start(claims: &Claims, now: u64) -> Result<(), Error> {
20 let starts_at = claims.iat.max(claims.nbf.unwrap_or(claims.iat));
21 let skew = starts_at.saturating_sub(now);
22 if skew > CLOCK_SKEW {
23 return Err(Error::NotYetValid {
24 skew_ms: u128::from(skew) * 1000,
25 });
26 }
27 Ok(())
28}
29
30#[derive(Deserialize)]
31struct Header {
32 alg: String,
33 typ: String,
34 kid: String,
35 crit: Option<Vec<String>>,
36 b64: Option<bool>,
37}
38
39#[derive(Deserialize)]
40struct Jwk {
41 kty: String,
42 crv: String,
43 kid: String,
44 x: String,
45 alg: Option<String>,
46 #[serde(rename = "use")]
47 usage: Option<String>,
48 key_ops: Option<Vec<String>>,
49}
50
51#[derive(Deserialize)]
52struct Jwks {
53 keys: Vec<Jwk>,
54}
55
56#[derive(Debug)]
58pub struct KeySet {
59 keys: BTreeMap<String, VerifyingKey>,
60}
61
62impl KeySet {
63 pub fn from_json(json: &str) -> Result<Self, Error> {
71 let published: Jwks = serde_json::from_str(json)?;
72 let mut keys = BTreeMap::new();
73 for key in published.keys {
74 if key.kty != "OKP"
75 || key.crv != "Ed25519"
76 || key.kid.is_empty()
77 || key.kid.chars().any(char::is_control)
78 || key.alg.as_deref().is_some_and(|alg| alg != "EdDSA")
79 || key.usage.as_deref().is_some_and(|usage| usage != "sig")
80 || key
81 .key_ops
82 .as_ref()
83 .is_some_and(|ops| !ops.iter().any(|op| op == "verify"))
84 {
85 return Err(Error::Invalid(
86 "published key is not an Ed25519 signing key",
87 ));
88 }
89 let bytes = URL_SAFE_NO_PAD.decode(&key.x)?;
90 let bytes: [u8; 32] = bytes.try_into().map_err(|bytes: Vec<u8>| {
91 if bytes.is_empty() {
92 Error::Invalid("published key is empty")
93 } else {
94 Error::Invalid("published Ed25519 key has the wrong length")
95 }
96 })?;
97 let public = VerifyingKey::from_bytes(&bytes).map_err(Error::Signature)?;
98 if public.is_weak() || keys.insert(key.kid, public).is_some() {
99 return Err(Error::Invalid("published key is weak or duplicated"));
100 }
101 }
102 Ok(Self { keys })
103 }
104}
105
106pub(crate) fn signed_payload(token: &str, keys: &KeySet, typ: &str) -> Result<Vec<u8>, Error> {
110 let mut parts = token.split('.');
111 let header = parts.next().ok_or(Error::Invalid("missing header"))?;
112 let payload = parts.next().ok_or(Error::Invalid("missing claims"))?;
113 let signature = parts.next().ok_or(Error::Invalid("missing signature"))?;
114 if parts.next().is_some() {
115 return Err(Error::Invalid("too many token segments"));
116 }
117 let decoded: Header = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(header)?)?;
118 if decoded.alg != "EdDSA"
119 || decoded.typ != typ
120 || decoded.kid.is_empty()
121 || decoded.kid.chars().any(char::is_control)
122 || decoded
123 .crit
124 .as_ref()
125 .is_some_and(|values| !values.is_empty())
126 || decoded.b64 == Some(false)
127 {
128 return Err(Error::Invalid("unsupported signed token header"));
129 }
130 let key = keys
131 .keys
132 .get(&decoded.kid)
133 .ok_or_else(|| Error::UnpublishedKey(decoded.kid.clone()))?;
134 let signature =
135 Signature::from_slice(&URL_SAFE_NO_PAD.decode(signature)?).map_err(Error::Signature)?;
136 key.verify_strict(format!("{header}.{payload}").as_bytes(), &signature)
137 .map_err(Error::Signature)?;
138 Ok(URL_SAFE_NO_PAD.decode(payload)?)
139}
140
141type ActionIndex = BTreeMap<String, Vec<usize>>;
142type ResourceIndex = BTreeMap<String, ActionIndex>;
143
144#[derive(Debug)]
146pub struct VerifiedPass {
147 claims: Claims,
148 index: BTreeMap<String, ResourceIndex>,
149}
150
151impl VerifiedPass {
152 pub fn verify(
164 token: &str,
165 keys: &KeySet,
166 issuer: &str,
167 audience: &str,
168 now: u64,
169 ) -> Result<Self, Error> {
170 if issuer.is_empty() || audience.is_empty() {
171 return Err(Error::Invalid("trusted issuer or audience is missing"));
172 }
173 let claims: Claims = serde_json::from_slice(&signed_payload(token, keys, "JWT")?)?;
174 if claims.iss != issuer {
175 return Err(Error::WrongIssuer);
176 }
177 if claims.aud != audience {
178 return Err(Error::WrongAudience);
179 }
180 if claims.sub.is_empty()
181 || claims.sub != claims.holder.id
182 || !matches!(
183 claims.holder.kind.as_str(),
184 "person" | "agent" | "connector" | "service_account" | "machine"
185 )
186 || (claims.holder.kind != "person"
187 && claims
188 .holder
189 .responsible
190 .as_deref()
191 .is_none_or(str::is_empty))
192 {
193 return Err(Error::Invalid("holder or responsibility is missing"));
194 }
195 if claims.exp <= claims.iat {
196 return Err(Error::Invalid("pass lifetime is empty"));
197 }
198 if claims.exp <= now {
199 return Err(Error::Expired);
200 }
201 check_start(&claims, now)?;
202 if claims
203 .rights_truncated
204 .as_deref()
205 .is_some_and(|app| app != audience)
206 {
207 return Err(Error::RightsOutsideAudience);
208 }
209 let mut index: BTreeMap<String, ResourceIndex> = BTreeMap::new();
210 for (position, right) in claims.rights.iter().enumerate() {
211 if !audience_owns(&right.resource.kind, audience) {
212 return Err(Error::RightsOutsideAudience);
213 }
214 if right.resource.kind.is_empty()
215 || right.resource.id.is_empty()
216 || right.grant.is_empty()
217 || right.actions.is_empty()
218 || right.resource.kind.chars().any(char::is_control)
219 || right.resource.id.chars().any(char::is_control)
220 {
221 return Err(Error::Invalid("effective right is incomplete"));
222 }
223 for action in &right.actions {
224 if action.is_empty() || action.chars().any(char::is_control) {
225 return Err(Error::Invalid("effective action is invalid"));
226 }
227 index
228 .entry(right.resource.kind.clone())
229 .or_default()
230 .entry(right.resource.id.clone())
231 .or_default()
232 .entry(action.clone())
233 .or_default()
234 .push(position);
235 }
236 }
237 Ok(Self { claims, index })
238 }
239
240 #[must_use]
242 pub const fn claims(&self) -> &Claims {
243 &self.claims
244 }
245
246 pub fn evaluate(
254 &self,
255 kind: &str,
256 id: &str,
257 action: &str,
258 now: u64,
259 ) -> Result<Decision<'_>, Error> {
260 if self.claims.exp <= now {
261 return Err(Error::Expired);
262 }
263 check_start(&self.claims, now)?;
264 if let Some(app) = &self.claims.rights_truncated {
265 return Err(Error::Truncated(app.clone()));
266 }
267 let Some(entries) = self
268 .index
269 .get(kind)
270 .and_then(|ids| ids.get(id))
271 .and_then(|actions| actions.get(action))
272 else {
273 return Ok(Decision::Refused);
274 };
275 let mut held = None;
276 for position in entries {
277 let right = self
278 .claims
279 .rights
280 .get(*position)
281 .ok_or(Error::Invalid("right index is inconsistent"))?;
282 if right.mode == Mode::Outright {
283 return Ok(Decision::Allowed {
284 grant: &right.grant,
285 });
286 }
287 if held.is_none() {
288 held = Some(Decision::Held {
289 grant: &right.grant,
290 mode: right.mode,
291 });
292 }
293 }
294 Ok(held.unwrap_or(Decision::Refused))
295 }
296
297 pub fn hot_allowed(&self, kind: &str, id: &str, action: &str, now: u64) -> Result<bool, Error> {
305 Ok(matches!(
306 self.evaluate(kind, id, action, now)?,
307 Decision::Allowed { .. }
308 ))
309 }
310
311 pub fn rights_in_prefix<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = &'a Right> + 'a {
313 self.claims
314 .rights
315 .iter()
316 .filter(move |right| in_prefix(&right.resource.kind, prefix))
317 }
318}