1pub mod claims;
74
75use crate::cbor::{self, Decode, Encode, Raw};
76use crate::{cose, xdsa};
77
78#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
80pub enum Error {
81 #[error("cbor: {0}")]
83 Cbor(#[from] cbor::Error),
84 #[error("cose: {0}")]
87 Cose(#[from] cose::Error),
88 #[error("missing nbf claim")]
90 MissingNbf,
91 #[error("duplicate claim key {0}")]
93 DuplicateKey(i64),
94 #[error("token not yet valid: nbf {nbf} > now {now}")]
97 NotYetValid {
98 nbf: u64,
100 now: u64,
102 },
103 #[error("token already expired: exp {exp} <= now {now}")]
106 AlreadyExpired {
107 exp: u64,
109 now: u64,
111 },
112}
113
114pub fn issue(
121 claims: &impl Encode,
122 signer: &xdsa::SecretKey,
123 domain: &[u8],
124) -> Result<Vec<u8>, Error> {
125 let claims_bytes = cbor::encode(claims)?;
126 Ok(cose::sign(Raw(claims_bytes), cbor::NULL, signer, domain)?)
127}
128
129pub fn issue_at(
133 claims: &impl Encode,
134 signer: &xdsa::SecretKey,
135 domain: &[u8],
136 timestamp: i64,
137) -> Result<Vec<u8>, Error> {
138 let claims_bytes = cbor::encode(claims)?;
139 Ok(cose::sign_at(
140 Raw(claims_bytes),
141 cbor::NULL,
142 signer,
143 domain,
144 timestamp,
145 )?)
146}
147
148pub fn verify<T: Decode>(
162 data: &[u8],
163 verifier: &xdsa::PublicKey,
164 domain: &[u8],
165 now: Option<u64>,
166) -> Result<T, Error> {
167 let raw: Raw = cose::verify(data, cbor::NULL, verifier, domain, None)?;
169
170 if let Some(now) = now {
172 let (nbf, exp) = read_temporal_claims(&raw)?;
173 if now < nbf {
174 return Err(Error::NotYetValid { nbf, now });
175 }
176 if let Some(exp) = exp
177 && now >= exp
178 {
179 return Err(Error::AlreadyExpired { exp, now });
180 }
181 }
182 Ok(cbor::decode(&raw.0)?)
184}
185
186pub fn signer(data: &[u8]) -> Result<xdsa::Fingerprint, Error> {
189 Ok(cose::signer(data)?)
190}
191
192pub fn peek<T: Decode>(data: &[u8]) -> Result<T, Error> {
199 let raw: Raw = cose::peek(data)?;
200 Ok(cbor::decode(&raw.0)?)
201}
202
203fn read_temporal_claims(raw: &[u8]) -> Result<(u64, Option<u64>), Error> {
206 let mut dec = cbor::Decoder::new(raw);
207 let n = dec.decode_map_header()?;
208
209 let mut nbf: Option<u64> = None;
210 let mut exp: Option<u64> = None;
211
212 for _ in 0..n {
213 let key = dec.decode_int()?;
214 match key {
215 4 => {
216 if exp.is_some() {
218 return Err(Error::DuplicateKey(4));
219 }
220 exp = Some(dec.decode_uint()?);
221 }
222 5 => {
223 if nbf.is_some() {
225 return Err(Error::DuplicateKey(5));
226 }
227 nbf = Some(dec.decode_uint()?);
228 }
229 _ => {
230 Raw::decode_cbor_notrail(&mut dec)?;
232 }
233 }
234 }
235 let nbf = nbf.ok_or(Error::MissingNbf)?;
236 Ok((nbf, exp))
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::cbor::Cbor;
243 use crate::cwt::claims;
244 use crate::cwt::claims::eat;
245
246 #[derive(Debug, Cbor)]
248 struct SimpleCert {
249 #[cbor(embed)]
250 sub: claims::Subject,
251 #[cbor(embed)]
252 exp: Option<claims::Expiration>,
253 #[cbor(embed)]
254 nbf: claims::NotBefore,
255 #[cbor(embed)]
256 cnf: claims::Confirm<xdsa::PublicKey>,
257 }
258
259 #[derive(Debug, Cbor)]
261 struct DeviceCert {
262 #[cbor(embed)]
263 sub: claims::Subject,
264 #[cbor(embed)]
265 exp: Option<claims::Expiration>,
266 #[cbor(embed)]
267 nbf: claims::NotBefore,
268 #[cbor(embed)]
269 cnf: claims::Confirm<xdsa::PublicKey>,
270 #[cbor(embed)]
271 ueid: eat::Ueid,
272 }
273
274 #[derive(Debug, Cbor)]
276 struct NoNbfCert {
277 #[cbor(embed)]
278 sub: claims::Subject,
279 #[cbor(embed)]
280 cnf: claims::Confirm<xdsa::PublicKey>,
281 }
282
283 #[derive(Debug, Cbor)]
285 struct NoExpCert {
286 #[cbor(embed)]
287 sub: claims::Subject,
288 #[cbor(embed)]
289 nbf: claims::NotBefore,
290 #[cbor(embed)]
291 cnf: claims::Confirm<xdsa::PublicKey>,
292 }
293
294 #[test]
296 fn test_issue_verify() {
297 let issuer = xdsa::SecretKey::generate();
298 let device = xdsa::SecretKey::generate();
299
300 let cert = DeviceCert {
301 sub: claims::Subject {
302 sub: "device-abc".into(),
303 },
304 exp: Some(claims::Expiration { exp: 2000000 }),
305 nbf: claims::NotBefore { nbf: 1000000 },
306 cnf: claims::Confirm::new(device.public_key()),
307 ueid: eat::Ueid {
308 ueid: b"SN-999".to_vec(),
309 },
310 };
311 let token = issue(&cert, &issuer, b"test-domain").expect("issue");
312 let got: DeviceCert =
313 verify(&token, &issuer.public_key(), b"test-domain", Some(1500000)).expect("verify");
314
315 assert_eq!(got.sub.sub, "device-abc");
316 assert_eq!(got.exp.unwrap().exp, 2000000);
317 assert_eq!(got.cnf.key().to_bytes(), device.public_key().to_bytes(),);
318 assert_eq!(got.ueid.ueid, b"SN-999");
319 }
320
321 #[test]
323 fn test_verify_skip_time() {
324 let issuer = xdsa::SecretKey::generate();
325
326 let cert = SimpleCert {
327 sub: claims::Subject { sub: "test".into() },
328 exp: None,
329 nbf: claims::NotBefore { nbf: 1000000 },
330 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
331 };
332 let token = issue(&cert, &issuer, b"test").expect("issue");
333 let got: SimpleCert =
334 verify(&token, &issuer.public_key(), b"test", None).expect("verify with None time");
335
336 assert_eq!(got.sub.sub, "test");
337 }
338
339 #[test]
341 fn test_verify_not_yet_valid() {
342 let issuer = xdsa::SecretKey::generate();
343
344 let cert = SimpleCert {
345 sub: claims::Subject { sub: "test".into() },
346 exp: None,
347 nbf: claims::NotBefore { nbf: 1000000 },
348 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
349 };
350 let token = issue(&cert, &issuer, b"test").expect("issue");
351 let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(500000))
352 .expect_err("should fail");
353
354 assert!(matches!(err, Error::NotYetValid { .. }));
355 }
356
357 #[test]
359 fn test_verify_expired() {
360 let issuer = xdsa::SecretKey::generate();
361
362 let cert = SimpleCert {
363 sub: claims::Subject { sub: "test".into() },
364 exp: Some(claims::Expiration { exp: 2000000 }),
365 nbf: claims::NotBefore { nbf: 1000000 },
366 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
367 };
368 let token = issue(&cert, &issuer, b"test").expect("issue");
369 let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(3000000))
370 .expect_err("should fail");
371
372 assert!(matches!(err, Error::AlreadyExpired { .. }));
373 }
374
375 #[test]
377 fn test_verify_missing_nbf() {
378 let issuer = xdsa::SecretKey::generate();
379
380 let cert = NoNbfCert {
381 sub: claims::Subject { sub: "test".into() },
382 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
383 };
384 let token = issue(&cert, &issuer, b"test").expect("issue");
385 let err = verify::<NoNbfCert>(&token, &issuer.public_key(), b"test", Some(1000000))
386 .expect_err("should fail");
387
388 assert!(matches!(err, Error::MissingNbf));
389 }
390
391 #[test]
393 fn test_verify_wrong_key() {
394 let issuer = xdsa::SecretKey::generate();
395 let wrong = xdsa::SecretKey::generate();
396
397 let cert = SimpleCert {
398 sub: claims::Subject { sub: "test".into() },
399 exp: None,
400 nbf: claims::NotBefore { nbf: 1000000 },
401 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
402 };
403 let token = issue(&cert, &issuer, b"test").expect("issue");
404
405 assert!(verify::<SimpleCert>(&token, &wrong.public_key(), b"test", Some(1500000)).is_err());
406 }
407
408 #[test]
410 fn test_signer() {
411 let issuer = xdsa::SecretKey::generate();
412
413 let cert = SimpleCert {
414 sub: claims::Subject { sub: "test".into() },
415 exp: None,
416 nbf: claims::NotBefore { nbf: 1000000 },
417 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
418 };
419 let token = issue(&cert, &issuer, b"test").expect("issue");
420 let fp = signer(&token).expect("signer");
421
422 assert_eq!(fp, issuer.public_key().fingerprint());
423 }
424
425 #[test]
427 fn test_peek() {
428 let issuer = xdsa::SecretKey::generate();
429
430 let cert = SimpleCert {
431 sub: claims::Subject {
432 sub: "peek-test".into(),
433 },
434 exp: None,
435 nbf: claims::NotBefore { nbf: 1000000 },
436 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
437 };
438 let token = issue(&cert, &issuer, b"test").expect("issue");
439 let got: SimpleCert = peek(&token).expect("peek");
440
441 assert_eq!(got.sub.sub, "peek-test");
442 }
443
444 #[test]
446 fn test_verify_wrong_domain() {
447 let issuer = xdsa::SecretKey::generate();
448
449 let cert = SimpleCert {
450 sub: claims::Subject { sub: "test".into() },
451 exp: None,
452 nbf: claims::NotBefore { nbf: 1000000 },
453 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
454 };
455 let token = issue(&cert, &issuer, b"domain-a").expect("issue");
456
457 assert!(
458 verify::<SimpleCert>(&token, &issuer.public_key(), b"domain-b", Some(1500000)).is_err()
459 );
460 }
461
462 #[test]
464 fn test_verify_boundary_exact() {
465 let issuer = xdsa::SecretKey::generate();
466
467 let cert = SimpleCert {
468 sub: claims::Subject { sub: "test".into() },
469 exp: Some(claims::Expiration { exp: 2000000 }),
470 nbf: claims::NotBefore { nbf: 1000000 },
471 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
472 };
473 let token = issue(&cert, &issuer, b"test").expect("issue");
474
475 verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1000000))
477 .expect("now == nbf should pass");
478
479 let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(2000000))
481 .expect_err("now == exp should fail");
482 assert!(matches!(err, Error::AlreadyExpired { .. }));
483 }
484
485 #[test]
487 fn test_verify_no_expiration() {
488 let issuer = xdsa::SecretKey::generate();
489
490 let cert = NoExpCert {
491 sub: claims::Subject { sub: "test".into() },
492 nbf: claims::NotBefore { nbf: 1000000 },
493 cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
494 };
495 let token = issue(&cert, &issuer, b"test").expect("issue");
496
497 verify::<NoExpCert>(&token, &issuer.public_key(), b"test", Some(99999999))
499 .expect("no exp should pass");
500 }
501
502 #[test]
504 fn test_verify_duplicate_nbf() {
505 let issuer = xdsa::SecretKey::generate();
506
507 let mut enc = cbor::Encoder::new();
509 enc.encode_map_header(3);
510 enc.encode_int(2);
511 enc.encode_text("test");
512 enc.encode_int(5);
513 enc.encode_uint(1000000);
514 enc.encode_int(5); enc.encode_uint(2000000);
516
517 let token = cose::sign(Raw(enc.finish()), cbor::NULL, &issuer, b"test").expect("sign");
519
520 let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1500000))
522 .expect_err("should reject duplicate nbf");
523 assert!(matches!(err, Error::DuplicateKey(5)));
524 }
525
526 const FIXTURES: &str = include_str!("testdata/v0.16.json");
528
529 fn fixture(corpus: &serde_json::Value, key: &str) -> Vec<u8> {
531 hex::decode(corpus[key].as_str().unwrap()).unwrap()
532 }
533
534 #[test]
537 fn test_v0_16_fixtures() {
538 let corpus: serde_json::Value = serde_json::from_str(FIXTURES).unwrap();
539
540 let xdsa_seed: [u8; 64] = fixture(&corpus, "xdsa_seed").try_into().unwrap();
541 let signer = xdsa::SecretKey::from_bytes(&xdsa_seed);
542
543 let domain = fixture(&corpus, "domain");
544 let now = corpus["now"].as_u64().unwrap();
545
546 let valid = fixture(&corpus, "valid");
548 let got: SimpleCert = verify(&valid, &signer.public_key(), &domain, Some(now)).unwrap();
549 assert_eq!(got.sub.sub, "fixture");
550 assert_eq!(got.exp.unwrap().exp, 4102444800);
551 assert_eq!(got.nbf.nbf, 1500000000);
552 assert_eq!(got.cnf.key().to_bytes(), signer.public_key().to_bytes());
553
554 let expired = fixture(&corpus, "expired");
556 let err = verify::<SimpleCert>(&expired, &signer.public_key(), &domain, Some(now));
557 assert!(matches!(err, Err(Error::AlreadyExpired { .. })));
558
559 let premature = fixture(&corpus, "premature");
560 let err = verify::<NoExpCert>(&premature, &signer.public_key(), &domain, Some(now));
561 assert!(matches!(err, Err(Error::NotYetValid { .. })));
562
563 let mut tampered = valid.clone();
565 *tampered.last_mut().unwrap() ^= 1;
566 let err = verify::<SimpleCert>(&tampered, &signer.public_key(), &domain, Some(now));
567 assert!(err.is_err());
568 }
569}