1use std::collections::{BTreeMap, HashMap};
46use std::sync::{PoisonError, RwLock};
47
48use serde::{Deserialize, Serialize};
49
50use cheers_core::{DeviceId, LeaseError, LeaseState, PeerKey, StandingBinding, StoreError};
51
52use crate::artifact::IssuerTrust;
53use crate::key_set::VerifyError;
54use crate::revocation::RevocationReader;
55
56#[derive(Debug, thiserror::Error)]
59pub enum StandingError {
60 #[error("standing binding refused: {0}")]
64 Verify(#[from] VerifyError),
65
66 #[error("standing binding is not bound to the presented peer key")]
68 PeerKeyMismatch,
69
70 #[error("standing binding for device {device:?} is superseded: seq {offered} is below {newest}")]
72 Superseded { device: DeviceId, newest: u64, offered: u64 },
73
74 #[error("standing binding {jti:?} is revoked")]
76 Revoked { jti: String },
77
78 #[error("device {device:?} is revoked")]
80 DeviceRevoked { device: DeviceId },
81
82 #[error("standing binding refused: {0}")]
84 Lease(#[from] LeaseError),
85
86 #[error(transparent)]
88 Store(#[from] StoreError),
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct VerifiedStanding {
94 pub binding: StandingBinding,
95 pub lease: LeaseState,
99}
100
101#[derive(Debug, thiserror::Error)]
104#[error("ledger belongs to issuer {got:?}, not {expected:?}")]
105pub struct ForeignLedger {
106 pub expected: String,
107 pub got: String,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct LedgerDoc {
113 pub issuer: String,
114 pub newest: BTreeMap<String, u64>,
115}
116
117#[derive(Debug)]
125pub struct BindingLedger {
126 issuer: String,
127 newest: RwLock<HashMap<DeviceId, u64>>,
128}
129
130impl BindingLedger {
131 pub fn new(issuer: impl Into<String>) -> Self {
132 Self {
133 issuer: issuer.into(),
134 newest: RwLock::new(HashMap::new()),
135 }
136 }
137
138 pub fn issuer(&self) -> &str {
139 &self.issuer
140 }
141
142 pub fn newest(&self, device: &DeviceId) -> Option<u64> {
144 self.newest
145 .read()
146 .unwrap_or_else(PoisonError::into_inner)
147 .get(device)
148 .copied()
149 }
150
151 pub fn observe(&self, device: &DeviceId, seq: u64) -> u64 {
154 let mut newest = self.newest.write().unwrap_or_else(PoisonError::into_inner);
155 let held = newest.entry(device.clone()).or_insert(seq);
156 *held = (*held).max(seq);
157 *held
158 }
159
160 pub fn export(&self) -> LedgerDoc {
162 let newest = self.newest.read().unwrap_or_else(PoisonError::into_inner);
163 LedgerDoc {
164 issuer: self.issuer.clone(),
165 newest: newest.iter().map(|(d, s)| (d.as_str().to_owned(), *s)).collect(),
166 }
167 }
168
169 pub fn import(&self, doc: &LedgerDoc) -> Result<(), ForeignLedger> {
171 if doc.issuer != self.issuer {
172 return Err(ForeignLedger {
173 expected: self.issuer.clone(),
174 got: doc.issuer.clone(),
175 });
176 }
177 for (device, seq) in &doc.newest {
178 self.observe(&DeviceId::new(device.clone()), *seq);
179 }
180 Ok(())
181 }
182}
183
184pub struct StandingVerifier<Rd> {
187 trust: IssuerTrust,
188 revoked: Rd,
189 ledger: BindingLedger,
190}
191
192impl<Rd> std::fmt::Debug for StandingVerifier<Rd> {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.debug_struct("StandingVerifier")
195 .field("trust", &self.trust)
196 .field("ledger", &self.ledger)
197 .finish_non_exhaustive()
198 }
199}
200
201impl<Rd: RevocationReader> StandingVerifier<Rd> {
202 pub fn new(trust: IssuerTrust, revoked: Rd) -> Self {
205 let ledger = BindingLedger::new(trust.issuer());
206 Self { trust, revoked, ledger }
207 }
208
209 pub fn trust(&self) -> &IssuerTrust {
210 &self.trust
211 }
212
213 pub fn revocations(&self) -> &Rd {
214 &self.revoked
215 }
216
217 pub fn ledger(&self) -> &BindingLedger {
218 &self.ledger
219 }
220
221 pub async fn adopt(&self, token: &str) -> Result<StandingBinding, VerifyError> {
227 let binding: StandingBinding = self.trust.verify(token).await?;
228 self.ledger.observe(&binding.device, binding.seq);
229 Ok(binding)
230 }
231
232 pub async fn verify_standing_at(
240 &self,
241 token: &str,
242 presented: &PeerKey,
243 now: i64,
244 ) -> Result<VerifiedStanding, StandingError> {
245 let binding: StandingBinding = self.trust.verify(token).await?;
246 binding.lease.validate(binding.iat)?;
247 let newest = self.ledger.observe(&binding.device, binding.seq);
248 if !binding.is_bound_to(presented) {
249 return Err(StandingError::PeerKeyMismatch);
250 }
251 if newest > binding.seq {
252 return Err(StandingError::Superseded {
253 device: binding.device,
254 newest,
255 offered: binding.seq,
256 });
257 }
258 if self.revoked.is_revoked(&binding.jti).await? {
259 return Err(StandingError::Revoked { jti: binding.jti });
260 }
261 if self.revoked.is_device_revoked(&binding.device, binding.seq).await? {
262 return Err(StandingError::DeviceRevoked { device: binding.device });
263 }
264 let lease = binding.lease.state_at(now);
265 Ok(VerifiedStanding { binding, lease })
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use cheers_core::Lease;
273 use crate::artifact::{AnchorDoc, TrustDoc};
274 use crate::jwks::{JwkKey, JwksDoc, KeySet};
275 use crate::key_set::KeySetVerifier;
276 use crate::public_verifier::PasetoV4PublicVerifier;
277 use crate::revocation::ReplicatedRevocations;
278 use cheers_core::{
279 Claims, DeviceBinding, KeyRole, McpClaims, PrincipalId, RevocationSet, Revoked, SignedArtifact, TokenVerifier,
280 UserId,
281 };
282 use pasetors::keys::{AsymmetricKeyPair, AsymmetricSecretKey, Generate};
283 use pasetors::version4::{PublicToken, V4};
284 use std::sync::Arc;
285
286 const ISS: &str = "https://cheers.test";
287 const DEVICE: &str = "node:a";
288 const IAT: i64 = 1_000;
289 const YEARS_LATER: i64 = IAT + 10 * 365 * 24 * 60 * 60;
291
292 fn kp() -> AsymmetricKeyPair<V4> {
293 AsymmetricKeyPair::<V4>::generate().unwrap()
294 }
295
296 fn pub_bytes(k: &AsymmetricKeyPair<V4>) -> [u8; 32] {
297 k.public.as_bytes().try_into().unwrap()
298 }
299
300 fn node(b: u8) -> PeerKey {
301 PeerKey::ed25519([b; 32])
302 }
303
304 fn sign_with(secret: &AsymmetricSecretKey<V4>, kid: &str, payload: &impl Serialize, ia: Option<&[u8]>) -> String {
306 let body = serde_json::to_vec(payload).unwrap();
307 let footer = format!(r#"{{"kid":"{kid}"}}"#);
308 PublicToken::sign(secret, &body, Some(footer.as_bytes()), ia).unwrap()
309 }
310
311 fn binding(seq: u64, key: PeerKey) -> StandingBinding {
312 StandingBinding {
313 issuer: ISS.into(),
314 sub: UserId::new("alice"),
315 device: DeviceId::new(DEVICE),
316 peer_key: key,
317 seq,
318 iat: IAT,
319 jti: format!("bind-{seq}"),
320 lease: Lease::new(IAT, IAT + 7 * 24 * 60 * 60, None).unwrap(),
321 }
322 }
323
324 struct Rig {
325 issuer: AsymmetricKeyPair<V4>,
326 assertion: AsymmetricKeyPair<V4>,
327 selfsig: AsymmetricKeyPair<V4>,
328 doc: JwksDoc,
329 replica: Arc<ReplicatedRevocations>,
330 edge: StandingVerifier<Arc<ReplicatedRevocations>>,
331 }
332
333 impl Rig {
334 fn sign(&self, b: &StandingBinding) -> String {
335 sign_with(&self.issuer.secret, "iss-1", b, Some(StandingBinding::IMPLICIT_ASSERTION))
336 }
337
338 fn revoke(&self, epoch: u64, revoked: Vec<Revoked>) {
339 let entries = revoked.iter().map(|e| crate::revocation_entry(e, crate::test_revocation_key)).collect();
340 let set = RevocationSet::new(ISS, epoch, entries);
341 let token = sign_with(&self.issuer.secret, "iss-1", &set, Some(RevocationSet::IMPLICIT_ASSERTION));
342 pollster::block_on(self.replica.adopt(&token)).unwrap();
343 }
344
345 fn verify(&self, token: &str, key: &PeerKey, now: i64) -> Result<VerifiedStanding, StandingError> {
346 pollster::block_on(self.edge.verify_standing_at(token, key, now))
347 }
348 }
349
350 fn rig() -> Rig {
351 let (issuer, assertion, selfsig) = (kp(), kp(), kp());
352 let doc = JwksDoc {
353 keys: vec![
354 JwkKey::ed25519("iss-1", &pub_bytes(&issuer), ISS, KeyRole::Issuer),
355 JwkKey::ed25519("asr-1", &pub_bytes(&assertion), "svc:issues", KeyRole::Assertion),
356 JwkKey::ed25519("self-1", &pub_bytes(&selfsig), "svc:issues", KeyRole::SelfSigner),
357 ],
358 };
359 let keys = KeySetVerifier::from_key_set(KeySet::from_doc(doc.clone()).unwrap());
360 let trust = IssuerTrust::key_set(ISS, keys.clone());
361 let replica = Arc::new(ReplicatedRevocations::new(trust.clone()));
362 Rig {
363 edge: StandingVerifier::new(trust, replica.clone()),
364 replica,
365 issuer,
366 assertion,
367 selfsig,
368 doc,
369 }
370 }
371
372 #[test]
373 fn a_binding_verifies_years_after_any_access_window() {
374 let r = rig();
375 let token = r.sign(&binding(5, node(1)));
376 let ok = r.verify(&token, &node(1), YEARS_LATER).unwrap();
377 assert_eq!(ok.binding, binding(5, node(1)));
378 assert_eq!(ok.lease, LeaseState::Warning { exp: None });
379 }
380
381 #[test]
382 fn refresh_after_in_the_past_still_verifies() {
383 let r = rig();
384 let b = binding(5, node(1));
385 let token = r.sign(&b);
386 let ra = b.lease.refresh_after();
387 let warn = LeaseState::Warning { exp: None };
388 assert_eq!(r.verify(&token, &node(1), ra - 1).unwrap().lease, LeaseState::Current);
389 assert_eq!(r.verify(&token, &node(1), ra).unwrap().lease, warn);
390 assert_eq!(r.verify(&token, &node(1), ra + 1).unwrap().lease, warn);
391 }
392
393 #[test]
394 fn a_lease_past_its_midpoint_is_refused() {
395 let r = rig();
396 let mut v = serde_json::to_value(binding(5, node(1))).unwrap();
398 v["refresh_after"] = (IAT + 600).into();
399 v["exp"] = (IAT + 1_000).into();
400 let forged: StandingBinding = serde_json::from_value(v).unwrap();
401 let token = r.sign(&forged);
402 let err = r.verify(&token, &node(1), IAT).unwrap_err();
403 assert!(matches!(err, StandingError::Lease(_)), "got {err:?}");
404 let mut v = serde_json::to_value(binding(5, node(1))).unwrap();
406 v["refresh_after"] = (IAT + 500).into();
407 v["exp"] = (IAT + 1_000).into();
408 let ok: StandingBinding = serde_json::from_value(v).unwrap();
409 let token = r.sign(&ok);
410 assert_eq!(r.verify(&token, &node(1), IAT + 500).unwrap().lease, LeaseState::Warning { exp: Some(IAT + 1_000) });
411 }
412
413 #[test]
414 fn a_wrong_peer_key_is_refused() {
415 let r = rig();
416 let token = r.sign(&binding(5, node(1)));
417 assert!(matches!(r.verify(&token, &node(2), IAT), Err(StandingError::PeerKeyMismatch)));
418 }
419
420 #[test]
421 fn a_revoked_jti_is_refused() {
422 let r = rig();
423 let token = r.sign(&binding(5, node(1)));
424 r.verify(&token, &node(1), IAT).unwrap();
425 r.revoke(1, vec![Revoked::jti("bind-5", None)]);
426 match r.verify(&token, &node(1), IAT) {
427 Err(StandingError::Revoked { jti }) => assert_eq!(jti, "bind-5"),
428 other => panic!("expected Revoked, got {other:?}"),
429 }
430 }
431
432 #[test]
433 fn a_revoked_device_is_refused() {
434 let r = rig();
435 let token = r.sign(&binding(5, node(1)));
436 r.revoke(1, vec![Revoked::device("node:other", 100)]);
437 r.verify(&token, &node(1), IAT).unwrap();
438 r.revoke(2, vec![Revoked::device(DEVICE, 5)]);
440 r.verify(&token, &node(1), IAT).unwrap();
441 r.revoke(3, vec![Revoked::device(DEVICE, 6)]);
442 match r.verify(&token, &node(1), YEARS_LATER) {
443 Err(StandingError::DeviceRevoked { device }) => assert_eq!(device.as_str(), DEVICE),
444 other => panic!("expected DeviceRevoked, got {other:?}"),
445 }
446 let reenrolled = r.sign(&binding(7, node(1)));
448 r.verify(&reenrolled, &node(1), YEARS_LATER).unwrap();
449 }
450
451 #[test]
452 fn a_superseded_binding_is_refused_and_its_successor_accepted() {
453 let r = rig();
454 let old = r.sign(&binding(5, node(1)));
455 let new = r.sign(&binding(7, node(2)));
457 r.verify(&old, &node(1), IAT).unwrap();
458 r.verify(&new, &node(2), IAT).unwrap();
459 match r.verify(&old, &node(1), IAT) {
460 Err(StandingError::Superseded { device, newest: 7, offered: 5 }) => assert_eq!(device.as_str(), DEVICE),
461 other => panic!("expected Superseded, got {other:?}"),
462 }
463 r.verify(&new, &node(2), YEARS_LATER).unwrap();
464 let mut elsewhere = binding(1, node(3));
466 elsewhere.device = DeviceId::new("node:b");
467 r.verify(&r.sign(&elsewhere), &node(3), IAT).unwrap();
468 }
469
470 #[test]
471 fn a_successor_heard_by_gossip_supersedes_without_being_presented() {
472 let r = rig();
473 let old = r.sign(&binding(5, node(1)));
474 pollster::block_on(r.edge.adopt(&r.sign(&binding(6, node(1))))).unwrap();
475 assert!(matches!(r.verify(&old, &node(1), IAT), Err(StandingError::Superseded { .. })));
476 }
477
478 #[test]
479 fn a_lower_sequence_adopted_after_a_higher_one_does_not_unsupersede() {
480 let r = rig();
481 let middle = r.sign(&binding(7, node(1)));
482 pollster::block_on(r.edge.adopt(&r.sign(&binding(9, node(1))))).unwrap();
483 pollster::block_on(r.edge.adopt(&r.sign(&binding(3, node(1))))).unwrap();
484 assert_eq!(r.edge.ledger().newest(&DeviceId::new(DEVICE)), Some(9));
485 assert!(matches!(
486 r.verify(&middle, &node(1), IAT),
487 Err(StandingError::Superseded { newest: 9, offered: 7, .. })
488 ));
489 let stale = LedgerDoc {
491 issuer: ISS.into(),
492 newest: [(DEVICE.to_owned(), 2)].into_iter().collect(),
493 };
494 r.edge.ledger().import(&stale).unwrap();
495 assert_eq!(r.edge.ledger().newest(&DeviceId::new(DEVICE)), Some(9));
496 }
497
498 #[test]
499 fn a_binding_is_refused_as_an_access_token() {
500 let r = rig();
501 let token = r.sign(&binding(5, node(1)));
502 let keys = KeySetVerifier::from_key_set(KeySet::from_doc(r.doc.clone()).unwrap());
503 let as_mcp = pollster::block_on(keys.verify::<serde_json::Value>(&token, IAT, ISS, None));
504 assert!(matches!(as_mcp, Err(VerifyError::SignatureMismatch)));
505 let pinned = PasetoV4PublicVerifier::from_public_key(&pub_bytes(&r.issuer)).unwrap();
506 assert!(pinned.verify_mcp_at(&token, IAT, "iss-1").is_err());
507 assert!(pinned.verify_at(&token, IAT).is_err());
508 }
509
510 #[test]
511 fn an_access_token_or_other_artifact_is_refused_as_a_binding() {
512 let r = rig();
513 let unasserted = sign_with(&r.issuer.secret, "iss-1", &binding(5, node(1)), None);
515 assert!(matches!(
516 r.verify(&unasserted, &node(1), IAT),
517 Err(StandingError::Verify(VerifyError::SignatureMismatch))
518 ));
519 let mcp = McpClaims::new(ISS, "https://aud.test", PrincipalId::user("alice"), IAT, IAT + 900, "j", vec![]);
521 let mcp = sign_with(&r.issuer.secret, "iss-1", &mcp, None);
522 assert!(matches!(r.verify(&mcp, &node(1), IAT), Err(StandingError::Verify(_))));
523 let claims = Claims::new(UserId::new("alice"), DeviceId::new(DEVICE), DeviceBinding::LanPair, IAT, IAT + 900)
525 .with_peer_key(node(1));
526 let session = sign_with(&r.issuer.secret, "iss-1", &serde_json::json!({ "cheers": claims }), None);
527 assert!(matches!(r.verify(&session, &node(1), IAT), Err(StandingError::Verify(_))));
528 let cross = sign_with(&r.issuer.secret, "iss-1", &binding(5, node(1)), Some(RevocationSet::IMPLICIT_ASSERTION));
530 assert!(matches!(
531 r.verify(&cross, &node(1), IAT),
532 Err(StandingError::Verify(VerifyError::SignatureMismatch))
533 ));
534 assert_eq!(r.edge.ledger().newest(&DeviceId::new(DEVICE)), None);
535 }
536
537 #[test]
538 fn a_non_issuer_role_key_is_refused() {
539 let r = rig();
540 let b = binding(5, node(1));
541 let ia = Some(StandingBinding::IMPLICIT_ASSERTION);
542 for (secret, kid) in [(&r.assertion.secret, "asr-1"), (&r.selfsig.secret, "self-1")] {
543 let token = sign_with(secret, kid, &b, ia);
544 assert!(matches!(
545 r.verify(&token, &node(1), IAT),
546 Err(StandingError::Verify(VerifyError::KeyRoleRejected { .. }))
547 ));
548 }
549 let mut foreign = b;
551 foreign.issuer = "https://elsewhere.test".into();
552 assert!(matches!(
553 r.verify(&r.sign(&foreign), &node(1), IAT),
554 Err(StandingError::Verify(VerifyError::BadIssuer { .. }))
555 ));
556 assert_eq!(r.edge.ledger().newest(&DeviceId::new(DEVICE)), None);
557 }
558
559 #[test]
560 fn ledger_export_import_round_trips() {
561 let r = rig();
562 let ledger = r.edge.ledger();
563 ledger.observe(&DeviceId::new("node:a"), 9);
564 ledger.observe(&DeviceId::new("node:b"), 4);
565 let json = serde_json::to_string(&ledger.export()).unwrap();
566
567 let back: LedgerDoc = serde_json::from_str(&json).unwrap();
568 assert_eq!(back, ledger.export());
569 let restored = BindingLedger::new(ISS);
570 restored.import(&back).unwrap();
571 assert_eq!(restored.export(), ledger.export());
572 assert_eq!(restored.newest(&DeviceId::new("node:a")), Some(9));
573 assert_eq!(restored.newest(&DeviceId::new("node:c")), None);
574
575 let other = BindingLedger::new(ISS);
577 other.observe(&DeviceId::new("node:a"), 12);
578 other.observe(&DeviceId::new("node:b"), 1);
579 restored.import(&other.export()).unwrap();
580 assert_eq!(restored.newest(&DeviceId::new("node:a")), Some(12));
581 assert_eq!(restored.newest(&DeviceId::new("node:b")), Some(4));
582
583 let foreign = BindingLedger::new("https://elsewhere.test");
584 assert!(restored.import(&foreign.export()).is_err());
585 }
586
587 #[test]
588 fn an_offline_restart_verifies_against_the_persisted_key_set() {
589 let r = rig();
590 let held = r.sign(&binding(7, node(1)));
591 let old = r.sign(&binding(5, node(1)));
592 r.verify(&held, &node(1), IAT).unwrap();
593 r.revoke(3, vec![Revoked::jti("bind-other", None)]);
594
595 let trust_json = serde_json::to_string(&r.edge.trust().export()).unwrap();
597 let ledger_json = serde_json::to_string(&r.edge.ledger().export()).unwrap();
598 let set = r.replica.export().unwrap();
599 drop(r.edge);
600
601 let rotated = JwksDoc {
604 keys: vec![JwkKey::ed25519("iss-2", &pub_bytes(&kp()), ISS, KeyRole::Issuer)],
605 };
606 let fetched = IssuerTrust::key_set(ISS, KeySetVerifier::from_key_set(KeySet::from_doc(rotated).unwrap()));
607 assert!(matches!(
608 pollster::block_on(fetched.verify::<StandingBinding>(&held)),
609 Err(VerifyError::UnknownKid(_))
610 ));
611
612 let doc: TrustDoc = serde_json::from_str(&trust_json).unwrap();
614 assert!(matches!(doc.anchor, AnchorDoc::KeySet(_)));
615 let trust = IssuerTrust::from_doc(doc).unwrap();
616 let replica = Arc::new(ReplicatedRevocations::new(trust.clone()));
617 pollster::block_on(replica.adopt(&set)).unwrap();
618 let edge = StandingVerifier::new(trust, replica);
619 edge.ledger().import(&serde_json::from_str(&ledger_json).unwrap()).unwrap();
620
621 let ok = pollster::block_on(edge.verify_standing_at(&held, &node(1), YEARS_LATER)).unwrap();
622 assert_eq!(ok.binding.seq, 7);
623 assert!(matches!(
625 pollster::block_on(edge.verify_standing_at(&old, &node(1), YEARS_LATER)),
626 Err(StandingError::Superseded { newest: 7, offered: 5, .. })
627 ));
628 let by_assertion = sign_with(&r.assertion.secret, "asr-1", &binding(8, node(1)), Some(StandingBinding::IMPLICIT_ASSERTION));
629 assert!(matches!(
630 pollster::block_on(edge.verify_standing_at(&by_assertion, &node(1), IAT)),
631 Err(StandingError::Verify(VerifyError::KeyRoleRejected { .. }))
632 ));
633 }
634
635 #[test]
636 fn a_pinned_trust_round_trips_through_its_document() {
637 let issuer = kp();
638 let pinned = IssuerTrust::pinned(ISS, PasetoV4PublicVerifier::from_public_key(&pub_bytes(&issuer)).unwrap());
639 let json = serde_json::to_string(&pinned.export()).unwrap();
640 let back = IssuerTrust::from_doc(serde_json::from_str(&json).unwrap()).unwrap();
641 let token = sign_with(&issuer.secret, "any", &binding(1, node(1)), Some(StandingBinding::IMPLICIT_ASSERTION));
642 let edge = StandingVerifier::new(back, Arc::new(ReplicatedRevocations::new(pinned)));
643 pollster::block_on(edge.verify_standing_at(&token, &node(1), YEARS_LATER)).unwrap();
644
645 let short = TrustDoc {
646 issuer: ISS.into(),
647 anchor: AnchorDoc::Pinned("AAAA".into()),
648 };
649 assert!(IssuerTrust::from_doc(short).is_err());
650 }
651}