1use alloc::collections::BTreeSet;
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::time::Duration;
15
16use crate::error::ClaimsError;
17use crate::hash::RequestHash;
18use crate::label;
19use crate::types::{KeyId, MessageId, ResponseSubject, Subject, UnixTime};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Claims {
28 pub issuer: Option<Subject>,
30 pub audience: Option<Subject>,
32 pub expires_at: Option<UnixTime>,
35 pub issued_at: UnixTime,
37 pub message_id: MessageId,
39 pub sender_key_id: Option<KeyId>,
41 pub response_key_id: Option<KeyId>,
43 pub response_subject: Option<ResponseSubject>,
45 pub in_reply_to: Option<MessageId>,
47 pub request_hash: Option<RequestHash>,
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct ProtectedHeaders {
54 pub signer_certificates_jwt: Vec<String>,
56}
57
58impl ProtectedHeaders {
59 #[must_use]
61 pub const fn is_empty(&self) -> bool {
62 self.signer_certificates_jwt.is_empty()
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum MessageRole {
69 Request,
72 Response,
75 Peer,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ValidationParams {
85 pub now: UnixTime,
87 pub max_clock_skew: Duration,
89 pub max_ttl: Duration,
91 pub default_ttl: Duration,
93 pub allowed_audiences: BTreeSet<Subject>,
96 pub role: MessageRole,
98}
99
100#[allow(clippy::cast_possible_wrap)]
102const fn secs_i64(d: Duration) -> i64 {
103 let s = d.as_secs();
104 if s > i64::MAX as u64 {
105 i64::MAX
106 } else {
107 s as i64
108 }
109}
110
111impl Claims {
112 pub fn validate(&self, params: &ValidationParams) -> Result<(), ClaimsError> {
120 let skew = secs_i64(params.max_clock_skew);
121 let iat = self.issued_at.0;
122 let now = params.now.0;
123
124 if iat > now.saturating_add(skew) {
125 return Err(ClaimsError::IssuedInFuture);
126 }
127
128 let effective_exp = match self.expires_at {
129 Some(UnixTime(exp)) => {
130 let span = exp.saturating_sub(iat);
131 if span <= 0 {
132 return Err(ClaimsError::NonPositiveTtl);
133 }
134 if span > secs_i64(params.max_ttl) {
135 return Err(ClaimsError::TtlTooLong { seconds: span });
136 }
137 exp
138 }
139 None => iat.saturating_add(secs_i64(params.default_ttl)),
140 };
141 if now > effective_exp.saturating_add(skew) {
142 return Err(ClaimsError::Expired);
143 }
144
145 if !params.allowed_audiences.is_empty()
146 && let Some(aud) = &self.audience
147 && !params.allowed_audiences.contains(aud)
148 {
149 return Err(ClaimsError::AudienceRejected);
150 }
151
152 self.validate_role(params.role)
153 }
154
155 pub fn validate_role(&self, role: MessageRole) -> Result<(), ClaimsError> {
162 let require = |present: bool, l: i64| {
163 if present {
164 Ok(())
165 } else {
166 Err(ClaimsError::MissingClaim { label: l })
167 }
168 };
169 let forbid = |present: bool, l: i64| {
170 if present {
171 Err(ClaimsError::ForbiddenClaim { label: l })
172 } else {
173 Ok(())
174 }
175 };
176 match role {
177 MessageRole::Request => {
178 require(self.sender_key_id.is_some(), label::SENDER_KEY_ID)?;
179 require(self.response_key_id.is_some(), label::RESPONSE_KEY_ID)?;
180 forbid(self.in_reply_to.is_some(), label::IN_REPLY_TO)?;
181 forbid(self.request_hash.is_some(), label::REQUEST_HASH)
182 }
183 MessageRole::Response => {
184 require(self.in_reply_to.is_some(), label::IN_REPLY_TO)?;
185 require(self.request_hash.is_some(), label::REQUEST_HASH)?;
186 forbid(self.response_key_id.is_some(), label::RESPONSE_KEY_ID)?;
187 forbid(self.response_subject.is_some(), label::RESPONSE_SUBJECT)
188 }
189 MessageRole::Peer => {
190 require(self.sender_key_id.is_some(), label::SENDER_KEY_ID)?;
191 forbid(self.in_reply_to.is_some(), label::IN_REPLY_TO)?;
192 forbid(self.request_hash.is_some(), label::REQUEST_HASH)?;
193 forbid(self.response_key_id.is_some(), label::RESPONSE_KEY_ID)?;
194 forbid(self.response_subject.is_some(), label::RESPONSE_SUBJECT)
195 }
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use alloc::string::ToString;
204 use alloc::vec;
205
206 fn base_claims() -> Claims {
207 Claims {
208 issuer: None,
209 audience: None,
210 expires_at: None,
211 issued_at: UnixTime(1_000),
212 message_id: MessageId::from_bytes(vec![1, 2, 3]).unwrap(),
213 sender_key_id: Some(KeyId::from_text("alice").unwrap()),
214 response_key_id: None,
215 response_subject: None,
216 in_reply_to: None,
217 request_hash: None,
218 }
219 }
220
221 fn params(now: i64) -> ValidationParams {
222 ValidationParams {
223 now: UnixTime(now),
224 max_clock_skew: Duration::from_secs(5),
225 max_ttl: Duration::from_mins(5),
226 default_ttl: Duration::from_mins(1),
227 allowed_audiences: BTreeSet::new(),
228 role: MessageRole::Peer,
229 }
230 }
231
232 #[test]
233 fn accepts_fresh_message() {
234 assert_eq!(base_claims().validate(¶ms(1_010)), Ok(()));
235 }
236
237 #[test]
238 fn rejects_future_iat_beyond_skew() {
239 assert_eq!(
240 base_claims().validate(¶ms(990)),
241 Err(ClaimsError::IssuedInFuture)
242 );
243 }
244
245 #[test]
246 fn accepts_future_iat_within_skew() {
247 assert_eq!(base_claims().validate(¶ms(996)), Ok(()));
248 }
249
250 #[test]
251 fn rejects_expired_default_ttl() {
252 assert_eq!(base_claims().validate(¶ms(1_065)), Ok(()));
254 assert_eq!(
255 base_claims().validate(¶ms(1_066)),
256 Err(ClaimsError::Expired)
257 );
258 }
259
260 #[test]
261 fn explicit_exp_overrides_default() {
262 let mut c = base_claims();
263 c.expires_at = Some(UnixTime(1_100));
264 assert_eq!(c.validate(¶ms(1_105)), Ok(()));
265 assert_eq!(c.validate(¶ms(1_106)), Err(ClaimsError::Expired));
266 }
267
268 #[test]
269 fn rejects_over_long_explicit_ttl() {
270 let mut c = base_claims();
271 c.expires_at = Some(UnixTime(1_000 + 301));
272 assert_eq!(
273 c.validate(¶ms(1_010)),
274 Err(ClaimsError::TtlTooLong { seconds: 301 })
275 );
276 }
277
278 #[test]
279 fn rejects_non_positive_ttl() {
280 let mut c = base_claims();
281 c.expires_at = Some(UnixTime(1_000));
282 assert_eq!(c.validate(¶ms(1_010)), Err(ClaimsError::NonPositiveTtl));
283 }
284
285 #[test]
286 fn audience_rules() {
287 let mut p = params(1_010);
288 p.allowed_audiences
289 .insert(Subject::new("svc-a".to_string()).unwrap());
290 assert_eq!(base_claims().validate(&p), Ok(()));
292 let mut c = base_claims();
294 c.audience = Some(Subject::new("svc-a".to_string()).unwrap());
295 assert_eq!(c.validate(&p), Ok(()));
296 c.audience = Some(Subject::new("svc-b".to_string()).unwrap());
298 assert_eq!(c.validate(&p), Err(ClaimsError::AudienceRejected));
299 assert_eq!(c.validate(¶ms(1_010)), Ok(()));
301 }
302
303 #[test]
304 fn request_role_shape() {
305 let mut c = base_claims();
306 assert_eq!(
307 c.validate_role(MessageRole::Request),
308 Err(ClaimsError::MissingClaim {
309 label: label::RESPONSE_KEY_ID
310 })
311 );
312 c.response_key_id = Some(KeyId::from_text("bob").unwrap());
313 assert_eq!(c.validate_role(MessageRole::Request), Ok(()));
314 c.request_hash = Some(RequestHash([0; 32]));
315 assert_eq!(
316 c.validate_role(MessageRole::Request),
317 Err(ClaimsError::ForbiddenClaim {
318 label: label::REQUEST_HASH
319 })
320 );
321 }
322
323 #[test]
324 fn response_role_shape() {
325 let mut c = base_claims();
326 assert_eq!(
327 c.validate_role(MessageRole::Response),
328 Err(ClaimsError::MissingClaim {
329 label: label::IN_REPLY_TO
330 })
331 );
332 c.in_reply_to = Some(MessageId::from_bytes(vec![9]).unwrap());
333 c.request_hash = Some(RequestHash([0; 32]));
334 assert_eq!(c.validate_role(MessageRole::Response), Ok(()));
335 c.response_subject = Some(ResponseSubject::new("inbox".to_string()).unwrap());
336 assert_eq!(
337 c.validate_role(MessageRole::Response),
338 Err(ClaimsError::ForbiddenClaim {
339 label: label::RESPONSE_SUBJECT
340 })
341 );
342 }
343
344 #[test]
345 fn peer_role_shape() {
346 let mut c = base_claims();
347 assert_eq!(c.validate_role(MessageRole::Peer), Ok(()));
348 c.sender_key_id = None;
349 assert_eq!(
350 c.validate_role(MessageRole::Peer),
351 Err(ClaimsError::MissingClaim {
352 label: label::SENDER_KEY_ID
353 })
354 );
355 }
356}